Many NP-hard problems become polynomial on structured graphs, and many “is this realisable” questions have closed-form criteria. This note covers cacti, chordal and interval graphs, treewidth (with the treewidth-2 case), degree sequences (Erdős–Gallai, Gale–Ryser, Landau), short cycle counting, and planarity.

Cactus graphs

Every edge lies on at most one simple cycle (edge cactus). A DFS closes every cycle with exactly one back edge. The cactus tree (one node per vertex, one per cycle, joined to the cycle’s vertices; bridges kept as vertex–vertex edges) turns path questions into tree questions: e.g. the number of simple paths between and is , and DP over the tree handles independent sets, matchings and distances (for each cycle, a small DP around it).

struct Cactus {
    int n; bool isCactus = true; vector<vector<int>> cycles, tree; vector<int> cycleOfEdge;
    Cactus(int n, const vector<pair<int, int>>& E) : n(n), cycleOfEdge(sz(E), -1) {   // simple graph
        vector<vector<pair<int, int>>> g(n);
        for (int e = 0; e < sz(E); e++) g[E[e].first].push_back({E[e].second, e}), g[E[e].second].push_back({E[e].first, e});
        vector<int> st(n), par(n, -1), pe(n, -1), it(n);
        for (int s = 0; s < n; s++) {
            if (st[s]) continue;
            vector<int> stk{s}; st[s] = 1;
            while (sz(stk)) {
                int u = stk.back();
                if (it[u] == sz(g[u])) { st[u] = 2; stk.pop_back(); continue; }
                auto [v, e] = g[u][it[u]++];
                if (e == pe[u]) continue;
                if (!st[v]) { st[v] = 1, par[v] = u, pe[v] = e; stk.push_back(v); }
                else if (st[v] == 1) {                         // back edge closes a cycle
                    int k = sz(cycles); vector<int> cyc;
                    for (int x = u; x != v; x = par[x]) { cyc.push_back(x); if (cycleOfEdge[pe[x]] != -1) isCactus = false; cycleOfEdge[pe[x]] = k; }
                    cyc.push_back(v); reverse(all(cyc)); cycleOfEdge[e] = k; cycles.push_back(cyc);
                }
            }
        }
        tree.assign(n + sz(cycles), {});
        for (int k = 0; k < sz(cycles); k++) for (int v : cycles[k]) tree[v].push_back(n + k), tree[n + k].push_back(v);
        for (int e = 0; e < sz(E); e++) if (cycleOfEdge[e] == -1) tree[E[e].first].push_back(E[e].second), tree[E[e].second].push_back(E[e].first);
    }
};

For vertex cacti or general graphs, the block-cut tree plays the same role (Biconnectivity).

Chordal graphs

Every cycle of length has a chord. Equivalent: there is a perfect elimination ordering (PEO): each vertex’s later neighbours form a clique. Maximum cardinality search (always visit next the vertex with the most visited neighbours) produces, reversed, a PEO iff the graph is chordal. On chordal graphs: maximum clique, chromatic number (equal), maximum independent set, and the chromatic polynomial ( = later neighbours) are all linear. Interval graphs and trees are chordal.

struct Chordal {
    int n; vector<vector<int>> g; vector<int> peo, pos;
    Chordal(const vector<vector<int>>& adj) : n(sz(adj)), g(adj), pos(n) {      // maximum cardinality search
        vector<int> label(n), done(n), order; vector<vector<int>> bucket(n + 1); int top = 0;
        for (int v = 0; v < n; v++) bucket[0].push_back(v);
        while (sz(order) < n) {
            while (bucket[top].empty()) top--;
            int u = bucket[top].back(); bucket[top].pop_back();
            if (done[u] || label[u] != top) continue;
            done[u] = 1; order.push_back(u);
            for (int v : g[u]) if (!done[v]) bucket[++label[v]].push_back(v), top = max(top, label[v]);
        }
        peo.assign(order.rbegin(), order.rend());
        for (int i = 0; i < n; i++) pos[peo[i]] = i;
    }
    bool isChordal() const {                                 // the earliest later neighbour must see all other later neighbours
        vector<set<int>> adj(n); for (int u = 0; u < n; u++) adj[u] = set<int>(all(g[u]));
        for (int u = 0; u < n; u++) {
            int best = -1;
            for (int v : g[u]) if (pos[v] > pos[u] && (best < 0 || pos[v] < pos[best])) best = v;
            if (best >= 0) for (int v : g[u]) if (pos[v] > pos[u] && v != best && !adj[best].count(v)) return false;
        }
        return true;
    }
    vector<int> laterDegrees() const { vector<int> d(n); for (int i = 0; i < n; i++) for (int v : g[peo[i]]) d[i] += pos[v] > i; return d; }
    int maxClique() const { int b = n ? 1 : 0; for (int d : laterDegrees()) b = max(b, d + 1); return b; }
    vector<int> coloring() const {                           // optimal on chordal graphs: greedy in reverse PEO
        vector<int> col(n, -1);
        for (int i = n - 1; i >= 0; i--) {
            int u = peo[i]; vector<char> used(sz(g[u]) + 1);
            for (int v : g[u]) if (col[v] >= 0 && col[v] < sz(used)) used[col[v]] = 1;
            int c = 0; while (used[c]) c++; col[u] = c;
        }
        return col;
    }
};

