# graphcalc/graphs/invariants/core_invariants.py
# ============================================================
# GENERAL "CORE" FUNCTIONS (intersection of all optimum sets)
# ============================================================
from itertools import combinations
import networkx as nx
import graphcalc.graphs as gc
from graphcalc.metadata import invariant_metadata
__all__ = [
"core_set_minimum",
"core_number_minimum",
"core_set_maximum_fast",
"core_number_maximum_fast",
"alpha_core_set",
"alpha_core_number",
"clique_core_set",
"clique_core_number",
"domination_core_set",
"domination_core_number",
"zero_forcing_core_set",
"zero_forcing_core_number",
]
[docs]
def core_set_minimum(G, k_func, is_valid_set):
r"""
Compute the **minimum core**: the intersection of all optimal (minimum-cardinality) valid sets.
Many graph parameters are defined as the minimum size of a vertex set satisfying some property.
Examples include dominating sets, zero forcing sets, feedback vertex sets, etc. This function
takes such a property (via a membership oracle) and returns the set of vertices that appear in
**every** minimum-size solution.
Formal definition
-----------------
Let :math:`P` be a property of vertex subsets (e.g., “is a dominating set”), and suppose
:math:`k(G)` is the minimum cardinality of a set satisfying :math:`P`:
.. math::
k(G) \;=\; \min\{\, |S| : S \subseteq V(G),\ P(G,S)\,\}.
The **minimum core** (sometimes called the *core of minimum solutions*) is
.. math::
\operatorname{Core}_{\min}(G)
\;=\;
\bigcap\{\, S \subseteq V(G) : P(G,S)\ \text{and}\ |S|=k(G)\,\}.
Intuitively, :math:`\operatorname{Core}_{\min}(G)` is the set of vertices that are
*forced* to occur in every optimal solution.
Parameters
----------
G : networkx.Graph-like
A finite graph.
k_func : callable
A function ``k_func(G) -> int`` returning the optimal minimum size :math:`k(G)`.
Typical examples are ``gc.domination_number`` or ``gc.zero_forcing_number``.
is_valid_set : callable
A predicate ``is_valid_set(G, S) -> bool`` that decides the property :math:`P(G,S)`,
where ``S`` is an iterable of vertices (e.g. a tuple from combinations).
Important: ``is_valid_set`` should interpret ``S`` as a *vertex subset*; it should not
depend on order or multiplicity.
Returns
-------
set
The set :math:`\operatorname{Core}_{\min}(G)`.
Conventions:
- If :math:`|V(G)|=0`, returns the empty set.
- If :math:`k(G)=0`, returns the empty set (since the empty set is a minimum solution, and the intersection over the family of minimum solutions is empty).
- If the running intersection becomes empty during enumeration, the function returns early.
If no valid set of size :math:`k(G)` is found (which indicates an inconsistency between
``k_func`` and ``is_valid_set``), this implementation returns the empty set.
Raises
------
ValueError
If ``k_func(G)`` does not return an integer, or returns a value outside
:math:`[0, |V(G)|]`.
Notes
-----
- **Exact brute force.** This routine enumerates all :math:`k`-subsets of :math:`V(G)` and tests validity. It is intended only for small graphs and/or small :math:`k`.
- Correctness depends on consistency: ``k_func`` must return the true minimum size for the property tested by ``is_valid_set``. If they disagree, the output is not meaningful.
Complexity
----------
Let :math:`n=|V(G)|` and :math:`k=k(G)`. In the worst case, the function performs
:math:`\binom{n}{k}` calls to ``is_valid_set``. Thus the runtime is exponential in :math:`n`
in general, and also depends on the cost of the validity test itself.
"""
n = gc.order(G)
if n == 0:
return set()
k = k_func(G)
if not isinstance(k, int):
raise ValueError(f"k_func(G) must return an int, got {type(k)}")
if k < 0 or k > n:
raise ValueError(f"k_func(G) returned k={k}, but must satisfy 0 <= k <= n={n}")
if k == 0:
return set()
nodes = list(G.nodes())
core = None # running intersection over all minimum solutions found
for S in combinations(nodes, k):
if is_valid_set(G, S):
Sset = set(S)
core = Sset if core is None else (core & Sset)
if not core:
return set() # early exit: intersection already empty
# If k is correct, at least one valid set of size k should exist.
# If none were found, return empty set (signals inconsistency but stays safe).
return core if core is not None else set()
[docs]
def core_number_minimum(G, k_func, is_valid_set):
r"""
Compute the **minimum core number**: the size of the minimum core.
This is the cardinality of the intersection of all minimum-cardinality valid sets:
.. math::
c_{\min}(G) \;=\; \bigl|\operatorname{Core}_{\min}(G)\bigr|,
where :math:`\operatorname{Core}_{\min}(G)` is returned by :func:`core_set_minimum`.
Parameters
----------
G : networkx.Graph-like
A finite graph.
k_func : callable
A function ``k_func(G) -> int`` returning the optimal minimum size :math:`k(G)` for the chosen property.
is_valid_set : callable
A predicate ``is_valid_set(G, S) -> bool`` deciding validity of a candidate vertex set.
Returns
-------
int
The size of the minimum core, i.e. the number of vertices that appear in **every** minimum solution. Returns 0 for the empty graph, and also returns 0 when :math:`k(G)=0`.
Notes
-----
This is a thin wrapper around :func:`core_set_minimum`.
Complexity
----------
Same as :func:`core_set_minimum`.
"""
return len(core_set_minimum(G, k_func, is_valid_set))
[docs]
def core_set_maximum_fast(G, f_max):
r"""
Compute the **maximum core** (intersection of all maximum solutions) via vertex-deletion tests.
Many maximization parameters are defined as the optimum value attained by some vertex subset
(e.g., maximum independent set, maximum clique). For such parameters, one can often detect
whether a vertex is **forced** to appear in every maximum solution by checking whether deleting
that vertex decreases the optimum.
This function returns the set of vertices that belong to **every** maximum solution, under the
following (crucial) assumption:
Assumption (vertex-forcing by deletion)
---------------------------------------
For the parameter value
.. math::
M(G) \;=\; f_{\max}(G),
the following equivalence holds for every vertex :math:`v \in V(G)`:
.. math::
v \text{ belongs to every maximum solution } \iff f_{\max}(G - v) < f_{\max}(G),
where :math:`G - v` denotes the induced subgraph obtained by deleting :math:`v`.
Intuition:
- If deleting :math:`v` does **not** decrease the optimum value, then there exists an optimal solution contained entirely in :math:`V(G)\setminus\{v\}`; hence :math:`v` is not forced.
- If deleting :math:`v` **does** decrease the optimum value, then no optimal solution can avoid :math:`v`, so :math:`v` lies in the intersection of all optimal solutions.
For example, this equivalence holds for:
- :math:`\alpha(G)` (maximum independent set size): if :math:`\alpha(G-v) = \alpha(G)`, then there is a maximum independent set avoiding :math:`v`.
- :math:`\omega(G)` (maximum clique size): if :math:`\omega(G-v) = \omega(G)`, then there is a maximum clique avoiding :math:`v`.
Parameters
----------
G : networkx.Graph-like
A finite graph.
f_max : callable
A function ``f_max(G) -> int`` returning a maximization optimum value.
Typical examples include ``gc.independence_number`` (for :math:`\alpha`) or
``gc.clique_number`` (for :math:`\omega`).
The correctness of this routine depends on ``f_max`` satisfying the
*vertex-forcing by deletion* equivalence stated above.
Returns
-------
set
The **maximum core**:
.. math::
\operatorname{Core}_{\max}(G)
\;=\;
\bigcap\{\, S \subseteq V(G) : S \text{ is a maximum solution for } f_{\max} \,\},
i.e., the set of vertices that appear in **every** maximum solution.
If :math:`|V(G)| = 0`, returns the empty set.
Notes
-----
- This method is exact **provided the stated equivalence holds** for your parameter. It is not valid for arbitrary maximization invariants, especially those not realized by vertex subsets in a straightforward induced-subgraph way.
- Compared to enumerating all maximum solutions, this is typically much faster: it performs one deletion test per vertex.
Complexity
----------
The function performs :math:`|V(G)|` evaluations of ``f_max`` on induced subgraphs with one
vertex deleted. Thus, the total time is dominated by:
.. math::
O\!\left(|V(G)| \cdot T_{f_{\max}}(|V(G)|-1, |E(G-v)|)\right),
where :math:`T_{f_{\max}}` is the time needed to compute ``f_max`` on a graph.
The additional overhead from forming induced subgraphs is typically :math:`O(|V|+|E|)` per vertex
(depending on graph representation).
"""
n = gc.order(G)
if n == 0:
return set()
base = f_max(G)
node_set = set(G.nodes())
core = set()
for v in list(node_set):
H = G.subgraph(node_set - {v}).copy()
if f_max(H) < base:
core.add(v)
return core
[docs]
def core_number_maximum_fast(G, f_max):
r"""
Compute the **maximum core number**: the size of the maximum core.
This is the cardinality of the set of vertices that appear in every maximum solution:
.. math::
c_{\max}(G) \;=\; \bigl|\operatorname{Core}_{\max}(G)\bigr|,
where :math:`\operatorname{Core}_{\max}(G)` is returned by
:func:`core_set_maximum_fast`.
Parameters
----------
G : networkx.Graph-like
A finite graph.
f_max : callable
A function ``f_max(G) -> int`` satisfying the deletion-test equivalence described in
:func:`core_set_maximum_fast`.
Returns
-------
int
The size of the maximum core, i.e., the number of vertices forced to appear in **every**
maximum solution. Returns 0 for the empty graph.
Notes
-----
This is a thin wrapper around :func:`core_set_maximum_fast`.
Complexity
----------
Same as :func:`core_set_maximum_fast`.
"""
return len(core_set_maximum_fast(G, f_max))
[docs]
@invariant_metadata(
display_name="Alpha-core",
notation=r"\operatorname{core}_{\alpha}(G)",
category="independence",
aliases=("independence core",),
definition=(
"The alpha-core of a graph G is the intersection of all maximum "
"independent sets of G."
),
)
def alpha_core_set(G):
r"""
Compute the **α-core** of a graph: the intersection of all maximum independent sets.
Let :math:`\alpha(G)` denote the independence number of :math:`G`, i.e., the maximum size of an
independent set. The **α-core** (also called the *core with respect to maximum independent sets*)
is the set of vertices that belong to **every** maximum independent set:
.. math::
\operatorname{core}_\alpha(G)
\;=\;
\bigcap \{\, I \subseteq V(G) : I \text{ is an independent set and } |I|=\alpha(G) \,\}.
This implementation uses the standard deletion characterization for independence number:
.. math::
v \in \operatorname{core}_\alpha(G) \iff \alpha(G - v) < \alpha(G),
where :math:`G-v` is the induced subgraph obtained by deleting :math:`v`. Equivalently, if
:math:`\alpha(G-v)=\alpha(G)`, then there exists a maximum independent set avoiding :math:`v`,
so :math:`v` is not in the intersection of all maximum independent sets.
Parameters
----------
G : networkx.Graph-like
A finite graph.
Returns
-------
set
The α-core of :math:`G`, i.e., the set of vertices contained in every maximum independent set.
Returns the empty set if :math:`G` has no vertices.
Notes
-----
- The deletion test used here is exact for :math:`\alpha(G)` because any maximum independent set
of :math:`G-v` is also an independent set of :math:`G` that avoids :math:`v`.
- This method requires only :math:`|V(G)|` evaluations of :math:`\alpha(\cdot)` on vertex-deleted
induced subgraphs, and is typically much faster than enumerating all maximum independent sets.
Complexity
----------
Performs :math:`|V(G)|` calls to ``gc.independence_number`` on graphs with one vertex deleted.
Total runtime therefore depends on the complexity of computing :math:`\alpha(G)` in your backend.
Examples
--------
>>> import networkx as nx
>>> import graphcalc.graphs as gc
>>> # In a star, every maximum independent set consists of all leaves, so the α-core is the set of leaves.
>>> G = nx.star_graph(4) # 5 vertices: center + 4 leaves
>>> gc.alpha_core_set(G) == set(range(1, 5))
True
"""
return core_set_maximum_fast(G, gc.independence_number)
[docs]
@invariant_metadata(
display_name="Alpha-core number",
notation=r"\left|\operatorname{core}_{\alpha}(G)\right|",
category="independence",
aliases=("independence core number",),
definition=(
"The alpha-core number of a graph G is the cardinality of the "
"intersection of all maximum independent sets of G."
),
)
def alpha_core_number(G):
r"""
Compute the **α-core number** of a graph: the size of the intersection of all maximum independent sets.
If :math:`\operatorname{core}_\alpha(G)` denotes the α-core (the set of vertices contained in every
maximum independent set), this function returns its cardinality:
.. math::
|\operatorname{core}_\alpha(G)|
\;=\;
\left|\bigcap \{\, I \subseteq V(G) : I \text{ is an independent set and } |I|=\alpha(G) \,\}\right|.
Parameters
----------
G : networkx.Graph-like
A finite graph.
Returns
-------
int
The α-core number of :math:`G`. Returns 0 if :math:`G` has no vertices.
Notes
-----
This is a thin wrapper around :func:`alpha_core_set`.
Examples
--------
>>> import networkx as nx
>>> import graphcalc.graphs as gc
>>> G = nx.star_graph(4)
>>> gc.alpha_core_number(G)
4
"""
return len(alpha_core_set(G))
[docs]
@invariant_metadata(
display_name="Clique core",
notation=r"\operatorname{core}_{\omega}(G)",
category="cliques",
aliases=("omega-core",),
definition=(
"The clique core of a graph G is the intersection of all maximum "
"cliques of G."
),
)
def clique_core_set(G):
r"""
Compute the **clique core** of a graph :math:`G`: the intersection of all maximum cliques.
Let :math:`\omega(G)` denote the **clique number** of :math:`G`, i.e., the maximum size of a
clique in :math:`G`. The **clique core** is the set of vertices that appear in **every**
maximum clique:
.. math::
\operatorname{core}_\omega(G)
\;=\;
\bigcap \{\, C \subseteq V(G) : C \text{ is a clique in } G \text{ and } |C|=\omega(G)\,\}.
Equivalently, :math:`\operatorname{core}_\omega(G)` is the set of vertices that are *forced*
to occur in any maximum clique.
Implementation (deletion test)
------------------------------
This implementation uses the standard deletion characterization for the clique number:
.. math::
v \in \operatorname{core}_\omega(G) \iff \omega(G - v) < \omega(G),
where :math:`G-v` is the induced subgraph obtained by deleting :math:`v`.
Justification: if :math:`\omega(G-v)=\omega(G)`, then :math:`G-v` has a maximum clique of size
:math:`\omega(G)` that avoids :math:`v`, so :math:`v` is not in the intersection. Conversely, if
deleting :math:`v` reduces :math:`\omega`, then no maximum clique can avoid :math:`v`.
Parameters
----------
G : networkx.Graph-like
A finite graph. For standard clique semantics, this is intended for **simple undirected**
graphs.
Returns
-------
set
The clique core :math:`\operatorname{core}_\omega(G)`, i.e., the set of vertices contained in
every maximum clique. If :math:`G` has no vertices, returns the empty set.
Notes
-----
- If :math:`G` has a **unique** maximum clique, then the clique core equals that clique.
- If :math:`G` has multiple maximum cliques with little overlap, the clique core may be empty.
- Correctness of the deletion test is specific to parameters like :math:`\omega(G)` that are
realized by vertex subsets in induced subgraphs (a maximum clique of :math:`G-v` is also a clique
in :math:`G` that avoids :math:`v`).
Complexity
----------
Performs :math:`|V(G)|` calls to ``gc.clique_number`` on graphs with one vertex deleted. The total
runtime is therefore dominated by the complexity of computing :math:`\omega(G)` in your backend.
See Also
--------
clique_core_number : Returns the cardinality :math:`|\operatorname{core}_\omega(G)|`.
core_set_maximum_fast : Generic deletion-test routine used internally.
"""
return core_set_maximum_fast(G, gc.clique_number)
[docs]
@invariant_metadata(
display_name="Clique core number",
notation=r"\left|\operatorname{core}_{\omega}(G)\right|",
category="cliques",
aliases=("omega-core number",),
definition=(
"The clique core number of a graph G is the cardinality of the "
"intersection of all maximum cliques of G."
),
)
def clique_core_number(G):
r"""
Compute the **clique core number** of a graph :math:`G`: the size of the clique core.
If :math:`\operatorname{core}_\omega(G)` denotes the intersection of all maximum cliques of
:math:`G`, this function returns its cardinality:
.. math::
|\operatorname{core}_\omega(G)|
\;=\;
\left|\bigcap \{\, C \subseteq V(G) : C \text{ is a clique and } |C|=\omega(G)\,\}\right|.
Parameters
----------
G : networkx.Graph-like
A finite graph (intended for simple undirected graphs).
Returns
-------
int
The number of vertices that appear in every maximum clique of :math:`G`.
Returns 0 if :math:`G` has no vertices or if the clique core is empty.
Notes
-----
This quantity can be interpreted as a robustness measure for maximum cliques: it counts how many
vertices are unavoidable in any maximum clique.
Complexity
----------
Same as :func:`clique_core_set`, since this is a thin wrapper.
See Also
--------
clique_core_set : Returns the clique core set itself.
"""
return len(clique_core_set(G))
[docs]
@invariant_metadata(
display_name="Domination core",
notation=r"\operatorname{core}_{\gamma}(G)",
category="domination",
aliases=("gamma-core",),
definition=(
"The domination core of a graph G is the intersection of all minimum "
"dominating sets of G."
),
)
def domination_core_set(G):
r"""
Compute the **domination core** of a graph :math:`G`: the intersection of all minimum dominating sets.
Let :math:`\gamma(G)` denote the **domination number** of :math:`G`, i.e., the minimum size of a
dominating set. The **domination core** is the set of vertices that appear in **every**
minimum dominating set:
.. math::
\operatorname{core}_\gamma(G)
\;=\;
\bigcap \{\, S \subseteq V(G) : S \text{ is dominating in } G \text{ and } |S|=\gamma(G)\,\}.
Equivalently, :math:`\operatorname{core}_\gamma(G)` is the set of vertices that are *forced*
to occur in any minimum dominating set.
A set :math:`S \subseteq V(G)` is **dominating** if every vertex of :math:`G` is in :math:`S` or
has a neighbor in :math:`S`, i.e., :math:`N[S]=V(G)`.
Parameters
----------
G : networkx.Graph-like
A finite graph. This is intended primarily for **simple undirected** graphs under the standard adjacency notion.
Returns
-------
set
The domination core :math:`\operatorname{core}_\gamma(G)`.
Conventions:
- If :math:`|V(G)|=0`, returns the empty set.
- If :math:`\gamma(G)=0` (which occurs only for the empty graph under standard conventions), returns the empty set.
- If the intersection of all minimum dominating sets is empty, returns the empty set.
Notes
-----
- This function delegates to :func:`core_set_minimum` using: ``k_func = gc.domination_number`` and ``is_valid_set = gc.is_dominating_set``.
- The implementation is **exact** but may be expensive: it enumerates all :math:`\gamma(G)`-subsets of :math:`V(G)` and tests which ones are dominating.
Complexity
----------
Let :math:`n=|V(G)|` and :math:`k=\gamma(G)`. In the worst case, the function performs
:math:`\binom{n}{k}` dominance checks, so the runtime is exponential in :math:`n` in general
(and depends on the cost of ``gc.is_dominating_set``).
Examples
--------
>>> import networkx as nx
>>> import graphcalc.graphs as gc
>>> # In a star, every minimum dominating set is {center}, so the domination core is {center}.
>>> G = nx.star_graph(4)
>>> gc.domination_core_set(G) == {0}
True
"""
return core_set_minimum(G, gc.domination_number, gc.is_dominating_set)
[docs]
@invariant_metadata(
display_name="Domination core number",
notation=r"\left|\operatorname{core}_{\gamma}(G)\right|",
category="domination",
aliases=("gamma-core number",),
definition=(
"The domination core number of a graph G is the cardinality of the "
"intersection of all minimum dominating sets of G."
),
)
def domination_core_number(G):
r"""
Compute the **domination core number** of a graph :math:`G`: the size of the domination core.
If :math:`\operatorname{core}_\gamma(G)` denotes the intersection of all minimum dominating sets
of :math:`G`, this function returns its cardinality:
.. math::
|\operatorname{core}_\gamma(G)|
\;=\;
\left|\bigcap \{\, S \subseteq V(G) : S \text{ is dominating and } |S|=\gamma(G)\,\}\right|.
Parameters
----------
G : networkx.Graph-like
A finite graph.
Returns
-------
int
The number of vertices that appear in every minimum dominating set of :math:`G`.
Returns 0 if :math:`G` is empty or if the domination core is empty.
Notes
-----
This is a thin wrapper around :func:`domination_core_set`.
Complexity
----------
Same as :func:`domination_core_set`, since this function simply takes the set cardinality.
See Also
--------
domination_core_set : Returns the domination core set itself.
"""
return len(domination_core_set(G))
[docs]
@invariant_metadata(
display_name="Zero forcing core",
notation=r"\operatorname{core}_{Z}(G)",
category="zero forcing",
aliases=("Z-core",),
definition=(
"The zero forcing core of a graph G is the intersection of all minimum "
"zero forcing sets of G."
),
)
def zero_forcing_core_set(G):
r"""
Compute the **zero forcing core** of a graph :math:`G`: the intersection of all minimum zero forcing sets.
Let :math:`Z(G)` denote the **zero forcing number** of :math:`G`, i.e., the minimum size of a
zero forcing set under the standard zero forcing rule. The **zero forcing core** is the set of
vertices that appear in **every** minimum zero forcing set:
.. math::
\operatorname{core}_Z(G)
\;=\;
\bigcap \{\, S \subseteq V(G) : S \text{ is zero forcing in } G \text{ and } |S|=Z(G)\,\}.
Equivalently, :math:`\operatorname{core}_Z(G)` is the set of vertices that are *forced* to occur
in any minimum zero forcing set.
Zero forcing (brief definition)
-------------------------------
Start with a set :math:`S` of initially colored vertices. Repeatedly apply the **color-change rule**:
a colored vertex with **exactly one** uncolored neighbor forces that neighbor to become colored.
A set :math:`S` is a **zero forcing set** if this process eventually colors all vertices. The
zero forcing number :math:`Z(G)` is the minimum size of such a set.
Parameters
----------
G : networkx.Graph-like
A finite graph. This is intended primarily for **simple undirected** graphs under the standard adjacency notion.
Returns
-------
set
The zero forcing core :math:`\operatorname{core}_Z(G)`.
Conventions:
- If :math:`|V(G)|=0`, returns the empty set.
- If :math:`Z(G)=0` (which occurs only for the empty graph under standard conventions), returns the empty set.
- If the intersection of all minimum zero forcing sets is empty, returns the empty set.
Notes
-----
- This function delegates to :func:`core_set_minimum` using:
* ``k_func = gc.zero_forcing_number`` and
* ``is_valid_set = gc.is_zero_forcing_set``.
- The implementation is **exact** but may be expensive: it enumerates all :math:`Z(G)`-subsets of :math:`V(G)` and tests which ones are zero forcing.
Complexity
----------
Let :math:`n=|V(G)|` and :math:`k=Z(G)`. In the worst case, the function performs
:math:`\binom{n}{k}` zero-forcing checks, so the runtime is exponential in :math:`n` in general
(and depends on the cost of ``gc.is_zero_forcing_set``).
Examples
--------
>>> import networkx as nx
>>> import graphcalc.graphs as gc
>>> # In a path, there are minimum zero forcing sets of size 1 (either endpoint),
>>> # so the intersection is empty.
>>> G = nx.path_graph(6)
>>> gc.zero_forcing_core_set(G)
set()
"""
return core_set_minimum(G, gc.zero_forcing_number, gc.is_zero_forcing_set)
[docs]
@invariant_metadata(
display_name="Zero forcing core number",
notation=r"\left|\operatorname{core}_{Z}(G)\right|",
category="zero forcing",
aliases=("Z-core number",),
definition=(
"The zero forcing core number of a graph G is the cardinality of the "
"intersection of all minimum zero forcing sets of G."
),
)
def zero_forcing_core_number(G):
r"""
Compute the **zero forcing core number** of a graph :math:`G`: the size of the zero forcing core.
If :math:`\operatorname{core}_Z(G)` denotes the intersection of all minimum zero forcing sets of
:math:`G`, this function returns its cardinality:
.. math::
|\operatorname{core}_Z(G)|
\;=\;
\left|\bigcap \{\, S \subseteq V(G) : S \text{ is zero forcing and } |S|=Z(G)\,\}\right|.
Parameters
----------
G : networkx.Graph-like
A finite graph.
Returns
-------
int
The number of vertices that appear in every minimum zero forcing set of :math:`G`.
Returns 0 if :math:`G` is empty or if the zero forcing core is empty.
Notes
-----
This is a thin wrapper around :func:`zero_forcing_core_set`.
Complexity
----------
Same as :func:`zero_forcing_core_set`.
See Also
--------
zero_forcing_core_set : Returns the zero forcing core set itself.
"""
return len(zero_forcing_core_set(G))