Perfect graphs ( for every induced subgraph; chordal, bipartite, comparability, interval graphs and their complements): colouring, clique, independent set are polynomial (via the Lovász theta function in theory; via the class structure in practice). For comparability graphs (orientable as partial orders), max clique = longest chain and max independent set = largest antichain = min chain cover (Dilworth, Matching). Interval graphs: sort by right end, greedy for independent set and colouring (interval partitioning, Intervals).

Treewidth

A tree decomposition is a tree of bags covering every edge, with each vertex’s bags forming a connected subtree; the width is (max bag size) − 1. DP over a decomposition of width solves independent set / dominating set / colouring / Hamiltonicity in or . Trees have width 1; series-parallel graphs (no minor) width ; grids width ; planar graphs .

Width : repeatedly eliminate a vertex of degree , joining its two neighbours; the graph has treewidth iff this empties it. The bag of is plus its neighbours at elimination, attached to the bag of its neighbour eliminated first afterwards (Library Checker “Tree Decomposition (Width 2)”).

struct Treewidth2 {
    bool ok = false; vector<vector<int>> bags; vector<pair<int, int>> treeEdges;
    Treewidth2(int n, const vector<pair<int, int>>& E) {
        vector<set<int>> adj(n); for (auto [u, v] : E) if (u != v) adj[u].insert(v), adj[v].insert(u);
        vector<int> order, pos(n, -1), st; vector<vector<int>> nb(n);
        for (int v = 0; v < n; v++) if (sz(adj[v]) <= 2) st.push_back(v);
        while (sz(st)) {
            int v = st.back(); st.pop_back();
            if (pos[v] != -1 || sz(adj[v]) > 2) continue;
            pos[v] = sz(order); order.push_back(v); nb[v].assign(all(adj[v]));
            for (int u : nb[v]) adj[u].erase(v);
            if (sz(nb[v]) == 2) adj[nb[v][0]].insert(nb[v][1]), adj[nb[v][1]].insert(nb[v][0]);
            for (int u : nb[v]) if (sz(adj[u]) <= 2) st.push_back(u);
            adj[v].clear();
        }
        if (sz(order) < n) return;
        ok = true; bags.resize(n); int lastRoot = -1;
        for (int t = 0; t < n; t++) {
            int v = order[t]; bags[t] = {v}; bags[t].insert(bags[t].end(), all(nb[v]));
            int parent = -1; for (int u : nb[v]) if (parent < 0 || pos[u] < parent) parent = pos[u];
            if (parent >= 0) treeEdges.push_back({t, parent});
            else { if (lastRoot >= 0) treeEdges.push_back({lastRoot, t}); lastRoot = t; }
        }
    }
};

Degree sequences

QuestionCriterion
simple graph with degrees (Erdős–Gallai) even and for all
construct one (Havel–Hakimi)repeatedly join the largest-degree vertex to the next largest ones
bipartite with sides , (Gale–Ryser) and for all : (sum of the largest )
tournament score sequence (Landau)sorted ascending: with equality at
tree with degrees (build via Prüfer codes)
connected simple graphgraphical and with all

Tournament graphs (a complete graph with every edge given one direction) always have a king: a vertex that reaches every other vertex in at most 2 steps, and a Hamiltonian path always exists.

bool isGraphical(vector<ll> d) {                            // Erdős–Gallai, O(n log n)
    int n = sz(d); sort(all(d), greater<>()); ll tot = 0;
    for (ll x : d) { if (x < 0 || x > n - 1) return false; tot += x; }
    if (tot % 2) return false;
    vector<ll> suf(n + 1); for (int i = n - 1; i >= 0; i--) suf[i] = suf[i + 1] + d[i];
    ll left = 0;
    for (int k = 1; k <= n; k++) {
        left += d[k - 1];
        int lo = k, hi = n; while (lo < hi) { int m = (lo + hi) / 2; if (d[m] < k) hi = m; else lo = m + 1; }   // first index >= k with d < k
        if (left > (ll)k * (k - 1) + (ll)(lo - k) * k + suf[lo]) return false;
    }
    return true;
}
pair<bool, vector<pair<int, int>>> havelHakimi(const vector<ll>& d) {
    priority_queue<pair<ll, int>> pq; vector<pair<int, int>> E;
    for (int i = 0; i < sz(d); i++) { if (d[i] < 0) return {false, {}}; if (d[i]) pq.push({d[i], i}); }
    while (sz(pq)) {
        auto [k, v] = pq.top(); pq.pop(); vector<pair<ll, int>> take;
        for (ll j = 0; j < k; j++) { if (pq.empty()) return {false, {}}; take.push_back(pq.top()); pq.pop(); }
        for (auto [c, u] : take) { E.push_back({v, u}); if (c > 1) pq.push({c - 1, u}); }
    }
    return {true, E};
}
bool isTournamentScore(vector<ll> s) {
    sort(all(s)); ll sum = 0; int n = sz(s);
    for (int k = 1; k <= n; k++) { sum += s[k - 1]; if (sum < (ll)k * (k - 1) / 2) return false; }
    return sum == (ll)n * (n - 1) / 2;
}

Counting short cycles

  • Triangles: orient each edge from the lower endpoint to the higher; each vertex has out-neighbours, and every triangle is found once from its lowest vertex: (Sqrt).
  • 4-cycles: for each vertex as the highest-ranked vertex of the cycle, count paths with ranked lower than ; two such paths to the same close a 4-cycle: .
ll countFourCycles(int n, const vector<pair<int, int>>& E) {   // simple graph, cycles as edge sets
    vector<vector<int>> g(n); vector<int> deg(n);
    for (auto [u, v] : E) g[u].push_back(v), g[v].push_back(u), deg[u]++, deg[v]++;
    auto lower = [&](int a, int b) { return pair(deg[a], a) < pair(deg[b], b); };
    vector<ll> cnt(n); ll res = 0;
    for (int a = 0; a < n; a++) {
        for (int b : g[a]) if (lower(b, a)) for (int c : g[b]) if (lower(c, a)) res += cnt[c]++;
        for (int b : g[a]) if (lower(b, a)) for (int c : g[b]) cnt[c] = 0;
    }
    return res;
}

Planar graphs

  • (simple, ; if triangle-free), a vertex of degree , 4-colourable (Appel–Haken; 5-colourable constructively in linear time by Kempe chains), separators (Lipton–Tarjan), treewidth .
  • Kuratowski / Wagner: planar iff no subdivision (no minor) of or — testing this directly is hopeless, so every practical algorithm takes a structural route instead.
  • Exploiting planarity: dual graphs (min – cut = shortest path in the dual for on the outer face), face enumeration (Planar Faces), counting perfect matchings by Pfaffians (FKT, Determinants), exact algorithms for many NP-hard problems via the separator.
  • 3-colourability of planar graphs is still NP-complete; deciding 4-colourability is trivial (always yes, by Appel–Haken).

Testing planarity in

All of these need a biconnected decomposition first (Biconnectivity) and are substantial to implement from scratch:

MethodAddsAux. structureProduces an embedding?Extracts Kuratowski subgraph?
Hopcroft–Tarjan (path addition, 1974, the original)pathsa conflict stackyeshard
Lempel–Even–Cederbaum + PQ-treesvertices (st-numbering)PQ-treeyeshard
Boyer–Myrvold (edge addition, 2004)edgesbicomp lists with lazy flippingyesbuilt in, for free
de Fraysseix–Rosenstiehl (left-right criterion)——yesmoderate

If you actually need to implement this

Use Boyer–Myrvold or the left-right criterion, not Hopcroft–Tarjan — both are substantially easier (Hopcroft–Tarjan’s original presentation is notoriously terse), and Boyer–Myrvold additionally extracts an explicit / subdivision on failure, which constructive problems often want. These are the algorithms Boost Graph Library and networkx actually ship.

Where planarity shows up in contests: rarely as an explicit test — more often you’re told the graph is planar and expected to exploit , the separator, or the dual-graph min-cut/shortest-path correspondence, rather than to run a planarity tester yourself.