cp-book

ecnerwala's competitive programming library

View the Project on GitHub ecnerwala/cp-book

:heavy_check_mark: verify/graph/two_edge_connected_components-spqr.test.cpp

Depends on

Code

// competitive-verifier: PROBLEM https://judge.yosupo.jp/problem/two_edge_connected_components

#include <bits/stdc++.h>
#include <cassert>

#include "graph/spqr_tree.hpp"

int main() {
	std::ios_base::sync_with_stdio(false), std::cin.tie(nullptr);

	int N, M; std::cin >> N >> M;
	std::vector<std::array<int, 2>> edges(M);
	for (auto& [x, y] : edges) std::cin >> x >> y;

	auto spqr = wala::spqr_tree::build(N, edges);
	using node_type = wala::spqr_tree::node_type;
	std::vector<std::pair<int, int>> stk; stk.reserve(N);

	std::vector<int> comp_verts(N, -1);
	std::vector<int> comp_bounds; comp_bounds.reserve(N+1); comp_bounds.push_back(0);

	for (int i = int(spqr.size()) - 1; i >= 0; i--) {
		int comp_end = -1;
		if (spqr.types[i] == node_type::V) {
			stk.push_back({i, spqr.orig_id[i]});
			if (spqr.par[i] == 0) {
				comp_end = spqr.subtree_end[i];
			}
		} else if (spqr.types[i] == node_type::I) {
			int p = spqr.par[i];
			assert(p == i-1 && spqr.types[p] == node_type::Q);
			comp_end = spqr.subtree_end[p];
		}
		if (comp_end != -1) {
			// Take off
			int idx = comp_bounds.back();
			while (!stk.empty() && stk.back().first < comp_end) {
				comp_verts[idx++] = stk.back().second;
				stk.pop_back();
			}
			comp_bounds.push_back(idx);
		}
	}
	assert(stk.empty());
	assert(comp_bounds.back() == N);

	int K = int(comp_bounds.size()) - 1;
	std::cout << K << '\n';
	for (int i = 0; i < K; i++) {
		std::cout << comp_bounds[i+1] - comp_bounds[i];
		for (int j = comp_bounds[i]; j < comp_bounds[i+1]; j++) {
			std::cout << ' ' << comp_verts[j];
		}
		std::cout << '\n';
	}

	return 0;
}
#include <bits/stdc++.h>
#line 1 "verify/graph/two_edge_connected_components-spqr.test.cpp"
// competitive-verifier: PROBLEM https://judge.yosupo.jp/problem/two_edge_connected_components

#line 5 "verify/graph/two_edge_connected_components-spqr.test.cpp"

#line 2 "src/graph/spqr_tree.hpp"

#line 14 "src/graph/spqr_tree.hpp"

namespace wala {

struct csr_index {
	std::vector<int> bounds;
	std::ranges::iota_view<int, int> indices(int i) const { return std::views::iota(bounds[i], bounds[i+1]); }
	template <std::ranges::contiguous_range R> auto slice(int i, R&& base) const {
		return std::span(base).subspan(bounds[i], bounds[i+1] - bounds[i]);
	}
	int num_rows() const { return bounds.empty() ? 0 : int(bounds.size()) - 1; }
	int num_entries() const { return bounds.empty() ? 0 : bounds.back(); }
};

template <typename T> struct csr : csr_index {
	std::vector<T> dat;
	std::span<T> operator [](int i) { return slice(i, dat); }
	std::span<const T> operator [](int i) const { return slice(i, dat); }
};

struct csr_index_builder {
	std::vector<int> bounds;
	csr_index_builder() = default;
	explicit csr_index_builder(int N) : bounds(N+1) {}
	void count(int k) { bounds[k+1]++; }
	csr_index finalize() && {
		for (int i = 1; i < int(bounds.size()); i++) {
			bounds[i] += bounds[i-1];
		}
		return {std::move(bounds)};
	}
};

template <typename T> struct csr_builder {
	csr_index idx;
	std::vector<T> dat;
	csr_builder() = default;
	explicit csr_builder(csr_index idx_, std::vector<T>&& dat_buf = {}) : idx(std::move(idx_)), dat(std::move(dat_buf)) {
		dat.resize(idx.num_entries());
		if (!idx.bounds.empty()) {
			idx.bounds.pop_back();
			idx.bounds.insert(idx.bounds.begin(), 0);
		}
	}
	explicit csr_builder(csr_index_builder&& idx_builder, std::vector<T>&& dat_buf = {}) : idx{std::move(idx_builder.bounds)}, dat(std::move(dat_buf)) {
		int l = 0;
		for (int i = 1; i < int(idx.bounds.size()); i++) {
			idx.bounds[i] = std::exchange(l, l + idx.bounds[i]);
		}
		dat.resize(l);
	}
	[[nodiscard]] T& push(int k) { return dat[idx.bounds[k+1]++]; }
	[[nodiscard]] csr<T> finalize() && { return { std::move(idx), std::move(dat) }; }
};

struct planar_spqr_tree;

struct spqr_tree {
	// The SPQR tree of a graph is a canonical/"maximal" decomposition of the graph by 2-vertex cuts.
	// The tree consists of nodes which are graphs of virtual edges (vedges), corresponding to nontrivial 2-vertex cuts.
	// Virtual edges are paired, and we can reassemble the graph by gluing nodes at their matching vedges (and removing the vedge).
	// Real edges are represented as special Q nodes which each contain exactly 1 real edge and exactly 1 vedge.
	//
	// Traditionally, the SPQR tree is defined for each biconnected component,
	// but we will embed the SPQR decompositions inside the block-cut tree to get a (rooted) decomposition of the entire graph.
	//
	// As such, we will have a tree of "items", which consist of SPQR nodes, real vertices, and a special "forest root" item:
	//  - Each vertex will be a child of the topmost node which contains it (or the forest root).
	//  - Each block will be a subtree of nodes rooted at a Q edge, which is the child of one of its vertices.
	//
	// Item types:
	//  F - forest - a root node corresponding to the whole forest.
	//  V - vertex - not really a node, just there because they're mixed into the tree a la block/cut tree
	//  Q - real edge - has exactly 1 vedge and 1 real edge
	//  I - bridge - has exactly 1 vedge connecting to a bridge Q node
	//  O - self-loop - has exactly 1 vedge connecting to a self-loop Q node
	//  S - series - a cycle of >= 3 vedges; note that any 2 vertices of the cycle form a cut
	//  P - parallel - a parallel group of >= 3 vedges with the same endpoints
	//  R - rigid - a 3-vertex-connected component
	//
	// Q nodes occur in 2 places: block roots and block leaves.
	// Block leaf Q's simply have no children.
	// Block root Q's have 2 children: their vedge, and their deeper vertex (unless it's a self-loop).
	//
	// Degenerate blocks:
	//  - a block consisting of a self-loop is a Q node connected to an O node.
	//  - a block consisting of a bridge is a Q node connected to an I node.
	//  - a block consisting of exactly 2 parallel edges is represented by 2 glued Q nodes.
	//
	// We have several id spaces:
	//  - items are in preorder
	//  - node_verts (nv's) are each node's vertices, given in node order then s-t order.
	//  - node_edges (ne's) are each node's vedges, given in node order then a s-t order.
	//  - node_adj is each node_vert's incident vedges, given as 2 lists per nv: left/rightwards edges each in reverse s-t order.
	//  - original verts and original edges can be converted to items as vert_item / edge_item
	//
	// Children of a node will be sorted in s-t order.
	// Specifically vertices are sorted, and edges are guaranteed to satisfy the strong "dominance" partial order:
	// if a.nvs[0] <= b.nvs[0] and a.nvs[1] <= b.nvs[1], then a <= b. (In practice, we'll sort by midpoint.)
	// Adjacency lists are sorted as "center-is-longest", which helps make laminar/bracket cases clean.
	//   (5->4) (5->3) (5->2) (5->1) *vertex 5* (5->9) (5->8) (5->7) (5->6)
	// More specifically, node_adj contains two lists per vertex: 2*nv+0 is leftwards and 2*nv+1 is rightwards.
	//
	// All id's are item indices unless clearly nv/ne id's.
	//
	// In general, there are 2 ways to use the SPQR tree: the rooted view and the unrooted view.
	//  - The rooted view uses par / ch walks, and either treats the tree as 1 top-down big decomposition, or walks in paths up/down the tree with LCA-like queries.
	//  - The unrooted view mostly uses nv/ne/nd lists and works locally within a node/sometimes jumps between them.

	enum class node_type : char {
		F = 'F', V = 'V', Q = 'Q', I = 'I', O = 'O', S = 'S', P = 'P', R = 'R'
	};
	friend std::ostream& operator<<(std::ostream& o, node_type t) { return o << char(t); }

	std::vector<int> vert_index;
	std::vector<int> edge_index;
	std::vector<bool> edge_flipped;

	std::vector<int> par;
	std::vector<int> subtree_end;
	std::vector<node_type> types;
	std::vector<int> orig_id;

	csr<int> ch;
	struct node_vert_t {
		int node;
		int vert;
	};
	std::vector<node_vert_t> node_verts;
	csr_index node_nvs;
	// The nv index of a vertex within its parent node
	std::vector<int> vert_par_nv;
	// TODO: Should we store a vert_nodes CSR?

	struct node_edge_t {
		int node;
		int twin_ne;
		// TODO: Should we store the twin node, the twin node type, and/or twin node type == Q?

		std::array<int, 2> nvs;
	};
	std::vector<node_edge_t> node_edges;
	csr_index node_nes;

	struct node_adj_t {
		int ne;
		int dest_nv;
	};
	csr<node_adj_t> node_adj;

	int size() const { return int(par.size()); }

	// vert_order and edge_order are (prefixes of) permutations of vertex / edge ids;
	// listed ids are visited first in the given order, then the rest in id order.
	// Roots are the first unvisited vertices, and DFS children are explored in edge order.
	// Use planar_spqr_tree::build to also compute the planar embeddings.
	static spqr_tree build(
		int NV,
		const std::vector<std::array<int, 2>>& edges,
		bool ternarize = false,
		std::span<const int> vert_order = {},
		std::span<const int> edge_order = {}
	) {
		return build_impl<false>(NV, edges, ternarize, vert_order, edge_order);
	}

protected:
	template <bool with_planarity>
	static std::conditional_t<with_planarity, planar_spqr_tree, spqr_tree> build_impl(
		int NV,
		const std::vector<std::array<int, 2>>& edges,
		bool ternarize,
		std::span<const int> vert_order,
		std::span<const int> edge_order
	);
};

struct planar_embedding {
	// We'll split our edges up into "quarter-edges", indexed according to
	//   4 * edge + 2 * side + dir, where side is v0 vs v1, and dir is cw vs ccw
	//
	//       1     2
	//    v0 ---e--- v1
	//       0     3
	//
	// We can think of a planar embedding as a collection of 3 involutions on quarter-edges:
	// * qe <-> qe ^ 1 maps quarter edges to their opposite side around the endpoint vertex.
	// * qe <-> qe ^ 3 maps quarter edges to their opposite side along the edge (around the face).
	// * qe <-> rot_adj[qe] maps quarter edges to their facing pair.
	// Walking around a vertex is alternating qe ^ 1 and rot_adj[qe], and walking around a face is qe ^ 3 and rot_adj[qe].
	//
	// Partial embeddings are represented with -1's in the rot_adj array.
	// NB: Helpers do not support -1's. It is up to the user to not access these entries!
	std::vector<int> rot_adj;
};

struct planar_spqr_tree : spqr_tree {
	std::vector<bool> node_planar;
	// Planarity adjacencies: ne_rot_adj is an involution of facing quarter-edges, indexed according to:
	// ne_rot_adj[4 * node_edge + 2 * side + dir]
	// Nonplanar nodes have all entries -1.
	planar_embedding ne_embedding;

	static planar_spqr_tree build(
		int NV,
		const std::vector<std::array<int, 2>>& edges,
		bool ternarize = false,
		std::span<const int> vert_order = {},
		std::span<const int> edge_order = {}
	) {
		return build_impl<true>(NV, edges, ternarize, vert_order, edge_order);
	}
};

// Phase 1: build a DFS skeleton with outedges sorted by lowval
struct lowval_storted_skeleton_t {
	std::vector<int> roots;
	struct key_t { int lowval; bool is_tree; bool is_type_1; };
	struct packed_key_t {
		int v;
		friend auto operator <=> (packed_key_t a, packed_key_t b) = default;
		[[nodiscard]] bool is_new_block() const { return v < 6; }
		[[nodiscard]] bool is_type_2() const { return v % 3 == 2; }
		[[nodiscard]] key_t unpack(int cur_depth) const {
			int lowval = v / 3 - 2; if (lowval < 0) lowval = cur_depth + ~lowval;
			int kind = v % 3;
			bool is_tree = kind != 1;
			bool is_type_1 = kind <= 1;
			return {lowval, is_tree, is_type_1 };
		}
	};
	struct outedge_t { int src, dest; int e_side; packed_key_t key; };
	csr<outedge_t> outedges;

	static lowval_storted_skeleton_t build(
		int NV,
		const std::vector<std::array<int, 2>>& edges,
		std::span<const int> vert_order,
		std::span<const int> edge_order
	) {
		// std::min is by reference, which breaks some optimizations
		auto min = [](auto a, auto b) { return a < b ? a : b; };

		int NE = int(edges.size());
		assert(int(vert_order.size()) <= NV);
		assert(int(edge_order.size()) <= NE);

		// Calls f(i) for i in order, then for the remaining i in [0, n) in increasing order.
		auto for_each_in_order = [] [[gnu::always_inline]] (int n, std::span<const int> order, auto f) -> void {
			for (int i : order) f(i);
			if (int(order.size()) == n) return;
			if (order.empty()) {
				for (int i = 0; i < n; i++) f(i);
			} else if (order.size() == 1) {
				for (int i = 0; i < n; i++) {
					if (i != order[0]) f(i);
				}
			} else {
				std::vector<bool> listed(n);
				for (int i : order) listed[i] = true;
				for (int i = 0; i < n; i++) {
					if (!listed[i]) f(i);
				}
			}
		};

		std::vector<int> roots; roots.reserve(NV);
		csr<outedge_t> outedges;
		{
			std::vector<int> depth(NV, -1);
			// 1a: build a normal adjacency list for the initial lowval dfs
			struct edge_t { int dest; int e; };
			csr_index_builder adj_idx_builder(NV);
			for (auto [u, v] : edges) {
				adj_idx_builder.count(u);
				if (u != v) adj_idx_builder.count(v);
			}
			csr_builder<edge_t> adj_builder(std::move(adj_idx_builder));
			for_each_in_order(NE, edge_order, [&] [[gnu::always_inline]] (int e) -> void {
				auto [u, v] = edges[e];
				adj_builder.push(u) = {v, 2 * e + 0};
				if (u != v) adj_builder.push(v) = {u, 2 * e + 1};
			});
			auto adj = std::move(adj_builder).finalize();

			std::vector<outedge_t> all_outedges; all_outedges.reserve(NE);
			// Return the 2 lowvals from this subtree
			struct dfs_stack_t {
				int cur;
				int prv_e;
				std::array<int, 2> lowvals;
				int ch_idx;
				int ch_end;
			};
			std::vector<dfs_stack_t> stk; stk.reserve(NV);
			auto push_vert = [&] [[gnu::always_inline]] (int cur, int prv_e) -> void {
				int d = int(stk.size());
				depth[cur] = d;
				stk.push_back({cur, prv_e, {d, d}, adj.bounds[cur], adj.bounds[cur+1]});
			};
			auto finish_edge = [&] [[gnu::always_inline]] (bool is_tree, std::array<int, 2> n_lowvals) -> void {
				int d = int(stk.size()) - 1;
				auto& s = stk.back();
				int cur = s.cur;
				assert(s.ch_idx < s.ch_end);
				auto [nxt, e] = adj.dat[s.ch_idx];
				auto& lowvals = s.lowvals;
				s.ch_idx++;

				{
					// Extra bit is 0 for type-1 children, 1 for backedges, 2 for children with lowval2
					// Bridges have lowval -2 (kind 0), and components loops have lowval -1 (components are kind 0, loops are kind 1)
					// We group all backedges together/last to avoid breaking a straight-line graph embedding
					int lowval = n_lowvals[0];
					if (lowval >= d) lowval = ~(lowval - d);
					int kind = 2 * (n_lowvals[1] < d) + !is_tree;
					all_outedges.push_back({cur, nxt, e, packed_key_t{3 * (lowval + 2) + kind}});
				}

				// Keep the 2 distinct mins
				if (n_lowvals[0] < lowvals[0]) lowvals = {n_lowvals[0], min(n_lowvals[1], lowvals[0])};
				else lowvals[1] = min(lowvals[1], n_lowvals[0] == lowvals[0] ? n_lowvals[1] : n_lowvals[0]);
			};
			auto start_edge = [&] [[gnu::always_inline]] () -> void {
				int d = int(stk.size()) - 1;
				auto& s = stk.back();
				assert(s.ch_idx < s.ch_end);
				auto [nxt, e] = adj.dat[s.ch_idx];

				if ((e ^ 1) == s.prv_e || depth[nxt] > d) {
					// skip the edge
					s.ch_idx++; return;
				}

				bool is_tree = depth[nxt] == -1;
				if (is_tree) {
					push_vert(nxt, e);
				} else {
					finish_edge(false, {depth[nxt], d});
				}
			};
			auto pop_vert = [&] [[gnu::always_inline]] () -> std::array<int, 2> {
				auto lowvals = stk.back().lowvals;
				stk.pop_back();
				return lowvals;
			};
			for_each_in_order(NV, vert_order, [&] [[gnu::always_inline]] (int rt) -> void {
				if (depth[rt] == -1) {
					roots.push_back(rt);
					push_vert(rt, -1);
					while (true) {
						if (stk.back().ch_idx == stk.back().ch_end) {
							auto lowvals = pop_vert();
							if (stk.empty()) break;
							finish_edge(true, lowvals);
						} else {
							start_edge();
						}
					}
				}
			});

			csr_index_builder by_key_idx_builder(3*NV+6);
			for (auto edge : all_outedges) by_key_idx_builder.count(edge.key.v);
			csr_builder<outedge_t> by_key_builder(std::move(by_key_idx_builder));
			for (auto edge : all_outedges) by_key_builder.push(edge.key.v) = edge;
			csr<outedge_t> by_key = std::move(by_key_builder).finalize();

			csr_index_builder by_src_idx_builder(NV);
			for (auto edge : by_key.dat) by_src_idx_builder.count(edge.src);
			csr_builder<outedge_t> by_src_builder(std::move(by_src_idx_builder), std::move(all_outedges));
			for (auto edge : by_key.dat) by_src_builder.push(edge.src) = edge;
			outedges = std::move(by_src_builder).finalize();
		}

		return {std::move(roots), std::move(outedges)};
	}
};

template <bool with_planarity>
std::conditional_t<with_planarity, planar_spqr_tree, spqr_tree> spqr_tree::build_impl(
	int NV,
	const std::vector<std::array<int, 2>>& edges,
	bool ternarize,
	std::span<const int> vert_order,
	std::span<const int> edge_order
) {
	// std::min is by reference, which breaks some optimizations
	auto setmin = [](auto& a, auto b) { if (b < a) a = b; };

	int NE = int(edges.size());
	assert(int(vert_order.size()) <= NV);
	assert(int(edge_order.size()) <= NE);

	auto [roots, outedges] = lowval_storted_skeleton_t::build(NV, edges, vert_order, edge_order);

	// Phase 2: do the big ear-decomposition-like walk

	// We're going to build a tree of all SPQR *nodes* + all original *vertices* (collectively *items*).
	// Vertices will hang off the first SPQR node containing them, and blocks will be rooted at a topmost Q node for the top edge.

	// As we build, we will represent the children of our nodes/vertices as linked lists.
	constexpr int ROOT_ITEM = 0;
	auto vert_item = [&] [[gnu::always_inline]] (int v) -> int { return 1 + v; };
	auto edge_item = [&] [[gnu::always_inline]] (int e) -> int { return 1 + NV + e; };

	// Helpers for working with std::array<T, 2> - these compile to cmov's better than direct index access.

	// return arr[dir] == a, arr[!dir] == b
	auto set_sides = []<typename T>(bool dir, T a, T b) -> std::array<T, 2> {
		return dir ? std::array<T, 2>{b, a} : std::array<T, 2>{a, b};
	};
	auto get_side = []<typename T>(std::array<T, 2> a, bool dir) -> T {
		return dir ? a[1] : a[0];
	};

	struct item_list {
		// Items are actually 2 * item + planarity_flip (always 0 without planarity)
		std::array<int, 2> v{-1, -1};

		[[nodiscard]] bool empty() const { return v[0] < 0; }
	};
	std::vector<int> ch_nxt; ch_nxt.reserve(1 + NV + NE + NE); ch_nxt.assign(1 + NV + NE, -1);
	auto concat = [&] [[gnu::always_inline]] (item_list a, item_list b) -> item_list {
		if (b.empty()) return a;
		if (a.empty()) return b;
		ch_nxt[a.v[1] >> 1] = b.v[0] ^ (a.v[1] & 1);
		return {{a.v[0], b.v[1]}};
	};
	auto unit_list = [&] [[gnu::always_inline]] (int item) -> item_list {
		return {{item << 1, item << 1}};
	};

	std::vector<std::array<int, 2>> item_vs; item_vs.reserve(1 + NV + 2 * NE); item_vs.resize(1 + NV + NE, {-1, -1});
	std::vector<item_list> item_ch; item_ch.reserve(1 + NV + 2 * NE); item_ch.resize(1 + NV + NE, item_list{});
	std::vector<node_type> item_types; item_types.reserve(1 + NV + 2 * NE);
	item_types.resize(1, node_type::F);
	item_types.resize(1 + NV, node_type::V);
	item_types.resize(1 + NV + NE, node_type::Q);

	// Quarter edges for planar embedding building.
	// Each vedge has 4 entries by 4 * vedge_id + 2 * source_vert + is_cw (is_cw is arbitrary)
	// vedges are identified with what item they cap, numbered by (item - 1 - NV)
	std::vector<int> quarter_edge_matches(with_planarity ? 8 * NE + 4 : 0, -1);
	struct nonplanarity_certficate_t {};
	std::vector<std::expected<std::array<int, 4>, nonplanarity_certficate_t>> node_planarity;
	if constexpr (with_planarity) node_planarity.reserve(NE);

	int tot_blocks = 0;
	int tot_self_loops = 0;

	{
		auto alloc_item = [&] [[gnu::always_inline]] (node_type type) -> int {
			int item = int(item_vs.size());
			item_vs.push_back({});
			item_ch.push_back({});
			item_types.push_back(type);
			ch_nxt.push_back(-1);
			if constexpr (with_planarity) node_planarity.emplace_back();
			return item;
		};

		// Declare these here: most of our code will be in terms of v_start / top_depth, so we'll want to read these out
		std::vector<int> stack_verts(NV);
		std::vector<int8_t> stack_dir(NV); // really bool, but I don't want vector<bool>

		auto make_vs = [&] [[gnu::always_inline]] (int v_start, int top_depth) -> std::array<int, 2> {
			return set_sides(stack_dir[top_depth], stack_verts[top_depth], v_start);
		};

		int nxt_edge_idx = 0; // Counts backedges only
		std::vector<int> first_occurrence(NV); // First backedge to this depth

		std::vector<int> edge_top_depths(with_planarity ? 2 * NE : 0, -1);

		struct tstack_planarity_side_t {
			// For each side, store pointers to the "linked lists" of the edges inside.
			// v[0] is the outer / longer edges and v[1] is the inner / shorter edges, matching the outside-in sort order.

			// bot_ends are the outer/innermost exposed pieces of the walk down the ear in the tree (they're connected to the bottommost/topmost vertices of the tree path)
			std::array<int, 2> bot_ends{-1, -1};
			struct top_t {
				int end = -1;
				int depth = -1;
			};
			// top_ends are the outer/innermost exposed backedges
			// depths should be increasing going inwards
			std::array<top_t, 2> tops{top_t{-1, -1}, top_t{-1, -1}};
		};
		struct tstack_planarity_t {
			// The convention is that sides[0].tops[0].depth == top_depth, i.e. at least one minimal return lives on side 0
			std::array<tstack_planarity_side_t, 2> sides;
		};
		struct tstack_nonplanarity_t {
			// TODO: What's the nonplanarity certificate look like?
		};
		using tstack_maybe_planarity_t = std::conditional_t<with_planarity, std::expected<tstack_planarity_t, tstack_nonplanarity_t>, std::monostate>;
		auto merge_planarity = [&] [[gnu::always_inline]] (tstack_maybe_planarity_t& a, const tstack_maybe_planarity_t& b) -> void {
			if constexpr (with_planarity) {
				if (!a) return;
				if (!b) { a = b; return; }
				for (int z = 0; z < 2; z++) {
					auto& as = a->sides[z];
					const auto& bs = b->sides[z];
					// If there's no bottom edges, then we must be an isolated vertex, so we can end early.
					if (bs.bot_ends[0] == -1) {
						// Do nothing
					} else if (as.bot_ends[0] == -1) {
						as = bs;
					} else {
						quarter_edge_matches[as.bot_ends[1]] = bs.bot_ends[0];
						quarter_edge_matches[bs.bot_ends[0]] = as.bot_ends[1];
						as.bot_ends[1] = bs.bot_ends[1];

						if (bs.tops[0].end == -1) {
							// Do nothing
						} else if (as.tops[0].end == -1) {
							as.tops = bs.tops;
						} else {
							// Caller must check that we're planar
							assert(as.tops[1].depth <= bs.tops[0].depth);
							quarter_edge_matches[as.tops[1].end] = bs.tops[0].end;
							quarter_edge_matches[bs.tops[0].end] = as.tops[1].end;
							as.tops[1] = bs.tops[1];
						}
					}
				}
			}
		};
		auto make_edge_planarity = [&] [[gnu::always_inline]] (int item, int top_depth, bool is_tree) -> tstack_maybe_planarity_t {
			if constexpr (with_planarity) {
				assert(item >= 1 + NV);
				int ve = item - (1 + NV);
				bool top_dir = stack_dir[top_depth];
				edge_top_depths[ve] = top_depth;
				tstack_planarity_t p;
				if (is_tree) {
					p.sides[0].bot_ends = {4 * ve + 2 * !top_dir + 0, 4 * ve + 2 * top_dir + 1};
					p.sides[1].bot_ends = {4 * ve + 2 * !top_dir + 1, 4 * ve + 2 * top_dir + 0};
				} else {
					p.sides[0].bot_ends = {4 * ve + 2 * !top_dir + 0, 4 * ve + 2 * !top_dir + 1};
					p.sides[0].tops = {{{4 * ve + 2 * top_dir + 1, top_depth}, {4 * ve + 2 * top_dir + 0, top_depth}}};
				}
				return p;
			} else {
				return {};
			}
		};
		struct tstack_t {
			int v_start = -1;
			int top_depth = -1;
			int first_idx = -1;
			std::array<item_list, 2> spans;
			[[no_unique_address]] tstack_maybe_planarity_t planarity;
		};
		std::vector<tstack_t> tstack; tstack.reserve(NV + NE);
		auto cur_tstack = [&] [[gnu::always_inline]] () -> tstack_t& { return tstack.end()[-1]; };
		auto nxt_tstack = [&] [[gnu::always_inline]] () -> tstack_t& { return tstack.end()[-2]; };

		auto push_tstack = [&] [[gnu::always_inline]] (int v_start, int top_depth, int item, tstack_maybe_planarity_t planarity) -> void {
			tstack.emplace_back(v_start, top_depth, nxt_edge_idx, set_sides(stack_dir[top_depth], unit_list(item), {}), planarity);
		};
		auto push_vert_tstack = [&] [[gnu::always_inline]] (int v, int top_depth) -> void {
			int item = vert_item(v);
			push_tstack(v, top_depth, item, {});
		};
		auto push_edge_tstack = [&] [[gnu::always_inline]] (int v_start, int top_depth, int e, bool is_tree) -> void {
			int item = edge_item(e);
			push_tstack(v_start, top_depth, item, make_edge_planarity(item, top_depth, is_tree));
		};
		auto flip_tstack_planarity = [&] [[gnu::always_inline]] (tstack_t& a) -> void {
			if constexpr (with_planarity) {
				a.spans[0].v[0] ^= 1;
				a.spans[0].v[1] ^= 1;
				a.spans[1].v[0] ^= 1;
				a.spans[1].v[1] ^= 1;
				if (a.planarity) {
					std::swap(a.planarity->sides[0], a.planarity->sides[1]);
				}
			}
		};
		auto merge_tstack_tops = [&] [[gnu::always_inline]] () -> void {
			tstack_t& a = nxt_tstack();
			const tstack_t& b = cur_tstack();
			setmin(a.top_depth, b.top_depth);
			a.spans[0] = concat(b.spans[0], a.spans[0]);
			a.spans[1] = concat(a.spans[1], b.spans[1]);
			if constexpr (with_planarity) {
				merge_planarity(a.planarity, b.planarity);
			}
			tstack.pop_back();
		};

		auto maybe_unwrap_nxt = [&] [[gnu::always_inline]] (node_type type, bool is_tree) -> int {
			tstack_t& t = nxt_tstack();

			if (type == node_type::R) return alloc_item(type);

			assert(type == node_type::P || type == node_type::S);

			// If we want to ternarize, never reuse.
			if (ternarize) return alloc_item(type);

			bool top_dir = stack_dir[t.top_depth];
			assert(get_side(t.spans, !top_dir).empty());
			int item = get_side(t.spans, top_dir).v[0] >> 1;
			assert(item == (get_side(t.spans, top_dir).v[1] >> 1));
			if (item_types[item] == type) {
				t.spans = set_sides(top_dir, item_ch[item], {});
				if constexpr (with_planarity) {
					// Unwrap the planarity data
					// We don't really need to maintain this at all because S/P nodes are known to be trivially planar
					// The current state is just make_edge_planarity(wrapped), which means that it has the right shape, just needs to be relabelled.
					assert(node_planarity[item - (1 + NV + NE)]);
					const auto& matches = *node_planarity[item - (1 + NV + NE)];
					assert(t.planarity);
					auto& p = *t.planarity;
					if (is_tree) {
						p.sides[0].bot_ends[0] = matches[2 * !top_dir + 1];
						p.sides[0].bot_ends[1] = matches[2 * top_dir + 0];
						p.sides[1].bot_ends[0] = matches[2 * !top_dir + 0];
						p.sides[1].bot_ends[1] = matches[2 * top_dir + 1];
					} else {
						p.sides[0].bot_ends[0] = matches[2 * !top_dir + 1];
						p.sides[0].bot_ends[1] = matches[2 * !top_dir + 0];
						p.sides[0].tops[0].end = matches[2 * top_dir + 0];
						p.sides[0].tops[1].end = matches[2 * top_dir + 1];
					}
				}
				return item;
			} else {
				return alloc_item(type);
			}
		};

		auto finish_tstack_top = [&] [[gnu::always_inline]] (int item, bool is_tree) -> void {
			tstack_t& t = cur_tstack();
			bool top_dir = stack_dir[t.top_depth];
			assert(get_side(t.spans, !top_dir).empty());

			if constexpr (with_planarity) {
				if (t.planarity) {
					const auto& p = *t.planarity;
					std::array<int, 4> matches{};
					if (is_tree) {
						matches[2 * !top_dir + 1] = p.sides[0].bot_ends[0];
						matches[2 * top_dir + 0] = p.sides[0].bot_ends[1];
						matches[2 * !top_dir + 0] = p.sides[1].bot_ends[0];
						matches[2 * top_dir + 1] = p.sides[1].bot_ends[1];
					} else {
						matches[2 * !top_dir + 1] = p.sides[0].bot_ends[0];
						matches[2 * !top_dir + 0] = p.sides[0].bot_ends[1];
						matches[2 * top_dir + 0] = p.sides[0].tops[0].end;
						matches[2 * top_dir + 1] = p.sides[0].tops[1].end;
					}
					node_planarity[item - (1 + NV + NE)] = matches;
				} else {
					assert(item_types[item] == node_type::R);
					node_planarity[item - (1 + NV + NE)] = std::unexpected(nonplanarity_certficate_t{});
				}
			}
			item_vs[item] = make_vs(t.v_start, t.top_depth);
			item_ch[item] = get_side(t.spans, top_dir);

			t.spans = set_sides(top_dir, unit_list(item), {});
			t.planarity = make_edge_planarity(item, t.top_depth, is_tree);
		};

		struct dfs_stack_t {
			bool has_vert_tstack;
			int ch_idx;
			int ch_end;
			int orig_tstack;
		};
		std::vector<dfs_stack_t> stk; stk.reserve(NV);
		for (auto rt : roots) {
			auto push_vert = [&] [[gnu::always_inline]] (int cur) -> void {
				// stack_dir[cur_depth] must already be set for the lowval, so that we can push the vert tstack
				int cur_depth = int(stk.size());
				stack_verts[cur_depth] = cur;

				int lo = outedges.bounds[cur];
				int hi = outedges.bounds[cur+1];
				bool has_vert_tstack;
				{
					// Find the first same-BCC edge, and check it's type 2 (has lowval2), if so it's the ear tstack and we defer pushing ourselves.
					int first_edge = lo;
					while (first_edge < hi && outedges.dat[first_edge].key.is_new_block()) first_edge++;
					if (first_edge < hi && outedges.dat[first_edge].key.is_type_2()) {
						// Move first_edge to the beginning
						auto e = outedges.dat[first_edge];
						std::move_backward(outedges.dat.begin() + lo, outedges.dat.begin() + first_edge, outedges.dat.begin() + first_edge + 1);
						outedges.dat[lo] = e;
						has_vert_tstack = false;
					} else {
						push_vert_tstack(cur, cur_depth);
						has_vert_tstack = true;
					}
				}
				stk.push_back({has_vert_tstack, lo, hi, -1});
			};
			// return true means jump to start_edge, return false means jump to finish_edge
			auto start_edge = [&] [[gnu::always_inline]] () -> std::optional<int> {
				int cur_depth = int(stk.size()) - 1;
				auto& s = stk.back();
				assert(s.ch_idx < s.ch_end);
				auto [_, nxt, e_side, key] = outedges.dat[s.ch_idx];
				auto [lowval, is_tree, is_type_1] = key.unpack(cur_depth);

				// edge_dir convention: false is forwards, true is backwards.
				// That means that cur is on the edge_dir side and nxt is on the !edge_dir side.
				stack_dir[cur_depth] = (lowval >= cur_depth ? false : !stack_dir[lowval]);

				s.orig_tstack = int(tstack.size());
				if (is_tree) {
					stack_dir[cur_depth+1] = !stack_dir[std::min(lowval, cur_depth)];
					first_occurrence[cur_depth] = NE;
					return nxt;
				} else {
					return std::nullopt;
				}
			};
			auto finish_edge = [&] [[gnu::always_inline]] () -> void {
				int cur_depth = int(stk.size()) - 1;
				auto& s = stk.back();
				int cur = stack_verts[cur_depth];
				assert(s.ch_idx < s.ch_end);

				auto [_, nxt, e_side, key] = outedges.dat[s.ch_idx];
				int e = e_side >> 1;
				s.ch_idx++;

				auto [lowval, is_tree, is_type_1] = key.unpack(cur_depth);

				const int orig_tstack = s.orig_tstack;
				const bool edge_dir = stack_dir[cur_depth];

				if (lowval >= cur_depth) {
					// There's no planarity handling for this because it's just a Q node. I/O nodes also don't need any tracking.
					item_vs[edge_item(e)] = {cur, -1};
					tot_blocks++;
					if (is_tree) {
						// Bridges and components
						if (lowval == cur_depth + 1) {
							// tstack[tstack_size-1] is currently just smuggling out the child vertex, prepend the bridge component
							// This is just a shortcut for allocating a full I-type tstack
							int item = alloc_item(node_type::I);
							item_vs[item] = make_vs(nxt, cur_depth);
							item_ch[edge_item(e)] = concat(unit_list(item), tstack.back().spans[1]); tstack.pop_back();
						} else {
							// tstack[tstack_size-2] is the vertex and tstack[tstack_size-1] is the backedge
							auto backedge = tstack.back().spans[0]; tstack.pop_back();
							item_ch[edge_item(e)] = concat(backedge, tstack.back().spans[1]); tstack.pop_back();
						}
					} else {
						// self loops
						assert(nxt == cur);
						tot_self_loops++;
						int item = alloc_item(node_type::O);
						// Make sure the nxt is -1 as well
						item_vs[item] = {cur, -1};
						item_ch[edge_item(e)] = unit_list(item);
					}
					item_ch[vert_item(cur)] = concat(item_ch[vert_item(cur)], unit_list(edge_item(e)));
					return;
				}
				assert(lowval < cur_depth);

				item_vs[edge_item(e)] = make_vs(nxt, cur_depth);

				if (is_tree) {
					// The span lives on side edge_dir
					push_edge_tstack(nxt, cur_depth, e, true);
					while (nxt_tstack().top_depth >= cur_depth) {
						node_type type;
						if (nxt_tstack().top_depth > cur_depth) {
							// This is a vertex in the tstack, followed by either an S edge, possibly merged with other things
							if (tstack.end()[-3].top_depth < cur_depth) {
								// Not actually a good return, just stop
								break;
							}

							// Just backfill this for maybe_unwrap
							stack_dir[nxt_tstack().top_depth] = edge_dir;
							merge_tstack_tops();

							type = nxt_tstack().top_depth > cur_depth ? node_type::S : node_type::R;
						} else if (nxt_tstack().v_start == cur_tstack().v_start) {
							// This will be a P node
							type = node_type::P;
						} else {
							type = node_type::R;
						}
						int item = maybe_unwrap_nxt(type, type == node_type::S);
						merge_tstack_tops();
						if constexpr (with_planarity) {
							if (cur_tstack().planarity) {
								// Merge all backedges into the component
								for (auto& side : cur_tstack().planarity->sides) {
									assert(side.bot_ends[1] != -1);
									if (side.tops[1].end == -1) continue;
									assert(side.tops[0].depth == cur_depth);
									assert(side.tops[1].depth == cur_depth);
									quarter_edge_matches[side.bot_ends[1]] = side.tops[1].end;
									quarter_edge_matches[side.tops[1].end] = side.bot_ends[1];
									side.bot_ends[1] = side.tops[0].end;
									side.tops = {};
								}
							}
						}
						finish_tstack_top(item, true);
					}

					if (cur_tstack().first_idx > first_occurrence[cur_depth]) {
						if constexpr (with_planarity) {
							[&] [[gnu::always_inline]] () -> void {
								int source = int(tstack.size()) - 1;
								do {
									--source;
									if (!tstack[source].planarity) {
										// Set it somewhere so it can get copied down
										cur_tstack().planarity = tstack[source].planarity;
										return;
									}
								} while (tstack[source].first_idx > first_occurrence[cur_depth]);

								// From planarity's perspective, we can view each tstack as one of 2 shapes:
								// * tstack[i] can be a single "atom" branching off tstack[i].v_start. It can be:
								//   * A single backedge (type 1)
								//   * A subtree (possibly not biconnected) with at least 2 different-depth backedges on its outside (type 2)
								// * tstack[i] can be a "chunk". Chunks contain:
								//   * A core spanning from tstack[i].v_start (bot[0]) to tstack[i+1].v_start (bot[1]).
								//     * The core is a cyclic outer face: it has 2 *disjoint* paths from bot[0] to bot[1]
								//     * Each path can have backedges from its interior (not bot[0] or bot[1])
								//     * side[0]'s path has a backedge to tstack[i].lowval
								//   * Extra atoms on side 1, anchored at tstack[i].v_start
								//     * these must have lowval > tstack[i].lowval, or can have lowval == tstack[i].lowval and be type 2
								//     * these extra atoms are an entire suffix of v_start's: a chunk will always eat them all
								//   * Most of the time, we can treat the side[1] core and the atoms all as separate backedges from tstack[i].v_start.
								//     * The exception is when side[1].tops[1].depth == lowval: then it's forced to be a type 2 atom or part of the core, which matters.
								//       * TODO: Can we easily distinguish the 2 cases?
								// * tstack[i] can also be a tree vertex or a tree edge (trivial cases)
								//
								// Note that all atoms (including the chunk-extras) at one v_start must be sorted by (lowval, type).
								// However, a chunk can occur later (closer to the top) than its (lowval, type) sort at its v_start.

								// last_top == cur_tstack().top_depth
								int last_top = cur_depth;
								while (int(tstack.size()) > source + 2) {
									if (nxt_tstack().top_depth > cur_depth) {
										// Vertex or tree edge, no conditions
									} else if (nxt_tstack().top_depth == cur_depth) {
										if (nxt_tstack().planarity->sides[1].tops[0].depth != -1) {
											// Double-sided to cur_depth, conflicts with cur_tstack()
											assert(last_top < cur_depth);
											// nxt_tstack() is a chunk and both backedges are on the core
											// K33 is:
											// * cur_tstack().tops[0]
											// * nxt_tstack().sides[0].tops[0].base
											// * nxt_tstack().sides[1].tops[0].base
											// + cur
											// + nxt_tstack().v_start
											// + cur_tstack().v_start
											//
											// nxt_tstack().v_start -> cur_tstack().tops[0] is the ear lowval loop
											// cur_tstack().v_start -> cur_tstack().tops[0] is just along cur
											// cur -> nxt_tstack().sides[*].tops[0].base is just the backedge
											// The rest is the outer face of nxt_tstack()
											cur_tstack().planarity = std::unexpected(tstack_nonplanarity_t{});
											return;
										}
										// We will put cur_depth on side 1 until the bottom
										flip_tstack_planarity(nxt_tstack());
									} else {
										if (nxt_tstack().planarity->sides[1].tops[0].depth != -1 && nxt_tstack().planarity->sides[1].tops[0].depth != cur_depth) {
											// Non-empty on both sides, conflicts with source
											// nxt_stack() is a chunk
											if (nxt_tstack().planarity->sides[1].tops[0].depth == nxt_tstack().top_depth) {
												// if it's core + type-2-atom
												// by the atom ordering, we're guaranteed source->cur isn't from v_start
												// * nxt_tstack().sides[0].tops[0].base
												// * nxt_tstack().sides[1].tops[0].base_fork
												// * source.base
												// + cur
												// + nxt_tstack().v_start
												// + nxt_tstack().top_depth
												//
												// cut nxt_tstack().core.side[1]
												// use nxt_tstack().sides[1].tops[0].prev to get from the fork to above cur down to cur
												//
												// otherwise it's double core
												// * cur
												// * nxt_tstack().sides[0].tops[0].base
												// * nxt_tstack().sides[1].tops[0].base
												// + cur_tstack().v_start
												// + nxt_tstack().v_start
												// + nxt_tstack().top_depth
												// (cut the lowval ear edge)
												//
												cur_tstack().planarity = std::unexpected(tstack_nonplanarity_t{});
											} else {
												// If it's an atom
												// by atom ordering, we're guaranteed source->cur isn't from v_start
												// * nxt_tstack().sides[0].tops[0].base
												// * nxt_tstack().sides[1].tops[0].end (go up/down to cur/top_depth)
												// * source.base
												// + cur
												// + nxt_tstack().v_start
												// + nxt_tstack().top_depth
												//
												// otherwise it's double core
												// * cur
												// * nxt_tstack().sides[0].tops[0].base
												// * nxt_tstack().sides[1].tops[0].base
												// + cur_tstack().v_start
												// + nxt_tstack().v_start
												// + nxt_tstack().sides[1].tops[0].end (side 0 gets there from above)
												// (cut the lowval ear edge)
												cur_tstack().planarity = std::unexpected(tstack_nonplanarity_t{});
											}
											return;
										}
										// Implicitly excludes -1
										if (nxt_tstack().planarity->sides[0].tops[1].depth > last_top) {
											assert(last_top < cur_depth);
											// 3 conflicting edges with nxt_tstack(), cur_tstack(), and source
											cur_tstack().planarity = std::unexpected(tstack_nonplanarity_t{});
											return;
										}
										last_top = nxt_tstack().top_depth;
									}
									merge_tstack_tops();
								}

								int t0 = nxt_tstack().planarity->sides[0].tops[1].depth;
								int t1 = nxt_tstack().planarity->sides[1].tops[1].depth;
								assert(t0 == cur_depth || t1 == cur_depth);
								if (std::min(t0, t1) > last_top) {
									assert(last_top < cur_depth);
									cur_tstack().planarity = std::unexpected(tstack_nonplanarity_t{});
									return;
								}
								if (t0 == cur_depth) {
									// We need to flip cur_tstack and nxt_tstack relative to each other.
									// Flip the one with worse top_depth.
									flip_tstack_planarity(cur_tstack().top_depth < nxt_tstack().top_depth ? nxt_tstack() : cur_tstack());
								}
								merge_tstack_tops();

								// Prune off finished cur-side things
								for (auto& side : cur_tstack().planarity->sides) {
									assert(side.bot_ends[1] != -1);
									while (side.tops[1].depth == cur_depth) {
										{
											// Link these to bot_ends[1]
											quarter_edge_matches[side.bot_ends[1]] = side.tops[1].end;
											quarter_edge_matches[side.tops[1].end] = side.bot_ends[1];
											side.bot_ends[1] = side.tops[1].end ^ 1;
										}
										side.tops[1].end = std::exchange(quarter_edge_matches[side.bot_ends[1]], -1);
										if (side.tops[1].end != -1) {
											quarter_edge_matches[side.tops[1].end] = -1;
											side.tops[1].depth = edge_top_depths[side.tops[1].end >> 2];
										} else {
											side.tops = {};
										}
									}
								}
							}();
						}
						while (cur_tstack().first_idx > first_occurrence[cur_depth]) {
							merge_tstack_tops();
						}
					}

					if (is_type_1) assert(s.has_vert_tstack);
					if (s.has_vert_tstack) {
						// NB: tstack[orig_size] is the vertex and tstack[orig_size+1] is the backedge; maybe we should reverse them?
						assert(int(tstack.size()) >= orig_tstack + 3);

						if (!is_type_1) {
							if constexpr (with_planarity) {
								[&] [[gnu::always_inline]] () -> void {
									// The lowval side should be side 1, everything else goes on side 0.
									// The exception is tstack[orig_tstack + 2], which could be == lowval on one/both sides,
									// but is guaranteed to have *something* > lowval by non-type-1-ness
									auto& t = tstack[orig_tstack + 2];
									if (!t.planarity) {
										cur_tstack().planarity = t.planarity;
										return;
									}
									assert(t.planarity->sides[0].tops[0].depth == t.top_depth);
									assert(t.planarity->sides[0].tops[1].depth != -1);
									if (t.planarity->sides[0].tops[1].depth == lowval) {
										flip_tstack_planarity(t);
									} else if (t.planarity->sides[1].tops[1].depth != -1 && t.planarity->sides[1].tops[1].depth != lowval) {
										cur_tstack().planarity = std::unexpected(tstack_nonplanarity_t{});
										return;
									}
									assert(t.planarity->sides[0].tops[1].depth > lowval);
									int last_top = t.planarity->sides[0].tops[1].depth;
									for (int i = orig_tstack + 3; i < int(tstack.size()); i++) {
										if (!tstack[i].planarity) {
											cur_tstack().planarity = tstack[i].planarity;
											return;
										}
										if (tstack[i].top_depth == lowval) {
											flip_tstack_planarity(tstack[i]);
										}
										if (tstack[i].planarity->sides[1].tops[1].depth != -1 && tstack[i].planarity->sides[1].tops[1].depth != lowval) {
											cur_tstack().planarity = std::unexpected(tstack_nonplanarity_t{});
											return;
										}
										int next_top = tstack[i].planarity->sides[0].tops[0].depth;
										if (next_top != -1) {
											if (last_top > next_top) {
												cur_tstack().planarity = std::unexpected(tstack_nonplanarity_t{});
												return;
											}
											last_top = tstack[i].planarity->sides[0].tops[1].depth;
										}
									}
								}();
							}
							while (int(tstack.size()) > orig_tstack + 3) {
								merge_tstack_tops();
							}
						}

						assert(int(tstack.size()) == orig_tstack + 3);
						int item;
						if (is_type_1) {
							item = maybe_unwrap_nxt(cur_tstack().top_depth == cur_depth ? node_type::S : node_type::R, false);
						} else {
							// Just for the type checker
							item = -1;
						}
						// Merge with the backedge
						merge_tstack_tops();
						// Merge with the vertex
						merge_tstack_tops();

						cur_tstack().v_start = cur;
						assert(cur_tstack().top_depth == lowval);

						// Fold everything to the correct side now that we're leaving the child.
						// The entire subtree should go to the !edge_dir side.
						cur_tstack().spans = set_sides(!edge_dir, concat(cur_tstack().spans[0], cur_tstack().spans[1]), {});

						if constexpr (with_planarity) {
							if (cur_tstack().planarity) {
								// precondition: side 1 should be the lowval only side
								auto& sides = cur_tstack().planarity->sides;
								auto& s0 = sides[0];
								auto& s1 = sides[1];
								quarter_edge_matches[s0.bot_ends[0]] = s1.bot_ends[0];
								quarter_edge_matches[s1.bot_ends[0]] = s0.bot_ends[0];
								s0.bot_ends[0] = s1.bot_ends[1];
								if (s1.tops[0].end != -1) {
									// Caller must have checked that we're planar
									assert(s1.tops[1].depth == lowval);

									// This is always true
									assert(s1.tops[0].depth == lowval);
									quarter_edge_matches[s0.tops[0].end] = s1.tops[0].end;
									quarter_edge_matches[s1.tops[0].end] = s0.tops[0].end;
									s0.tops[0].end = s1.tops[1].end;
									// Already true since the backedge was on side 0
									assert(s0.tops[0].depth == lowval);
								}
								s1 = tstack_planarity_side_t{};
							}
						}

						if (is_type_1) {
							finish_tstack_top(item, false);
						}
					}
				} else {
					assert(is_type_1);
					// The span lives on side !edge_dir
					push_edge_tstack(cur, lowval, e, false);
					setmin(first_occurrence[lowval], nxt_edge_idx++);
				}

				assert(int(tstack.size()) >= orig_tstack + 1);

				// If is_type_1, the last entry on the tstack is either the vert_tstack, or the previous child as a unit
				if (is_type_1 && nxt_tstack().top_depth == lowval) {
					// This will be a P node
					int item = maybe_unwrap_nxt(node_type::P, false);
					merge_tstack_tops();
					finish_tstack_top(item, false);
				}

				if (!s.has_vert_tstack) {
					// Throw cur_vert_node onto the tstack so it'll get interleaved correctly
					push_vert_tstack(cur, cur_depth);
					s.has_vert_tstack = true;
					assert(!is_type_1);
				}
			};
			auto pop_vert = [&] [[gnu::always_inline]] () -> void {
				auto& s = stk.back();
				assert(s.ch_idx == s.ch_end);
				assert(s.has_vert_tstack);
				stk.pop_back();
			};

			// Set something arbitrary, this is the normal convention for block-roots
			stack_dir[0] = true;
			push_vert(rt);
			while (true) {
				if (stk.back().ch_idx == stk.back().ch_end) {
					pop_vert();
					if (stk.empty()) break;
					finish_edge();
				} else if (std::optional<int> nxt = start_edge(); nxt) {
					push_vert(*nxt);
				} else {
					finish_edge();
				}
			}
			item_ch[ROOT_ITEM] = concat(item_ch[ROOT_ITEM], tstack.back().spans[1]); tstack.pop_back();
		}
	}

	// Phase 3: relabel the full tree in preorder
	int tot_items = int(item_types.size());
	{
		std::vector<int> vert_index(NV, -1);
		std::vector<int> edge_index(NE, -1);
		std::vector<bool> edge_flipped(NE);

		std::vector<int> par(tot_items, -1);
		std::vector<int> subtree_end(tot_items, -1);
		std::vector<node_type> types(tot_items, node_type::F);
		std::vector<int> orig_id(tot_items, -1);

		csr<int> ch;
		ch.bounds.resize(tot_items + 1, 0);
		ch.dat.resize(tot_items - 1);

		// Each node is a child, and additionally most non-block node has 2 cap verts; blocks have 1, and O nodes have 1
		int tot_node_verts = NV + (tot_items - 1 - NV) * 2 - tot_blocks - tot_self_loops;
		std::vector<node_vert_t> node_verts(tot_node_verts);
		csr_index node_nvs; node_nvs.bounds.resize(tot_items + 1);
		std::vector<int> vert_par_nv(tot_items, -1);

		int tot_node_edges = (tot_items - 1 - NV - tot_blocks) * 2;
		std::vector<node_edge_t> node_edges(tot_node_edges);
		csr_index node_nes; node_nes.bounds.resize(tot_items + 1);

		csr<node_adj_t> node_adj;
		node_adj.bounds.resize(tot_node_verts * 2 + 1);
		node_adj.dat.resize(tot_node_edges * 2);

		std::vector<bool> node_planar(with_planarity ? tot_items : 0);
		std::vector<int> ne_rot_adj(with_planarity ? 4 * tot_node_edges : 0, -1);

		std::vector<int> vert_pos_buf(NV, -1);
		std::vector<int> cnts_buf(2 * NV, -1);
		struct ch_buf_t {
			int loc;
			int item_id;
		};
		std::vector<ch_buf_t> ch_buf(tot_items);
		std::vector<int> rot_edge_ne(with_planarity ? 2 * NE + 1 : 0);

		int nxt_unassigned_idx = 0;

		struct dfs_stack_t {
			int cur_idx;
			int ch_idx;
			int ch_end;
			int cur_nv;
			int cur_ne;
		};
		std::vector<dfs_stack_t> stk; stk.reserve(tot_items);
		auto push_item = [&] [[gnu::always_inline]] (int cur_item) -> void {
			int cur_idx = nxt_unassigned_idx++;
			node_type cur_type = types[cur_idx] = item_types[cur_item];
			bool planar = true;
			if (cur_type == node_type::F) {
				assert(cur_item == 0);
			} else if (cur_type == node_type::V) {
				assert(1 <= cur_item && cur_item < 1 + NV);
				int orig_vert = cur_item - 1;
				orig_id[cur_idx] = orig_vert;
				vert_index[orig_vert] = cur_idx;
			} else if (cur_type == node_type::Q) {
				assert(1 + NV <= cur_item && cur_item < 1 + NV + NE);
				int orig_edge = cur_item - 1 - NV;
				orig_id[cur_idx] = orig_edge;
				edge_index[orig_edge] = cur_idx;
				assert(item_vs[cur_item][0] != -1);
				edge_flipped[orig_edge] = item_vs[cur_item][0] != edges[orig_edge][0];
			} else {
				assert(1 + NV + NE <= cur_item);
				if constexpr (with_planarity) {
					if (cur_type == node_type::O || cur_type == node_type::I) {
						// No planarity data was set up
					} else if (cur_type == node_type::S || cur_type == node_type::P || cur_type == node_type::R) {
						const auto& p = node_planarity[cur_item - (1 + NV + NE)];
						if (p) {
							// Make sure this runs before our planarity_flip checks
							for (int s = 0; s < 4; s++) {
								int a = 8 * NE + s, b = (*p)[s];
								quarter_edge_matches[a] = b;
								quarter_edge_matches[b] = a;
							}
						} else {
							// TODO: Any certificate stuff
							planar = false;
						}
					} else assert(false);
				}
			}
			if constexpr (with_planarity) node_planar[cur_idx] = planar;

			// HACK: Fill ch and vert_items in with orig items / orig verts for now,
			// because we don't have the final item id's yet.
			int ch_st = ch.bounds[cur_idx];
			int ch_en = ch_st;
			int nv_st = node_nvs.bounds[cur_idx];
			int nv_en = nv_st;
			int n_edges = 0;
			if (item_vs[cur_item][0] != -1) {
				node_verts[nv_en++] = {cur_idx, item_vs[cur_item][0]};
			}
			if (!item_ch[cur_item].empty()) {
				bool planarity_flip = item_ch[cur_item].v[0] & 1;
				for (int ch_item = item_ch[cur_item].v[0] >> 1; true; planarity_flip ^= (ch_nxt[ch_item] & 1), ch_item = ch_nxt[ch_item] >> 1) {
					ch.dat[ch_en++] = ch_item;
					assert(ch_item >= 1);
					if (ch_item < 1 + NV) {
						node_verts[nv_en++] = {cur_idx, ch_item - 1};
					} else {
						if constexpr (with_planarity) {
							if (cur_type != node_type::R) {
								assert(!planarity_flip);
							} else {
								// Fix the planarity direction right here: reverse quarter_edge_matches upfront;
								// this breaks the involution property, but from here on we'll never read the low bits anyways.
								int ve = ch_item - (1 + NV);
								if (planarity_flip) {
									std::swap(quarter_edge_matches[4 * ve + 0], quarter_edge_matches[4 * ve + 1]);
									std::swap(quarter_edge_matches[4 * ve + 2], quarter_edge_matches[4 * ve + 3]);
								}
							}
						}
						n_edges++;
					}
					if (ch_item == (item_ch[cur_item].v[1] >> 1)) {
						assert(ch_nxt[ch_item] == -1);
						break;
					}
				}
				planarity_flip ^= item_ch[cur_item].v[1] & 1;
				assert(!planarity_flip);
			}
			if (item_vs[cur_item][1] != -1) {
				node_verts[nv_en++] = {cur_idx, item_vs[cur_item][1]};
			}
			ch.bounds[cur_idx+1] = ch_en;
			node_nvs.bounds[cur_idx+1] = nv_en;

			int n_verts = nv_en - nv_st;

			bool is_node = cur_type != node_type::F && cur_type != node_type::V;
			bool has_cap = is_node && !(cur_type == node_type::Q && ch_en - ch_st > 0);

			if (!is_node) n_edges = 0;
			if (has_cap) n_edges++;

			int ne_st = node_nes.bounds[cur_idx];
			int ne_en = node_nes.bounds[cur_idx+1] = ne_st + n_edges;

			auto set_ne = [&] [[gnu::always_inline]] (int ne, std::array<int, 2> nvs, std::array<int, 2> nds, std::array<int, 4> rot_adjs) -> void {
				node_edges[ne].node = cur_idx;
				node_edges[ne].nvs = nvs;
				node_adj.dat[nds[0]] = {ne, nvs[1]};
				node_adj.dat[nds[1]] = {ne, nvs[0]};
				if constexpr (with_planarity) {
					for (int z = 0; z < 4; z++) ne_rot_adj[4 * ne + z] = rot_adjs[z];
				}
			};
			if (cur_type == node_type::F) {
				// Just set node_adj bounds and we're good
				for (int i = 2 * nv_st+1; i <= 2 * nv_en; i++) {
					node_adj.bounds[i] = 2 * ne_st;
				}
			} else if (cur_type == node_type::V) {
				// Nothing to do
			} else if (n_verts == 1) {
				assert(cur_type == node_type::Q || cur_type == node_type::O);
				assert(n_edges == 1);
				node_adj.bounds[2 * nv_st + 1] = 2 * ne_st + 1 * n_edges;
				node_adj.bounds[2 * nv_st + 2] = 2 * ne_st + 2 * n_edges;
				set_ne(ne_st, {nv_st, nv_st}, {2 * ne_st + 1, 2 * ne_st}, {4 * ne_st + 3, 4 * ne_st + 2, 4 * ne_st + 1, 4 * ne_st + 0});
			} else if (cur_type == node_type::Q || cur_type == node_type::I) {
				assert(n_verts == 2);
				assert(n_edges == 1);
				node_adj.bounds[2 * nv_st + 1] = 2 * ne_st + 0 * n_edges;
				node_adj.bounds[2 * nv_st + 2] = 2 * ne_st + 1 * n_edges;
				node_adj.bounds[2 * nv_st + 3] = 2 * ne_st + 2 * n_edges;
				node_adj.bounds[2 * nv_st + 4] = 2 * ne_st + 2 * n_edges;
				set_ne(ne_st, {nv_st, nv_st + 1}, {2 * ne_st, 2 * ne_st + 1}, {4 * ne_st + 1, 4 * ne_st + 0, 4 * ne_st + 3, 4 * ne_st + 2});
			} else if (cur_type == node_type::P) {
				// Special case: tiebreak the parallel edges so they're reversed
				assert(n_verts == 2);
				assert(n_edges >= 3);
				node_adj.bounds[2 * nv_st + 1] = 2 * ne_st + 0 * n_edges;
				node_adj.bounds[2 * nv_st + 2] = 2 * ne_st + 1 * n_edges;
				node_adj.bounds[2 * nv_st + 3] = 2 * ne_st + 2 * n_edges;
				node_adj.bounds[2 * nv_st + 4] = 2 * ne_st + 2 * n_edges;
				for (int ne = ne_st; ne < ne_en; ne++) {
					int ne_prv = (ne == ne_st ? ne_en : ne) - 1;
					int ne_nxt = (ne+1 == ne_en ? ne_st : ne+1);
					std::array<int, 4> rot_adjs{4 * ne_prv + 1, 4 * ne_nxt + 0, 4 * ne_nxt + 3, 4 * ne_prv + 2};
					set_ne(ne, {nv_st, nv_st + 1}, {2 * ne_st + (ne - ne_st), 2 * ne_en - 1 - (ne - ne_st)}, rot_adjs);
				}
			} else if (cur_type == node_type::S) {
				assert(n_verts == n_edges);
				assert(n_verts >= 3);
				for (int i = 2 * nv_st + 1; i <= 2 * nv_en; i++) {
					node_adj.bounds[i] = i + 2 * (ne_st - nv_st);
				}
				// Fix bounds for the cap
				node_adj.bounds[2 * nv_st + 1]--;
				node_adj.bounds[2 * nv_en - 1]++;
				set_ne(ne_st, {nv_st, nv_en - 1}, {2 * ne_st, 2 * ne_en - 1}, {4 * (ne_st+1) + 1, 4 * (ne_st+1) + 0, 4 * (ne_en-1) + 3, 4 * (ne_en-1) + 2});
				for (int i = 1; i < n_edges; i++) {
					int ne = ne_st + i;
					std::array<int, 4> rot_adjs{4 * (ne-1) + 3, 4 * (ne-1) + 2, 4 * (ne+1) + 1, 4 * (ne+1) + 0};
					if (ne-1 == ne_st) { rot_adjs[0] = 4 * ne_st + 1, rot_adjs[1] = 4 * ne_st + 0; }
					if (ne+1 == ne_en) { rot_adjs[2] = 4 * ne_st + 3, rot_adjs[3] = 4 * ne_st + 2; }
					set_ne(ne, {nv_st + i - 1, nv_st + i}, {2 * ne - 1, 2 * ne}, rot_adjs);
				}
			} else if (cur_type == node_type::R) {
				// Bucketsort the children by the midpoint
				for (int nv = nv_st; nv < nv_en; nv++) {
					vert_pos_buf[node_verts[nv].vert] = nv;
				}
				cnts_buf.assign(n_verts * 2 - 1, 0);
				ch_buf.clear();

				assert(has_cap);

				// Cap node_adj bounds
				node_adj.bounds[2 * nv_st + 2]++;
				node_adj.bounds[2 * nv_en - 1]++;

				for (int i = ch_st; i < ch_en; i++) {
					int item = ch.dat[i];
					assert(item >= 1);
					std::array<int, 2> nvs;
					if (item < 1 + NV) {
						nvs = {vert_pos_buf[item-1], vert_pos_buf[item-1]};
					} else {
						nvs = {vert_pos_buf[item_vs[item][0]], vert_pos_buf[item_vs[item][1]]};
						assert(nvs[0] < nvs[1]);
						node_adj.bounds[2 * nvs[0] + 2]++;
						node_adj.bounds[2 * nvs[1] + 1]++;
					}
					int loc = (nvs[0] - nv_st) + (nvs[1] - nv_st);
					ch_buf.emplace_back(loc, item);
					cnts_buf[loc]++;
				}
				int offset = ch_st;
				for (auto& cnt : cnts_buf) {
					offset += cnt;
					cnt = offset;
				}
				for (auto [loc, n] : std::views::reverse(ch_buf)) {
					ch.dat[--cnts_buf[loc]] = n;
				}

				if constexpr (with_planarity) {
					// Set up the reverse mapping for ourselves
					int nxt_ne = ne_en;
					for (int i = ch_en - 1; i >= ch_st; i--) {
						int item = ch.dat[i];
						assert(item >= 1);
						if (item < 1 + NV) continue;
						nxt_ne--;
						rot_edge_ne[item - (1 + NV)] = nxt_ne;
					}
					assert(nxt_ne == ne_st + 1);
					rot_edge_ne[2 * NE] = ne_st;
				}
				auto map_rot_edge = [&] [[gnu::always_inline]] (int ve) -> std::array<int, 4> {
					if constexpr (!with_planarity) return {-1, -1, -1, -1};
					if (!planar) return {-1, -1, -1, -1};
					std::array<int, 4> res{};
					for (int z = 0; z < 4; z++) {
						int o = quarter_edge_matches[4 * ve + z];
						assert(o != -1);
						res[z] = (rot_edge_ne[o >> 2] << 2) + (o & 2) + !(z & 1);
					}
					return res;
				};

				{
					int off = 2 * ne_st;
					for (int i = 2 * nv_st + 1; i <= 2 * nv_en; i++) {
						off += std::exchange(node_adj.bounds[i], off);
					}
					assert(off == 2 * ne_en);
				}

				// Fill in node_edges and node_adj.
				// Reverse order to get the adj in bracket ordering.
				{
					// Handle cap as special: it's first in the node_edges, which means it's in the wrong place for the left endpoint.
					node_adj.bounds[2 * nv_st + 2]++;

					int nxt_ne = ne_en;
					for (int i = ch_en - 1; i >= ch_st; i--) {
						int item = ch.dat[i];
						assert(item >= 1);
						if (item < 1 + NV) continue;
						nxt_ne--;
						auto [v0, v1] = item_vs[item];
						// TODO: Reuse this from the ch pass?
						std::array<int, 2> nvs = {vert_pos_buf[v0], vert_pos_buf[v1]};
						set_ne(nxt_ne, nvs, {
							node_adj.bounds[2 * nvs[0] + 2]++,
							node_adj.bounds[2 * nvs[1] + 1]++,
						}, map_rot_edge(item - (1 + NV)));
					}
					assert(nxt_ne == ne_st + 1);

					// Insert the cap / bump its bound
					set_ne(ne_st, {nv_st, nv_en - 1}, {2 * ne_st, 2 * ne_en - 1}, map_rot_edge(2 * NE));
					node_adj.bounds[2 * nv_en - 1]++;
				}
			} else assert(false);

			int cur_nv = nv_st + (item_vs[cur_item][0] != -1);
			int cur_ne = ne_st + has_cap;
			stk.push_back({cur_idx, ch_st, ch_en, cur_nv, cur_ne});
		};

		auto start_child = [&] [[gnu::always_inline]] () -> int {
			auto& [cur_idx, ch_idx, ch_en, cur_nv, cur_ne] = stk.back();
			assert(ch_idx < ch_en);
			int nxt_item = ch.dat[ch_idx];
			int nxt_idx = nxt_unassigned_idx;
			ch.dat[ch_idx] = nxt_idx;
			par[nxt_idx] = cur_idx;
			int nxt_ne = node_nes.bounds[nxt_idx];
			if (nxt_item < 1 + NV) {
				vert_par_nv[nxt_idx] = cur_nv++;
			} else if (types[cur_idx] != node_type::F && types[cur_idx] != node_type::V) {
				node_edges[cur_ne].twin_ne = nxt_ne;
				node_edges[nxt_ne].twin_ne = cur_ne;
				cur_ne++;
			}

			ch_idx++;
			return nxt_item;
		};

		auto pop_item = [&] [[gnu::always_inline]] () -> void {
			auto [cur_idx, ch_idx, ch_en, cur_nv, cur_ne] = stk.back(); stk.pop_back();
			assert(ch_idx == ch_en);
			subtree_end[cur_idx] = nxt_unassigned_idx;
		};

		par[nxt_unassigned_idx] = -1;
		push_item(ROOT_ITEM);
		while (true) {
			if (stk.back().ch_idx == stk.back().ch_end) {
				pop_item();
				if (stk.empty()) break;
			} else {
				push_item(start_child());
			}
		}

		assert(nxt_unassigned_idx == tot_items);
		assert(ch.bounds.back() == int(ch.dat.size()));
		assert(node_nvs.bounds.back() == int(node_verts.size()));
		assert(node_nes.bounds.back() == int(node_edges.size()));
		assert(node_adj.bounds.back() == int(node_adj.dat.size()));

		// Rewrite node_vertices to the correct index
		for (auto& v : node_verts) {
			v.vert = vert_index[v.vert];
		}

		spqr_tree res{
			std::move(vert_index),
			std::move(edge_index),
			std::move(edge_flipped),
			std::move(par),
			std::move(subtree_end),
			std::move(types),
			std::move(orig_id),
			std::move(ch),
			std::move(node_verts),
			std::move(node_nvs),
			std::move(vert_par_nv),
			std::move(node_edges),
			std::move(node_nes),
			std::move(node_adj),
		};
		if constexpr (with_planarity) {
			return planar_spqr_tree{std::move(res), std::move(node_planar), {std::move(ne_rot_adj)}};
		} else {
			return res;
		}
	}
}

inline std::optional<planar_embedding> planar_embed(const planar_spqr_tree& tree) {
	using node_type = planar_spqr_tree::node_type;

	if (!std::ranges::all_of(tree.node_planar, std::identity{})) {
		return std::nullopt;
	}

	int NE = int(tree.edge_index.size());
	std::vector<int> rot_adj(4 * NE, -1);
	auto link = [&] [[gnu::always_inline]] (int a, int b) -> void {
		assert(a != -1 && b != -1);
		assert(rot_adj[a] == -1 && rot_adj[b] == -1);
		assert((a & 1) != (b & 1));
		rot_adj[a] = b;
		rot_adj[b] = a;
	};

	std::vector<std::array<std::array<int, 2>, 2>> outer_e(tree.size(), {{{-1, -1}, {-1, -1}}});
	for (int i = tree.size() - 1; i >= 0; i--) {
		auto type = tree.types[i];
		if (type == node_type::F) {
			for (int j : tree.ch[i]) {
				assert(tree.types[j] == node_type::V);
				auto [a, b] = outer_e[j][0];
				if (a != -1) {
					link(a, b);
				}
			}
		} else if (type == node_type::V) {
			std::array<int, 2> qes{-1, -1};
			for (int j : tree.ch[i]) {
				assert(tree.types[j] == node_type::Q);
				if (qes[0] == -1) {
					qes = outer_e[j][0];
				} else {
					link(qes[1], outer_e[j][0][0]);
					qes[1] = outer_e[j][0][1];
				}
			}
			outer_e[i][0] = qes;
		} else if (type == node_type::Q) {
			int e = tree.orig_id[i];
			bool flip = tree.edge_flipped[e];
			std::array<std::array<int, 2>, 2> qes = {{{4 * e + 2 * flip + 0, 4 * e + 2 * flip + 1}, {4 * e + 2 * !flip + 0, 4 * e + 2 * !flip + 1}}};
			if (tree.ch[i].empty()) {
				// Just return ourselves
				outer_e[i] = qes;
			} else {
				int j = tree.ch[i][0];
				if (tree.types[j] == node_type::O) {
					link(qes[0][1], qes[1][0]);
					outer_e[i][0] = {qes[0][0], qes[1][1]};
				} else {
					if (tree.types[j] != node_type::I) {
						link(qes[0][1], outer_e[j][0][0]);
						qes[0][1] = outer_e[j][0][1];
						link(qes[1][0], outer_e[j][1][1]);
						qes[1][0] = outer_e[j][1][0];
					}
					{
						int k = tree.ch[i][1];
						if (outer_e[k][0][0] != -1) {
							link(qes[1][1], outer_e[k][0][0]);
							link(qes[1][0], outer_e[k][0][1]);
						} else {
							link(qes[1][1], qes[1][0]);
						}
					}
					outer_e[i][0] = qes[0];
				}
			}
		} else if (type == node_type::O || type == node_type::I) {
			// Do nothing, the Q node handles it
		} else if (type == node_type::P || type == node_type::S || type == node_type::R) {
			// Just merge things according to the ne_embedding
			for (int ta = 4 * tree.node_nes.bounds[i]; ta < 4 * tree.node_nes.bounds[i+1]; ta++) {
				int tb = tree.ne_embedding.rot_adj[ta];
				if (tb < ta) continue;

				auto tree_qe_to_qe = [&] [[gnu::always_inline]] (int t) -> int {
					return outer_e[tree.node_edges[tree.node_edges[t>>2].twin_ne].node][(t >> 1) & 1][t & 1];
				};
				int qb = tree_qe_to_qe(tb);
				if (ta < 4 * (tree.node_nes.bounds[i] + 1)) {
					outer_e[i][(ta >> 1) & 1][!(ta & 1)] = qb;
				} else {
					int qa = tree_qe_to_qe(ta);
					if ((ta & 3) == 2 && (tb & 3) == 1) {
						// We're the transition between left and right of a vertex, splice it in here.
						int v = tree.node_verts[tree.node_edges[ta >> 2].nvs[1]].vert;
						if (outer_e[v][0][0] != -1) {
							link(qa, outer_e[v][0][1]);
							link(qb, outer_e[v][0][0]);
						} else {
							link(qa, qb);
						}
					} else {
						link(qa, qb);
					}
				}
			}
		} else assert(false);
	}

	return planar_embedding{std::move(rot_adj)};
}

inline std::optional<planar_embedding> planar_embed(
	int NV,
	const std::vector<std::array<int, 2>>& edges,
	std::span<const int> vert_order,
	std::span<const int> edge_order
) {
	// std::min is by reference, which breaks some optimizations
	auto setmin = [](auto& a, auto b) { if (b < a) a = b; };

	int NE = int(edges.size());
	auto [roots, outedges] = lowval_storted_skeleton_t::build(NV, edges, vert_order, edge_order);

	// Phase 2: do the big ear-decomposition-like walk

	// We're going to build a tree of all SPQR *nodes* + all original *vertices* (collectively *items*).
	// Vertices will hang off the first SPQR node containing them, and blocks will be rooted at a topmost Q node for the top edge.

	// Quarter edges for planar embedding building.
	// Each edge has 4 entries by 4 * edge_id + 2 * source_vert + is_cw (is_cw is arbitrary)
	std::vector<int> quarter_edge_matches(4 * NE, -1);

	{
		int nxt_edge_idx = 0; // Counts backedges only
		std::vector<int> postorder_edges; postorder_edges.reserve(NE);
		std::vector<bool> postorder_flip(NE+1, false);

		std::vector<int> first_occurrence(NV); // First backedge to this depth

		std::vector<int> edge_top_depths(NE, -1);

		struct tstack_planarity_side_t {
			// For each side, store pointers to the "linked lists" of the edges inside.
			// v[0] is the outer / longer edges and v[1] is the inner / shorter edges, matching the outside-in sort order.

			// bot_ends are the outer/innermost exposed pieces of the walk down the ear in the tree (they're connected to the bottommost/topmost vertices of the tree path)
			std::array<int, 2> bot_ends{-1, -1};
			struct top_t {
				int end = -1;
				int depth = -1;
			};
			// top_ends are the outer/innermost exposed backedges
			// depths should be increasing going inwards
			std::array<top_t, 2> tops{top_t{-1, -1}, top_t{-1, -1}};
		};
		struct tstack_planarity_t {
			// The convention is that sides[0].tops[0].depth == top_depth, i.e. at least one minimal return lives on side 0
			std::array<tstack_planarity_side_t, 2> sides;
		};
		struct tstack_nonplanarity_t {
			// TODO: What's the nonplanarity certificate look like?
		};
		auto merge_planarity = [&] [[gnu::always_inline]] (tstack_planarity_t& a, const tstack_planarity_t& b) -> void {
			for (int z = 0; z < 2; z++) {
				auto& as = a.sides[z];
				const auto& bs = b.sides[z];
				// If there's no bottom edges, then we must be an isolated vertex, so we can end early.
				if (bs.bot_ends[0] == -1) {
					// Do nothing
				} else if (as.bot_ends[0] == -1) {
					as = bs;
				} else {
					quarter_edge_matches[as.bot_ends[1]] = bs.bot_ends[0];
					quarter_edge_matches[bs.bot_ends[0]] = as.bot_ends[1];
					as.bot_ends[1] = bs.bot_ends[1];

					if (bs.tops[0].end == -1) {
						// Do nothing
					} else if (as.tops[0].end == -1) {
						as.tops = bs.tops;
					} else {
						// Caller must check that we're planar
						assert(as.tops[1].depth <= bs.tops[0].depth);
						quarter_edge_matches[as.tops[1].end] = bs.tops[0].end;
						quarter_edge_matches[bs.tops[0].end] = as.tops[1].end;
						as.tops[1] = bs.tops[1];
					}
				}
			}
		};
		auto make_edge_planarity = [&] [[gnu::always_inline]] (int e_side, int top_depth, bool is_tree) -> tstack_planarity_t {
			edge_top_depths[e_side >> 1] = top_depth;
			tstack_planarity_t p;
			if (is_tree) {
				p.sides[0].bot_ends = {2 * (e_side ^ 1) + 0, 2 * e_side + 1};
				p.sides[1].bot_ends = {2 * (e_side ^ 1) + 1, 2 * e_side + 0};
			} else {
				p.sides[0].bot_ends = {2 * e_side + 0, 2 * e_side + 1};
				p.sides[0].tops = {{{2 * (e_side ^ 1) + 1, top_depth}, {2 * (e_side ^ 1) + 0, top_depth}}};
			}
			return p;
		};
		struct tstack_t {
			int top_depth = -1;
			int first_idx = -1;
			tstack_planarity_t planarity;
		};
		std::vector<tstack_t> tstack; tstack.reserve(NV + NE);
		auto cur_tstack = [&] [[gnu::always_inline]] () -> tstack_t& { return tstack.end()[-1]; };
		auto nxt_tstack = [&] [[gnu::always_inline]] () -> tstack_t& { return tstack.end()[-2]; };

		auto push_tstack = [&] [[gnu::always_inline]] (int top_depth, tstack_planarity_t planarity) -> void {
			tstack.emplace_back(top_depth, nxt_edge_idx, planarity);
		};
		auto push_vert_tstack = [&] [[gnu::always_inline]] (int top_depth) -> void {
			push_tstack(top_depth, {});
		};
		auto push_edge_tstack = [&] [[gnu::always_inline]] (int top_depth, int e_side, bool is_tree) -> int {
			push_tstack(top_depth, make_edge_planarity(e_side, top_depth, is_tree));
			postorder_edges.push_back(e_side >> 1);
			return nxt_edge_idx++;
		};
		auto flip_tstack_planarity = [&] [[gnu::always_inline]] (int i) -> void {
			tstack_t& a = tstack[i];
			postorder_flip[a.first_idx].flip();
			postorder_flip[(i+1==int(tstack.size())) ? nxt_edge_idx : tstack[i+1].first_idx].flip();
			std::swap(a.planarity.sides[0], a.planarity.sides[1]);
		};
		auto merge_tstack_tops = [&] [[gnu::always_inline]] () -> void {
			tstack_t& a = nxt_tstack();
			const tstack_t& b = cur_tstack();
			setmin(a.top_depth, b.top_depth);
			merge_planarity(a.planarity, b.planarity);
			tstack.pop_back();
		};

		struct dfs_stack_t {
			bool has_vert_tstack;
			int ch_idx;
			int ch_end;
			int orig_tstack;
		};
		std::vector<dfs_stack_t> stk; stk.reserve(NV);
		for (auto rt : roots) {
			auto push_vert = [&] [[gnu::always_inline]] (int cur) -> void {
				int cur_depth = int(stk.size());

				int lo = outedges.bounds[cur];
				int hi = outedges.bounds[cur+1];
				bool has_vert_tstack;
				{
					// Find the first same-BCC edge, and check it's type 2 (has lowval2), if so it's the ear tstack and we defer pushing ourselves.
					int first_edge = lo;
					while (first_edge < hi && outedges.dat[first_edge].key.is_new_block()) first_edge++;
					if (first_edge < hi && outedges.dat[first_edge].key.is_type_2()) {
						// Move first_edge to the beginning
						auto e = outedges.dat[first_edge];
						std::move_backward(outedges.dat.begin() + lo, outedges.dat.begin() + first_edge, outedges.dat.begin() + first_edge + 1);
						outedges.dat[lo] = e;
						has_vert_tstack = false;
					} else {
						push_vert_tstack(cur_depth);
						has_vert_tstack = true;
					}
				}
				stk.push_back({has_vert_tstack, lo, hi, -1});
			};
			// return true means jump to start_edge, return false means jump to finish_edge
			auto start_edge = [&] [[gnu::always_inline]] () -> std::optional<int> {
				int cur_depth = int(stk.size()) - 1;
				auto& s = stk.back();
				assert(s.ch_idx < s.ch_end);
				auto [_, nxt, e_side, key] = outedges.dat[s.ch_idx];
				auto [lowval, is_tree, is_type_1] = key.unpack(cur_depth);

				if (lowval >= cur_depth || is_type_1) assert(s.has_vert_tstack);

				s.orig_tstack = int(tstack.size());
				if (is_tree) {
					first_occurrence[cur_depth] = NE;
					return nxt;
				} else {
					return std::nullopt;
				}
			};
			auto finish_edge = [&] [[gnu::always_inline]] [[nodiscard]] () -> std::optional<tstack_nonplanarity_t> {
				int cur_depth = int(stk.size()) - 1;
				auto& s = stk.back();
				assert(s.ch_idx < s.ch_end);

				auto [_, nxt, e_side, key] = outedges.dat[s.ch_idx];
				s.ch_idx++;

				auto [lowval, is_tree, is_type_1] = key.unpack(cur_depth);

				const int orig_tstack = s.orig_tstack;

				auto join_backedges_to_top = [&] [[gnu::always_inline]] () -> void {
					// Merge all backedges into the component
					for (auto& side : cur_tstack().planarity.sides) {
						// in the self-loop case, sides[1].bot_ends[1] == -1; otherwise, it should never be -1
						if (side.tops[1].end == -1) continue;
						assert(side.bot_ends[1] != -1);
						assert(side.tops[0].depth == cur_depth);
						assert(side.tops[1].depth == cur_depth);
						quarter_edge_matches[side.bot_ends[1]] = side.tops[1].end;
						quarter_edge_matches[side.tops[1].end] = side.bot_ends[1];
						side.bot_ends[1] = side.tops[0].end;
						side.tops = {};
					}
				};

				auto join_bottoms_to_empty = [&] [[gnu::always_inline]] () -> void {
					// precondition: side 1 should be the lowval only side
					auto& sides = cur_tstack().planarity.sides;
					auto& s0 = sides[0];
					auto& s1 = sides[1];
					quarter_edge_matches[s0.bot_ends[0]] = s1.bot_ends[0];
					quarter_edge_matches[s1.bot_ends[0]] = s0.bot_ends[0];
					s0.bot_ends[0] = s1.bot_ends[1];
					if (s1.tops[0].end != -1) {
						// Caller must have checked that we're planar
						assert(s1.tops[1].depth == lowval);

						// This is always true
						assert(s1.tops[0].depth == lowval);
						quarter_edge_matches[s0.tops[0].end] = s1.tops[0].end;
						quarter_edge_matches[s1.tops[0].end] = s0.tops[0].end;
						s0.tops[0].end = s1.tops[1].end;
						// Already true since the backedge was on side 0
						assert(s0.tops[0].depth == lowval);
					}
					s1 = tstack_planarity_side_t{};
				};

				if (lowval >= cur_depth) {
					if (is_tree) {
						push_edge_tstack(cur_depth, e_side, true);
						if (lowval == cur_depth) {
							// Merge the backedge
							merge_tstack_tops();
							join_backedges_to_top();
						}
						// Merge the vertex
						merge_tstack_tops();
						join_bottoms_to_empty();
					} else {
						push_edge_tstack(lowval, e_side, false);
						join_backedges_to_top();
					}
					assert(s.has_vert_tstack);
					// Merge into the vertex tstack
					merge_tstack_tops();
					return std::nullopt;
				}

				if (is_tree) {
					push_edge_tstack(cur_depth, e_side, true);
					while (nxt_tstack().top_depth >= cur_depth) {
						if (nxt_tstack().top_depth > cur_depth) {
							if (tstack.end()[-3].top_depth < cur_depth) {
								break;
							}
							// Merge the vertex in
							merge_tstack_tops();
						}

						merge_tstack_tops();
						join_backedges_to_top();
					}

					if (cur_tstack().first_idx > first_occurrence[cur_depth]) {
						int source = int(tstack.size()) - 2;
						while (tstack[source].first_idx > first_occurrence[cur_depth]) --source;

						// last_top == cur_tstack().top_depth
						int last_top = cur_depth;
						while (int(tstack.size()) > source + 2) {
							if (nxt_tstack().top_depth > cur_depth) {
								// Vertex or tree edge, no conditions
							} else if (nxt_tstack().top_depth == cur_depth) {
								if (nxt_tstack().planarity.sides[1].tops[0].depth != -1) {
									// Double-sided to cur_depth, conflicts with cur_tstack()
									assert(last_top < cur_depth);
									return tstack_nonplanarity_t{};
								}
								// We will put cur_depth on side 1 until the bottom
								flip_tstack_planarity(int(tstack.size()) - 2);
							} else {
								if (nxt_tstack().planarity.sides[1].tops[0].depth != -1 && nxt_tstack().planarity.sides[1].tops[0].depth != cur_depth) {
									// Non-empty on both sides, conflicts with source
									return tstack_nonplanarity_t{};
								}
								if (nxt_tstack().planarity.sides[0].tops[1].depth > last_top) {
									// Nonlaminar with cur_tstack()
									return tstack_nonplanarity_t{};
								}
								last_top = nxt_tstack().top_depth;
							}
							merge_tstack_tops();
						}

						int t0 = nxt_tstack().planarity.sides[0].tops[1].depth;
						int t1 = nxt_tstack().planarity.sides[1].tops[1].depth;
						assert(t0 == cur_depth || t1 == cur_depth);
						if (std::min(t0, t1) > last_top) {
							assert(last_top < cur_depth);
							return tstack_nonplanarity_t{};
						}
						if (t0 == cur_depth) {
							// We need to flip cur_tstack and nxt_tstack relative to each other.
							// Flip the one with worse top_depth.
							flip_tstack_planarity(cur_tstack().top_depth < nxt_tstack().top_depth ? int(tstack.size()) - 2 : int(tstack.size()) - 1);
						}
						merge_tstack_tops();

						// Prune off finished cur-side things
						for (auto& side : cur_tstack().planarity.sides) {
							assert(side.bot_ends[1] != -1);
							while (side.tops[1].depth == cur_depth) {
								{
									// Link these to bot_ends[1]
									quarter_edge_matches[side.bot_ends[1]] = side.tops[1].end;
									quarter_edge_matches[side.tops[1].end] = side.bot_ends[1];
									side.bot_ends[1] = side.tops[1].end ^ 1;
								}
								side.tops[1].end = std::exchange(quarter_edge_matches[side.bot_ends[1]], -1);
								if (side.tops[1].end != -1) {
									quarter_edge_matches[side.tops[1].end] = -1;
									side.tops[1].depth = edge_top_depths[side.tops[1].end >> 2];
								} else {
									side.tops = {};
								}
							}
						}
					}

					if (is_type_1) assert(s.has_vert_tstack);
					if (s.has_vert_tstack) {
						// NB: tstack[orig_size] is the vertex and tstack[orig_size+1] is the backedge; maybe we should reverse them?
						assert(int(tstack.size()) >= orig_tstack + 3);

						if (!is_type_1) {
							// The lowval side should be side 1, everything else goes on side 0.
							// The exception is tstack[orig_tstack + 2], which could be == lowval on one/both sides,
							// but is guaranteed to have *something* > lowval by non-type-1-ness
							auto& t = tstack[orig_tstack + 2];
							{
								assert(t.planarity.sides[0].tops[0].depth == t.top_depth);
								assert(t.planarity.sides[0].tops[1].depth != -1);
								if (t.planarity.sides[0].tops[1].depth == lowval) {
									flip_tstack_planarity(orig_tstack + 2);
								} else if (t.planarity.sides[1].tops[1].depth != -1 && t.planarity.sides[1].tops[1].depth != lowval) {
									return tstack_nonplanarity_t{};
								}
								assert(t.planarity.sides[0].tops[1].depth > lowval);
							}
							int last_top = t.planarity.sides[0].tops[1].depth;
							for (int i = orig_tstack + 3; i < int(tstack.size()); i++) {
								if (tstack[i].top_depth == lowval) {
									flip_tstack_planarity(i);
								}
								if (tstack[i].planarity.sides[1].tops[1].depth != -1 && tstack[i].planarity.sides[1].tops[1].depth != lowval) {
									return tstack_nonplanarity_t{};
								}
								int next_top = tstack[i].planarity.sides[0].tops[0].depth;
								if (next_top != -1) {
									if (last_top > next_top) return tstack_nonplanarity_t{};
									last_top = tstack[i].planarity.sides[0].tops[1].depth;
								}
							}
							while (int(tstack.size()) > orig_tstack + 3) {
								merge_tstack_tops();
							}
						}

						assert(int(tstack.size()) == orig_tstack + 3);

						// Merge with the backedge
						merge_tstack_tops();
						// Merge with the vertex
						merge_tstack_tops();

						assert(cur_tstack().top_depth == lowval);

						join_bottoms_to_empty();
					}
				} else {
					assert(is_type_1);
					int idx = push_edge_tstack(lowval, e_side, false);
					setmin(first_occurrence[lowval], idx);
				}

				if (is_type_1 && nxt_tstack().top_depth == lowval) {
					assert(s.has_vert_tstack);
					merge_tstack_tops();
				}

				if (!s.has_vert_tstack) {
					assert(!is_type_1);
					// Throw cur_vert_node onto the tstack so it'll get interleaved correctly
					push_vert_tstack(cur_depth);
					s.has_vert_tstack = true;
				}
				return std::nullopt;
			};
			auto pop_vert = [&] [[gnu::always_inline]] () -> void {
				auto& s = stk.back();
				assert(s.ch_idx == s.ch_end);
				assert(s.has_vert_tstack);
				stk.pop_back();
			};

			push_vert(rt);
			while (true) {
				if (stk.back().ch_idx == stk.back().ch_end) {
					pop_vert();
					if (stk.empty()) break;
					if (auto res = finish_edge(); res) return std::nullopt;
				} else if (std::optional<int> nxt = start_edge(); nxt) {
					push_vert(*nxt);
				} else {
					if (auto res = finish_edge(); res) assert(false);
				}
			}
			assert(int(tstack.size()) == 1);
			auto s0 = tstack.back().planarity.sides[0];
			// Fold the root
			int a = s0.bot_ends[0];
			int b = s0.bot_ends[1];
			if (a != -1) {
				quarter_edge_matches[a] = b;
				quarter_edge_matches[b] = a;
			}
			tstack.pop_back();
		}
		assert(nxt_edge_idx == NE);
		{
			std::vector<bool> edge_flip(NE);
			{
				bool planarity_flip = false;
				for (int e = 0; e < NE; e++) {
					planarity_flip ^= postorder_flip[e];
					edge_flip[postorder_edges[e]] = planarity_flip;
				}
				planarity_flip ^= postorder_flip[NE];
				assert(!planarity_flip);
			}
			for (int e = 0; e < NE; e++) {
				if (edge_flip[e]) {
					std::swap(quarter_edge_matches[4*e + 0], quarter_edge_matches[4*e + 1]);
					std::swap(quarter_edge_matches[4*e + 2], quarter_edge_matches[4*e + 3]);
				}
				for (int z = 0; z < 4; z++) {
					quarter_edge_matches[4*e + z] = (quarter_edge_matches[4*e+z] >> 1 << 1) | !(z & 1);
				}
			}
		}
	}
	return planar_embedding{std::move(quarter_edge_matches)};
}

inline bool can_planar_embed(
	int NV,
	const std::vector<std::array<int, 2>>& edges,
	std::span<const int> vert_order,
	std::span<const int> edge_order
) {
	// std::min is by reference, which breaks some optimizations
	auto setmin = [](auto& a, auto b) { if (b < a) a = b; };

	int NE = int(edges.size());

	auto [roots, outedges] = lowval_storted_skeleton_t::build(NV, edges, vert_order, edge_order);

	// Phase 2: do the big ear-decomposition-like walk

	// We're going to build a tree of all SPQR *nodes* + all original *vertices* (collectively *items*).
	// Vertices will hang off the first SPQR node containing them, and blocks will be rooted at a topmost Q node for the top edge.

	{
		int nxt_edge_idx = 0; // Counts backedges only

		std::vector<int> first_occurrence(NV); // First backedge to this depth

		struct tstack_planarity_side_t {
			// For each side, store pointers to the "linked lists" of the edges inside.
			// v[0] is the outer / longer edges and v[1] is the inner / shorter edges, matching the outside-in sort order.

			struct top_t {
				int end = -1;
				int depth = -1;
			};
			// top_ends are the outer/innermost exposed backedges
			// depths should be increasing going inwards
			std::array<top_t, 2> tops{top_t{-1, -1}, top_t{-1, -1}};
		};
		struct tstack_planarity_t {
			// The convention is that sides[0].tops[0].depth == top_depth, i.e. at least one minimal return lives on side 0
			std::array<tstack_planarity_side_t, 2> sides;
		};
		std::vector<tstack_planarity_side_t::top_t> prev_edge(NE, {-1, -1});
		auto merge_planarity_side = [&] [[gnu::always_inline]] (tstack_planarity_side_t& as, const tstack_planarity_side_t& bs) -> void {
			// If there's no bottom edges, then we must be an isolated vertex, so we can end early.
			// Caller must check that we're planar
			assert(as.tops[1].depth <= bs.tops[0].depth);
			prev_edge[bs.tops[0].end] = as.tops[1];
			as.tops[1] = bs.tops[1];
		};
		auto make_edge_planarity = [&] [[gnu::always_inline]] (int e, int top_depth, bool is_tree) -> tstack_planarity_t {
			tstack_planarity_t p;
			if (!is_tree) {
				p.sides[0].tops = {{{e, top_depth}, {e, top_depth}}};
			}
			return p;
		};
		struct tstack_t {
			int top_depth = -1;
			int first_idx = -1;
			tstack_planarity_t planarity;
		};
		std::vector<tstack_t> tstack; tstack.reserve(NV + NE);
		auto cur_tstack = [&] [[gnu::always_inline]] () -> tstack_t& { return tstack.end()[-1]; };
		auto nxt_tstack = [&] [[gnu::always_inline]] () -> tstack_t& { return tstack.end()[-2]; };

		auto push_tstack = [&] [[gnu::always_inline]] (int top_depth, tstack_planarity_t planarity) -> void {
			tstack.emplace_back(top_depth, nxt_edge_idx, planarity);
		};
		auto push_vert_tstack = [&] [[gnu::always_inline]] (int top_depth) -> void {
			push_tstack(top_depth, {});
		};
		auto push_edge_tstack = [&] [[gnu::always_inline]] (int top_depth, int e, bool is_tree) -> int {
			push_tstack(top_depth, make_edge_planarity(e, top_depth, is_tree));
			return nxt_edge_idx++;
		};

		struct dfs_stack_t {
			bool has_vert_tstack;
			int ch_idx;
			int ch_end;
			int orig_tstack;
		};
		std::vector<dfs_stack_t> stk; stk.reserve(NV);
		for (auto rt : roots) {
			auto push_vert = [&] [[gnu::always_inline]] (int cur) -> void {
				int cur_depth = int(stk.size());

				int lo = outedges.bounds[cur];
				int hi = outedges.bounds[cur+1];
				bool has_vert_tstack;
				{
					// Find the first same-BCC edge, and check it's type 2 (has lowval2), if so it's the ear tstack and we defer pushing ourselves.
					int first_edge = lo;
					while (first_edge < hi && outedges.dat[first_edge].key.is_new_block()) first_edge++;
					if (first_edge < hi && outedges.dat[first_edge].key.is_type_2()) {
						// Move first_edge to the beginning
						auto e = outedges.dat[first_edge];
						std::move_backward(outedges.dat.begin() + lo, outedges.dat.begin() + first_edge, outedges.dat.begin() + first_edge + 1);
						outedges.dat[lo] = e;
						has_vert_tstack = false;
					} else {
						push_vert_tstack(cur_depth);
						has_vert_tstack = true;
					}
				}
				stk.push_back({has_vert_tstack, lo, hi, -1});
			};
			// return true means jump to start_edge, return false means jump to finish_edge
			auto start_edge = [&] [[gnu::always_inline]] () -> std::optional<int> {
				int cur_depth = int(stk.size()) - 1;
				auto& s = stk.back();
				assert(s.ch_idx < s.ch_end);
				auto [_, nxt, e_side, key] = outedges.dat[s.ch_idx];
				auto [lowval, is_tree, is_type_1] = key.unpack(cur_depth);

				if (lowval >= cur_depth || is_type_1) assert(s.has_vert_tstack);

				s.orig_tstack = int(tstack.size());
				if (is_tree) {
					first_occurrence[cur_depth] = NE;
					return nxt;
				} else {
					return std::nullopt;
				}
			};
			auto finish_edge = [&] [[gnu::always_inline]] [[nodiscard]] () -> bool {
				int cur_depth = int(stk.size()) - 1;
				auto& s = stk.back();
				assert(s.ch_idx < s.ch_end);

				auto [_, nxt, e_side, key] = outedges.dat[s.ch_idx];
				int e = e_side >> 1;
				s.ch_idx++;

				auto [lowval, is_tree, is_type_1] = key.unpack(cur_depth);

				const int orig_tstack = s.orig_tstack;

				if (lowval >= cur_depth) {
					if (is_tree) {
						if (lowval == cur_depth) {
							// Delete the backedge
							tstack.pop_back();
						}
						// Delete the vertex
						tstack.pop_back();
					} else {
					}
					assert(s.has_vert_tstack);
					assert(int(tstack.size()) == orig_tstack);
					return true;
				}

				if (is_tree) {
					push_edge_tstack(cur_depth, e, true);
					while (nxt_tstack().top_depth >= cur_depth) {
						if (nxt_tstack().top_depth > cur_depth) {
							if (tstack.end()[-3].top_depth < cur_depth) {
								break;
							}
							// Merge the vertex in
							tstack.pop_back();
						}

						tstack.pop_back();
					}
					cur_tstack().planarity = tstack_planarity_t{};

					if (cur_tstack().first_idx > first_occurrence[cur_depth]) {
						int source = int(tstack.size()) - 2;
						while (tstack[source].first_idx > first_occurrence[cur_depth]) --source;

						// last_top == cur_tstack().top_depth
						int last_top = cur_depth;
						tstack_planarity_side_t cur_planarity = {};
						while (int(tstack.size()) > source + 2) {
							if (nxt_tstack().top_depth > cur_depth) {
								// Vertex or tree edge, no conditions
							} else if (nxt_tstack().top_depth == cur_depth) {
								if (nxt_tstack().planarity.sides[1].tops[0].depth != -1) {
									// Double-sided to cur_depth, conflicts with cur_tstack()
									assert(last_top < cur_depth);
									return false;
								}
								// Throw away the inner edge
							} else {
								if (nxt_tstack().planarity.sides[1].tops[0].depth != -1 && nxt_tstack().planarity.sides[1].tops[0].depth != cur_depth) {
									// Non-empty on both sides, conflicts with source
									return false;
								}
								if (nxt_tstack().planarity.sides[0].tops[1].depth > last_top) {
									// Nonlaminar with cur_tstack()
									return false;
								}
								if (last_top < cur_depth) {
									auto nxt_planarity = nxt_tstack().planarity.sides[0];
									prev_edge[cur_planarity.tops[0].end] = nxt_planarity.tops[1];
									cur_planarity.tops[0] = nxt_planarity.tops[0];
								} else {
									cur_planarity = nxt_tstack().planarity.sides[0];
								}
								last_top = nxt_tstack().top_depth;
							}
							tstack.pop_back();
						}

						int t0 = nxt_tstack().planarity.sides[0].tops[1].depth;
						int t1 = nxt_tstack().planarity.sides[1].tops[1].depth;
						assert(t0 == cur_depth || t1 == cur_depth);
						// Handles -1 correctly
						if (std::min(t0, t1) > last_top) {
							assert(last_top < cur_depth);
							return false;
						}
						if (last_top < cur_depth) {
							if (t0 == cur_depth) {
								// We need to flip cur_tstack and nxt_tstack relative to each other.
								// Flip the one with worse top_depth.
								if (t1 != -1) {
									// merge into side 1
									merge_planarity_side(nxt_tstack().planarity.sides[1], cur_planarity);
								} else {
									nxt_tstack().planarity.sides[1] = cur_planarity;
								}
								if (last_top < nxt_tstack().top_depth) {
									nxt_tstack().top_depth = last_top;
									std::swap(nxt_tstack().planarity.sides[0], nxt_tstack().planarity.sides[1]);
								}
							} else {
								assert(t0 < cur_depth);
								assert(t0 <= last_top);
								// merge into side 0
								merge_planarity_side(nxt_tstack().planarity.sides[0], cur_planarity);
							}
						}
						tstack.pop_back();

						// Prune off finished cur-side things
						for (auto& side : cur_tstack().planarity.sides) {
							while (side.tops[1].depth == cur_depth) {
								side.tops[1] = prev_edge[side.tops[1].end];
								if (side.tops[1].end == -1) {
									side.tops[0] = {};
								}
							}
						}
					}

					if (is_type_1) assert(s.has_vert_tstack);
					if (s.has_vert_tstack) {
						// NB: tstack[orig_size] is the vertex and tstack[orig_size+1] is the backedge; maybe we should reverse them?
						assert(int(tstack.size()) >= orig_tstack + 3);

						if (!is_type_1) {
							// The lowval side should be side 1, everything else goes on side 0.
							// The exception is tstack[orig_tstack + 2], which could be == lowval on one/both sides,
							// but is guaranteed to have *something* > lowval by non-type-1-ness
							auto& t = tstack[orig_tstack + 2];
							{
								assert(t.planarity.sides[0].tops[0].depth == t.top_depth);
								assert(t.planarity.sides[0].tops[1].depth != -1);
								if (t.planarity.sides[0].tops[1].depth == lowval) {
									t.planarity.sides[0] = t.planarity.sides[1];
								} else if (t.planarity.sides[1].tops[1].depth != -1 && t.planarity.sides[1].tops[1].depth != lowval) {
									return false;
								}
								assert(t.planarity.sides[0].tops[1].depth > lowval);
							}
							tstack_planarity_side_t cur_planarity = t.planarity.sides[0];
							for (int i = orig_tstack + 3; i < int(tstack.size()); i++) {
								if (tstack[i].top_depth == lowval) {
									std::swap(tstack[i].planarity.sides[0], tstack[i].planarity.sides[1]);
								}
								if (tstack[i].planarity.sides[1].tops[1].depth != -1 && tstack[i].planarity.sides[1].tops[1].depth != lowval) {
									return false;
								}
								int next_top = tstack[i].planarity.sides[0].tops[0].depth;
								if (next_top != -1) {
									if (cur_planarity.tops[1].depth > next_top) return false;
									merge_planarity_side(cur_planarity, tstack[i].planarity.sides[0]);
								}
							}
							merge_planarity_side(tstack[orig_tstack+1].planarity.sides[0], cur_planarity);
							tstack[orig_tstack+1].planarity.sides[1] = tstack_planarity_side_t{};
						} else {
							assert(int(tstack.size()) == orig_tstack + 3);
						}
						tstack[orig_tstack] = tstack[orig_tstack+1];
						tstack.resize(orig_tstack + 1);
						assert(cur_tstack().top_depth == lowval);
					}
				} else {
					assert(is_type_1);
					int idx = push_edge_tstack(lowval, e, false);
					setmin(first_occurrence[lowval], idx);
				}

				if (is_type_1 && nxt_tstack().top_depth == lowval) {
					assert(s.has_vert_tstack);
					tstack.pop_back();
				}

				if (!s.has_vert_tstack) {
					assert(!is_type_1);
					// Throw cur_vert_node onto the tstack so it'll get interleaved correctly
					push_vert_tstack(cur_depth);
					s.has_vert_tstack = true;
				}
				return true;
			};
			auto pop_vert = [&] [[gnu::always_inline]] () -> void {
				auto& s = stk.back();
				assert(s.ch_idx == s.ch_end);
				assert(s.has_vert_tstack);
				stk.pop_back();
			};

			push_vert(rt);
			while (true) {
				if (stk.back().ch_idx == stk.back().ch_end) {
					pop_vert();
					if (stk.empty()) break;
					if (auto res = finish_edge(); !res) return false;
				} else if (std::optional<int> nxt = start_edge(); nxt) {
					push_vert(*nxt);
				} else {
					if (auto res = finish_edge(); !res) assert(false);
				}
			}
			assert(int(tstack.size()) == 1);
			tstack.pop_back();
		}
	}
	return true;
}

} // namespace wala
#line 7 "verify/graph/two_edge_connected_components-spqr.test.cpp"

int main() {
	std::ios_base::sync_with_stdio(false), std::cin.tie(nullptr);

	int N, M; std::cin >> N >> M;
	std::vector<std::array<int, 2>> edges(M);
	for (auto& [x, y] : edges) std::cin >> x >> y;

	auto spqr = wala::spqr_tree::build(N, edges);
	using node_type = wala::spqr_tree::node_type;
	std::vector<std::pair<int, int>> stk; stk.reserve(N);

	std::vector<int> comp_verts(N, -1);
	std::vector<int> comp_bounds; comp_bounds.reserve(N+1); comp_bounds.push_back(0);

	for (int i = int(spqr.size()) - 1; i >= 0; i--) {
		int comp_end = -1;
		if (spqr.types[i] == node_type::V) {
			stk.push_back({i, spqr.orig_id[i]});
			if (spqr.par[i] == 0) {
				comp_end = spqr.subtree_end[i];
			}
		} else if (spqr.types[i] == node_type::I) {
			int p = spqr.par[i];
			assert(p == i-1 && spqr.types[p] == node_type::Q);
			comp_end = spqr.subtree_end[p];
		}
		if (comp_end != -1) {
			// Take off
			int idx = comp_bounds.back();
			while (!stk.empty() && stk.back().first < comp_end) {
				comp_verts[idx++] = stk.back().second;
				stk.pop_back();
			}
			comp_bounds.push_back(idx);
		}
	}
	assert(stk.empty());
	assert(comp_bounds.back() == N);

	int K = int(comp_bounds.size()) - 1;
	std::cout << K << '\n';
	for (int i = 0; i < K; i++) {
		std::cout << comp_bounds[i+1] - comp_bounds[i];
		for (int j = comp_bounds[i]; j < comp_bounds[i+1]; j++) {
			std::cout << ' ' << comp_verts[j];
		}
		std::cout << '\n';
	}

	return 0;
}
// clang-format off
// @formatter:off
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wpragmas"
#pragma GCC diagnostic ignored "-Wunknown-warning-option"
#pragma GCC diagnostic ignored "-Wmisleading-indentation"
#pragma GCC diagnostic ignored "-Wmultistatement-macros"
#include <bits/stdc++.h>
// src/graph/spqr_tree.hpp
namespace wala{
struct csr_index{
std::vector<int>bounds;
std::ranges::iota_view<int,int>indices(int i)const{return std::views::iota(bounds[i],bounds[i+1]);}
template<std::ranges::contiguous_range R>auto slice(int i,R&&base)const{
return std::span(base).subspan(bounds[i],bounds[i+1]-bounds[i]);
}
int num_rows()const{return bounds.empty()?0:int(bounds.size())-1;}
int num_entries()const{return bounds.empty()?0:bounds.back();}
};
template<typename T>struct csr:csr_index{
std::vector<T>dat;
std::span<T>operator[](int i){return slice(i,dat);}
std::span<const T>operator[](int i)const{return slice(i,dat);}
};
struct csr_index_builder{
std::vector<int>bounds;
csr_index_builder()=default;
explicit csr_index_builder(int N):bounds(N+1){}
void count(int k){bounds[k+1]++;}
csr_index finalize()&&{
for(int i=1;i<int(bounds.size());i++){
bounds[i]+=bounds[i-1];
}
return{std::move(bounds)};
}
};
template<typename T>struct csr_builder{
csr_index idx;
std::vector<T>dat;
csr_builder()=default;
explicit csr_builder(csr_index idx_,std::vector<T>&&dat_buf={}):idx(std::move(idx_)),dat(std::move(dat_buf)){
dat.resize(idx.num_entries());
if(!idx.bounds.empty()){
idx.bounds.pop_back();
idx.bounds.insert(idx.bounds.begin(),0);
}
}
explicit csr_builder(csr_index_builder&&idx_builder,std::vector<T>&&dat_buf={}):idx{std::move(idx_builder.bounds)},dat(std::move(dat_buf)){
int l=0;
for(int i=1;i<int(idx.bounds.size());i++){
idx.bounds[i]=std::exchange(l,l+idx.bounds[i]);
}
dat.resize(l);
}
[[nodiscard]]T&push(int k){return dat[idx.bounds[k+1]++];}
[[nodiscard]]csr<T>finalize()&&{return{std::move(idx),std::move(dat)};}
};
struct planar_spqr_tree;
struct spqr_tree{
enum class node_type:char{
F='F',V='V',Q='Q',I='I',O='O',S='S',P='P',R='R'
};
friend std::ostream&operator<<(std::ostream&o,node_type t){return o<<char(t);}
std::vector<int>vert_index;
std::vector<int>edge_index;
std::vector<bool>edge_flipped;
std::vector<int>par;
std::vector<int>subtree_end;
std::vector<node_type>types;
std::vector<int>orig_id;
csr<int>ch;
struct node_vert_t{
int node;
int vert;
};
std::vector<node_vert_t>node_verts;
csr_index node_nvs;
std::vector<int>vert_par_nv;
struct node_edge_t{
int node;
int twin_ne;
std::array<int,2>nvs;
};
std::vector<node_edge_t>node_edges;
csr_index node_nes;
struct node_adj_t{
int ne;
int dest_nv;
};
csr<node_adj_t>node_adj;
int size()const{return int(par.size());}
static spqr_tree build(
int NV,
const std::vector<std::array<int,2>>&edges,
bool ternarize=false,
std::span<const int>vert_order={},
std::span<const int>edge_order={}
){
return build_impl<false>(NV,edges,ternarize,vert_order,edge_order);
}
protected:
template<bool with_planarity>
static std::conditional_t<with_planarity,planar_spqr_tree,spqr_tree>build_impl(
int NV,
const std::vector<std::array<int,2>>&edges,
bool ternarize,
std::span<const int>vert_order,
std::span<const int>edge_order
);
};
struct planar_embedding{
std::vector<int>rot_adj;
};
struct planar_spqr_tree:spqr_tree{
std::vector<bool>node_planar;
planar_embedding ne_embedding;
static planar_spqr_tree build(
int NV,
const std::vector<std::array<int,2>>&edges,
bool ternarize=false,
std::span<const int>vert_order={},
std::span<const int>edge_order={}
){
return build_impl<true>(NV,edges,ternarize,vert_order,edge_order);
}
};
struct lowval_storted_skeleton_t{
std::vector<int>roots;
struct key_t{int lowval;bool is_tree;bool is_type_1;};
struct packed_key_t{
int v;
friend auto operator<=>(packed_key_t a,packed_key_t b)=default;
[[nodiscard]]bool is_new_block()const{return v<6;}
[[nodiscard]]bool is_type_2()const{return v%3==2;}
[[nodiscard]]key_t unpack(int cur_depth)const{
int lowval=v/3-2;if(lowval<0)lowval=cur_depth+~lowval;
int kind=v%3;
bool is_tree=kind!=1;
bool is_type_1=kind<=1;
return{lowval,is_tree,is_type_1};
}
};
struct outedge_t{int src,dest;int e_side;packed_key_t key;};
csr<outedge_t>outedges;
static lowval_storted_skeleton_t build(
int NV,
const std::vector<std::array<int,2>>&edges,
std::span<const int>vert_order,
std::span<const int>edge_order
){
auto min=[](auto a,auto b){return a<b?a:b;};
int NE=int(edges.size());
assert(int(vert_order.size())<=NV);
assert(int(edge_order.size())<=NE);
auto for_each_in_order=[][[gnu::always_inline]](int n,std::span<const int>order,auto f)->void{
for(int i:order)f(i);
if(int(order.size())==n)return;
if(order.empty()){
for(int i=0;i<n;i++)f(i);
}else if(order.size()==1){
for(int i=0;i<n;i++){
if(i!=order[0])f(i);
}
}else{
std::vector<bool>listed(n);
for(int i:order)listed[i]=true;
for(int i=0;i<n;i++){
if(!listed[i])f(i);
}
}
};
std::vector<int>roots;roots.reserve(NV);
csr<outedge_t>outedges;
{
std::vector<int>depth(NV,-1);
struct edge_t{int dest;int e;};
csr_index_builder adj_idx_builder(NV);
for(auto[u,v]:edges){
adj_idx_builder.count(u);
if(u!=v)adj_idx_builder.count(v);
}
csr_builder<edge_t>adj_builder(std::move(adj_idx_builder));
for_each_in_order(NE,edge_order,[&][[gnu::always_inline]](int e)->void{
auto[u,v]=edges[e];
adj_builder.push(u)={v,2*e+0};
if(u!=v)adj_builder.push(v)={u,2*e+1};
});
auto adj=std::move(adj_builder).finalize();
std::vector<outedge_t>all_outedges;all_outedges.reserve(NE);
struct dfs_stack_t{
int cur;
int prv_e;
std::array<int,2>lowvals;
int ch_idx;
int ch_end;
};
std::vector<dfs_stack_t>stk;stk.reserve(NV);
auto push_vert=[&][[gnu::always_inline]](int cur,int prv_e)->void{
int d=int(stk.size());
depth[cur]=d;
stk.push_back({cur,prv_e,{d,d},adj.bounds[cur],adj.bounds[cur+1]});
};
auto finish_edge=[&][[gnu::always_inline]](bool is_tree,std::array<int,2>n_lowvals)->void{
int d=int(stk.size())-1;
auto&s=stk.back();
int cur=s.cur;
assert(s.ch_idx<s.ch_end);
auto[nxt,e]=adj.dat[s.ch_idx];
auto&lowvals=s.lowvals;
s.ch_idx++;
{
int lowval=n_lowvals[0];
if(lowval>=d)lowval=~(lowval-d);
int kind=2*(n_lowvals[1]<d)+!is_tree;
all_outedges.push_back({cur,nxt,e,packed_key_t{3*(lowval+2)+kind}});
}
if(n_lowvals[0]<lowvals[0])lowvals={n_lowvals[0],min(n_lowvals[1],lowvals[0])};
else lowvals[1]=min(lowvals[1],n_lowvals[0]==lowvals[0]?n_lowvals[1]:n_lowvals[0]);
};
auto start_edge=[&][[gnu::always_inline]]()->void{
int d=int(stk.size())-1;
auto&s=stk.back();
assert(s.ch_idx<s.ch_end);
auto[nxt,e]=adj.dat[s.ch_idx];
if((e^1)==s.prv_e||depth[nxt]>d){
s.ch_idx++;return;
}
bool is_tree=depth[nxt]==-1;
if(is_tree){
push_vert(nxt,e);
}else{
finish_edge(false,{depth[nxt],d});
}
};
auto pop_vert=[&][[gnu::always_inline]]()->std::array<int,2>{
auto lowvals=stk.back().lowvals;
stk.pop_back();
return lowvals;
};
for_each_in_order(NV,vert_order,[&][[gnu::always_inline]](int rt)->void{
if(depth[rt]==-1){
roots.push_back(rt);
push_vert(rt,-1);
while(true){
if(stk.back().ch_idx==stk.back().ch_end){
auto lowvals=pop_vert();
if(stk.empty())break;
finish_edge(true,lowvals);
}else{
start_edge();
}
}
}
});
csr_index_builder by_key_idx_builder(3*NV+6);
for(auto edge:all_outedges)by_key_idx_builder.count(edge.key.v);
csr_builder<outedge_t>by_key_builder(std::move(by_key_idx_builder));
for(auto edge:all_outedges)by_key_builder.push(edge.key.v)=edge;
csr<outedge_t>by_key=std::move(by_key_builder).finalize();
csr_index_builder by_src_idx_builder(NV);
for(auto edge:by_key.dat)by_src_idx_builder.count(edge.src);
csr_builder<outedge_t>by_src_builder(std::move(by_src_idx_builder),std::move(all_outedges));
for(auto edge:by_key.dat)by_src_builder.push(edge.src)=edge;
outedges=std::move(by_src_builder).finalize();
}
return{std::move(roots),std::move(outedges)};
}
};
template<bool with_planarity>
std::conditional_t<with_planarity,planar_spqr_tree,spqr_tree>spqr_tree::build_impl(
int NV,
const std::vector<std::array<int,2>>&edges,
bool ternarize,
std::span<const int>vert_order,
std::span<const int>edge_order
){
auto setmin=[](auto&a,auto b){if(b<a)a=b;};
int NE=int(edges.size());
assert(int(vert_order.size())<=NV);
assert(int(edge_order.size())<=NE);
auto[roots,outedges]=lowval_storted_skeleton_t::build(NV,edges,vert_order,edge_order);
constexpr int ROOT_ITEM=0;
auto vert_item=[&][[gnu::always_inline]](int v)->int{return 1+v;};
auto edge_item=[&][[gnu::always_inline]](int e)->int{return 1+NV+e;};
auto set_sides=[]<typename T>(bool dir,T a,T b)->std::array<T,2>{
return dir?std::array<T,2>{b,a}:std::array<T,2>{a,b};
};
auto get_side=[]<typename T>(std::array<T,2>a,bool dir)->T{
return dir?a[1]:a[0];
};
struct item_list{
std::array<int,2>v{-1,-1};
[[nodiscard]]bool empty()const{return v[0]<0;}
};
std::vector<int>ch_nxt;ch_nxt.reserve(1+NV+NE+NE);ch_nxt.assign(1+NV+NE,-1);
auto concat=[&][[gnu::always_inline]](item_list a,item_list b)->item_list{
if(b.empty())return a;
if(a.empty())return b;
ch_nxt[a.v[1]>>1]=b.v[0]^(a.v[1]&1);
return{{a.v[0],b.v[1]}};
};
auto unit_list=[&][[gnu::always_inline]](int item)->item_list{
return{{item<<1,item<<1}};
};
std::vector<std::array<int,2>>item_vs;item_vs.reserve(1+NV+2*NE);item_vs.resize(1+NV+NE,{-1,-1});
std::vector<item_list>item_ch;item_ch.reserve(1+NV+2*NE);item_ch.resize(1+NV+NE,item_list{});
std::vector<node_type>item_types;item_types.reserve(1+NV+2*NE);
item_types.resize(1,node_type::F);
item_types.resize(1+NV,node_type::V);
item_types.resize(1+NV+NE,node_type::Q);
std::vector<int>quarter_edge_matches(with_planarity?8*NE+4:0,-1);
struct nonplanarity_certficate_t{};
std::vector<std::expected<std::array<int,4>,nonplanarity_certficate_t>>node_planarity;
if constexpr(with_planarity)node_planarity.reserve(NE);
int tot_blocks=0;
int tot_self_loops=0;
{
auto alloc_item=[&][[gnu::always_inline]](node_type type)->int{
int item=int(item_vs.size());
item_vs.push_back({});
item_ch.push_back({});
item_types.push_back(type);
ch_nxt.push_back(-1);
if constexpr(with_planarity)node_planarity.emplace_back();
return item;
};
std::vector<int>stack_verts(NV);
std::vector<int8_t>stack_dir(NV);
auto make_vs=[&][[gnu::always_inline]](int v_start,int top_depth)->std::array<int,2>{
return set_sides(stack_dir[top_depth],stack_verts[top_depth],v_start);
};
int nxt_edge_idx=0;
std::vector<int>first_occurrence(NV);
std::vector<int>edge_top_depths(with_planarity?2*NE:0,-1);
struct tstack_planarity_side_t{
std::array<int,2>bot_ends{-1,-1};
struct top_t{
int end=-1;
int depth=-1;
};
std::array<top_t,2>tops{top_t{-1,-1},top_t{-1,-1}};
};
struct tstack_planarity_t{
std::array<tstack_planarity_side_t,2>sides;
};
struct tstack_nonplanarity_t{
};
using tstack_maybe_planarity_t=std::conditional_t<with_planarity,std::expected<tstack_planarity_t,tstack_nonplanarity_t>,std::monostate>;
auto merge_planarity=[&][[gnu::always_inline]](tstack_maybe_planarity_t&a,const tstack_maybe_planarity_t&b)->void{
if constexpr(with_planarity){
if(!a)return;
if(!b){a=b;return;}
for(int z=0;z<2;z++){
auto&as=a->sides[z];
const auto&bs=b->sides[z];
if(bs.bot_ends[0]==-1){
}else if(as.bot_ends[0]==-1){
as=bs;
}else{
quarter_edge_matches[as.bot_ends[1]]=bs.bot_ends[0];
quarter_edge_matches[bs.bot_ends[0]]=as.bot_ends[1];
as.bot_ends[1]=bs.bot_ends[1];
if(bs.tops[0].end==-1){
}else if(as.tops[0].end==-1){
as.tops=bs.tops;
}else{
assert(as.tops[1].depth<=bs.tops[0].depth);
quarter_edge_matches[as.tops[1].end]=bs.tops[0].end;
quarter_edge_matches[bs.tops[0].end]=as.tops[1].end;
as.tops[1]=bs.tops[1];
}
}
}
}
};
auto make_edge_planarity=[&][[gnu::always_inline]](int item,int top_depth,bool is_tree)->tstack_maybe_planarity_t{
if constexpr(with_planarity){
assert(item>=1+NV);
int ve=item-(1+NV);
bool top_dir=stack_dir[top_depth];
edge_top_depths[ve]=top_depth;
tstack_planarity_t p;
if(is_tree){
p.sides[0].bot_ends={4*ve+2*!top_dir+0,4*ve+2*top_dir+1};
p.sides[1].bot_ends={4*ve+2*!top_dir+1,4*ve+2*top_dir+0};
}else{
p.sides[0].bot_ends={4*ve+2*!top_dir+0,4*ve+2*!top_dir+1};
p.sides[0].tops={{{4*ve+2*top_dir+1,top_depth},{4*ve+2*top_dir+0,top_depth}}};
}
return p;
}else{
return{};
}
};
struct tstack_t{
int v_start=-1;
int top_depth=-1;
int first_idx=-1;
std::array<item_list,2>spans;
[[no_unique_address]]tstack_maybe_planarity_t planarity;
};
std::vector<tstack_t>tstack;tstack.reserve(NV+NE);
auto cur_tstack=[&][[gnu::always_inline]]()->tstack_t&{return tstack.end()[-1];};
auto nxt_tstack=[&][[gnu::always_inline]]()->tstack_t&{return tstack.end()[-2];};
auto push_tstack=[&][[gnu::always_inline]](int v_start,int top_depth,int item,tstack_maybe_planarity_t planarity)->void{
tstack.emplace_back(v_start,top_depth,nxt_edge_idx,set_sides(stack_dir[top_depth],unit_list(item),{}),planarity);
};
auto push_vert_tstack=[&][[gnu::always_inline]](int v,int top_depth)->void{
int item=vert_item(v);
push_tstack(v,top_depth,item,{});
};
auto push_edge_tstack=[&][[gnu::always_inline]](int v_start,int top_depth,int e,bool is_tree)->void{
int item=edge_item(e);
push_tstack(v_start,top_depth,item,make_edge_planarity(item,top_depth,is_tree));
};
auto flip_tstack_planarity=[&][[gnu::always_inline]](tstack_t&a)->void{
if constexpr(with_planarity){
a.spans[0].v[0]^=1;
a.spans[0].v[1]^=1;
a.spans[1].v[0]^=1;
a.spans[1].v[1]^=1;
if(a.planarity){
std::swap(a.planarity->sides[0],a.planarity->sides[1]);
}
}
};
auto merge_tstack_tops=[&][[gnu::always_inline]]()->void{
tstack_t&a=nxt_tstack();
const tstack_t&b=cur_tstack();
setmin(a.top_depth,b.top_depth);
a.spans[0]=concat(b.spans[0],a.spans[0]);
a.spans[1]=concat(a.spans[1],b.spans[1]);
if constexpr(with_planarity){
merge_planarity(a.planarity,b.planarity);
}
tstack.pop_back();
};
auto maybe_unwrap_nxt=[&][[gnu::always_inline]](node_type type,bool is_tree)->int{
tstack_t&t=nxt_tstack();
if(type==node_type::R)return alloc_item(type);
assert(type==node_type::P||type==node_type::S);
if(ternarize)return alloc_item(type);
bool top_dir=stack_dir[t.top_depth];
assert(get_side(t.spans,!top_dir).empty());
int item=get_side(t.spans,top_dir).v[0]>>1;
assert(item==(get_side(t.spans,top_dir).v[1]>>1));
if(item_types[item]==type){
t.spans=set_sides(top_dir,item_ch[item],{});
if constexpr(with_planarity){
assert(node_planarity[item-(1+NV+NE)]);
const auto&matches=*node_planarity[item-(1+NV+NE)];
assert(t.planarity);
auto&p=*t.planarity;
if(is_tree){
p.sides[0].bot_ends[0]=matches[2*!top_dir+1];
p.sides[0].bot_ends[1]=matches[2*top_dir+0];
p.sides[1].bot_ends[0]=matches[2*!top_dir+0];
p.sides[1].bot_ends[1]=matches[2*top_dir+1];
}else{
p.sides[0].bot_ends[0]=matches[2*!top_dir+1];
p.sides[0].bot_ends[1]=matches[2*!top_dir+0];
p.sides[0].tops[0].end=matches[2*top_dir+0];
p.sides[0].tops[1].end=matches[2*top_dir+1];
}
}
return item;
}else{
return alloc_item(type);
}
};
auto finish_tstack_top=[&][[gnu::always_inline]](int item,bool is_tree)->void{
tstack_t&t=cur_tstack();
bool top_dir=stack_dir[t.top_depth];
assert(get_side(t.spans,!top_dir).empty());
if constexpr(with_planarity){
if(t.planarity){
const auto&p=*t.planarity;
std::array<int,4>matches{};
if(is_tree){
matches[2*!top_dir+1]=p.sides[0].bot_ends[0];
matches[2*top_dir+0]=p.sides[0].bot_ends[1];
matches[2*!top_dir+0]=p.sides[1].bot_ends[0];
matches[2*top_dir+1]=p.sides[1].bot_ends[1];
}else{
matches[2*!top_dir+1]=p.sides[0].bot_ends[0];
matches[2*!top_dir+0]=p.sides[0].bot_ends[1];
matches[2*top_dir+0]=p.sides[0].tops[0].end;
matches[2*top_dir+1]=p.sides[0].tops[1].end;
}
node_planarity[item-(1+NV+NE)]=matches;
}else{
assert(item_types[item]==node_type::R);
node_planarity[item-(1+NV+NE)]=std::unexpected(nonplanarity_certficate_t{});
}
}
item_vs[item]=make_vs(t.v_start,t.top_depth);
item_ch[item]=get_side(t.spans,top_dir);
t.spans=set_sides(top_dir,unit_list(item),{});
t.planarity=make_edge_planarity(item,t.top_depth,is_tree);
};
struct dfs_stack_t{
bool has_vert_tstack;
int ch_idx;
int ch_end;
int orig_tstack;
};
std::vector<dfs_stack_t>stk;stk.reserve(NV);
for(auto rt:roots){
auto push_vert=[&][[gnu::always_inline]](int cur)->void{
int cur_depth=int(stk.size());
stack_verts[cur_depth]=cur;
int lo=outedges.bounds[cur];
int hi=outedges.bounds[cur+1];
bool has_vert_tstack;
{
int first_edge=lo;
while(first_edge<hi&&outedges.dat[first_edge].key.is_new_block())first_edge++;
if(first_edge<hi&&outedges.dat[first_edge].key.is_type_2()){
auto e=outedges.dat[first_edge];
std::move_backward(outedges.dat.begin()+lo,outedges.dat.begin()+first_edge,outedges.dat.begin()+first_edge+1);
outedges.dat[lo]=e;
has_vert_tstack=false;
}else{
push_vert_tstack(cur,cur_depth);
has_vert_tstack=true;
}
}
stk.push_back({has_vert_tstack,lo,hi,-1});
};
auto start_edge=[&][[gnu::always_inline]]()->std::optional<int>{
int cur_depth=int(stk.size())-1;
auto&s=stk.back();
assert(s.ch_idx<s.ch_end);
auto[_,nxt,e_side,key]=outedges.dat[s.ch_idx];
auto[lowval,is_tree,is_type_1]=key.unpack(cur_depth);
stack_dir[cur_depth]=(lowval>=cur_depth?false:!stack_dir[lowval]);
s.orig_tstack=int(tstack.size());
if(is_tree){
stack_dir[cur_depth+1]=!stack_dir[std::min(lowval,cur_depth)];
first_occurrence[cur_depth]=NE;
return nxt;
}else{
return std::nullopt;
}
};
auto finish_edge=[&][[gnu::always_inline]]()->void{
int cur_depth=int(stk.size())-1;
auto&s=stk.back();
int cur=stack_verts[cur_depth];
assert(s.ch_idx<s.ch_end);
auto[_,nxt,e_side,key]=outedges.dat[s.ch_idx];
int e=e_side>>1;
s.ch_idx++;
auto[lowval,is_tree,is_type_1]=key.unpack(cur_depth);
const int orig_tstack=s.orig_tstack;
const bool edge_dir=stack_dir[cur_depth];
if(lowval>=cur_depth){
item_vs[edge_item(e)]={cur,-1};
tot_blocks++;
if(is_tree){
if(lowval==cur_depth+1){
int item=alloc_item(node_type::I);
item_vs[item]=make_vs(nxt,cur_depth);
item_ch[edge_item(e)]=concat(unit_list(item),tstack.back().spans[1]);tstack.pop_back();
}else{
auto backedge=tstack.back().spans[0];tstack.pop_back();
item_ch[edge_item(e)]=concat(backedge,tstack.back().spans[1]);tstack.pop_back();
}
}else{
assert(nxt==cur);
tot_self_loops++;
int item=alloc_item(node_type::O);
item_vs[item]={cur,-1};
item_ch[edge_item(e)]=unit_list(item);
}
item_ch[vert_item(cur)]=concat(item_ch[vert_item(cur)],unit_list(edge_item(e)));
return;
}
assert(lowval<cur_depth);
item_vs[edge_item(e)]=make_vs(nxt,cur_depth);
if(is_tree){
push_edge_tstack(nxt,cur_depth,e,true);
while(nxt_tstack().top_depth>=cur_depth){
node_type type;
if(nxt_tstack().top_depth>cur_depth){
if(tstack.end()[-3].top_depth<cur_depth){
break;
}
stack_dir[nxt_tstack().top_depth]=edge_dir;
merge_tstack_tops();
type=nxt_tstack().top_depth>cur_depth?node_type::S:node_type::R;
}else if(nxt_tstack().v_start==cur_tstack().v_start){
type=node_type::P;
}else{
type=node_type::R;
}
int item=maybe_unwrap_nxt(type,type==node_type::S);
merge_tstack_tops();
if constexpr(with_planarity){
if(cur_tstack().planarity){
for(auto&side:cur_tstack().planarity->sides){
assert(side.bot_ends[1]!=-1);
if(side.tops[1].end==-1)continue;
assert(side.tops[0].depth==cur_depth);
assert(side.tops[1].depth==cur_depth);
quarter_edge_matches[side.bot_ends[1]]=side.tops[1].end;
quarter_edge_matches[side.tops[1].end]=side.bot_ends[1];
side.bot_ends[1]=side.tops[0].end;
side.tops={};
}
}
}
finish_tstack_top(item,true);
}
if(cur_tstack().first_idx>first_occurrence[cur_depth]){
if constexpr(with_planarity){
[&][[gnu::always_inline]]()->void{
int source=int(tstack.size())-1;
do{
--source;
if(!tstack[source].planarity){
cur_tstack().planarity=tstack[source].planarity;
return;
}
}while(tstack[source].first_idx>first_occurrence[cur_depth]);
int last_top=cur_depth;
while(int(tstack.size())>source+2){
if(nxt_tstack().top_depth>cur_depth){
}else if(nxt_tstack().top_depth==cur_depth){
if(nxt_tstack().planarity->sides[1].tops[0].depth!=-1){
assert(last_top<cur_depth);
cur_tstack().planarity=std::unexpected(tstack_nonplanarity_t{});
return;
}
flip_tstack_planarity(nxt_tstack());
}else{
if(nxt_tstack().planarity->sides[1].tops[0].depth!=-1&&nxt_tstack().planarity->sides[1].tops[0].depth!=cur_depth){
if(nxt_tstack().planarity->sides[1].tops[0].depth==nxt_tstack().top_depth){
cur_tstack().planarity=std::unexpected(tstack_nonplanarity_t{});
}else{
cur_tstack().planarity=std::unexpected(tstack_nonplanarity_t{});
}
return;
}
if(nxt_tstack().planarity->sides[0].tops[1].depth>last_top){
assert(last_top<cur_depth);
cur_tstack().planarity=std::unexpected(tstack_nonplanarity_t{});
return;
}
last_top=nxt_tstack().top_depth;
}
merge_tstack_tops();
}
int t0=nxt_tstack().planarity->sides[0].tops[1].depth;
int t1=nxt_tstack().planarity->sides[1].tops[1].depth;
assert(t0==cur_depth||t1==cur_depth);
if(std::min(t0,t1)>last_top){
assert(last_top<cur_depth);
cur_tstack().planarity=std::unexpected(tstack_nonplanarity_t{});
return;
}
if(t0==cur_depth){
flip_tstack_planarity(cur_tstack().top_depth<nxt_tstack().top_depth?nxt_tstack():cur_tstack());
}
merge_tstack_tops();
for(auto&side:cur_tstack().planarity->sides){
assert(side.bot_ends[1]!=-1);
while(side.tops[1].depth==cur_depth){
{
quarter_edge_matches[side.bot_ends[1]]=side.tops[1].end;
quarter_edge_matches[side.tops[1].end]=side.bot_ends[1];
side.bot_ends[1]=side.tops[1].end^1;
}
side.tops[1].end=std::exchange(quarter_edge_matches[side.bot_ends[1]],-1);
if(side.tops[1].end!=-1){
quarter_edge_matches[side.tops[1].end]=-1;
side.tops[1].depth=edge_top_depths[side.tops[1].end>>2];
}else{
side.tops={};
}
}
}
}();
}
while(cur_tstack().first_idx>first_occurrence[cur_depth]){
merge_tstack_tops();
}
}
if(is_type_1)assert(s.has_vert_tstack);
if(s.has_vert_tstack){
assert(int(tstack.size())>=orig_tstack+3);
if(!is_type_1){
if constexpr(with_planarity){
[&][[gnu::always_inline]]()->void{
auto&t=tstack[orig_tstack+2];
if(!t.planarity){
cur_tstack().planarity=t.planarity;
return;
}
assert(t.planarity->sides[0].tops[0].depth==t.top_depth);
assert(t.planarity->sides[0].tops[1].depth!=-1);
if(t.planarity->sides[0].tops[1].depth==lowval){
flip_tstack_planarity(t);
}else if(t.planarity->sides[1].tops[1].depth!=-1&&t.planarity->sides[1].tops[1].depth!=lowval){
cur_tstack().planarity=std::unexpected(tstack_nonplanarity_t{});
return;
}
assert(t.planarity->sides[0].tops[1].depth>lowval);
int last_top=t.planarity->sides[0].tops[1].depth;
for(int i=orig_tstack+3;i<int(tstack.size());i++){
if(!tstack[i].planarity){
cur_tstack().planarity=tstack[i].planarity;
return;
}
if(tstack[i].top_depth==lowval){
flip_tstack_planarity(tstack[i]);
}
if(tstack[i].planarity->sides[1].tops[1].depth!=-1&&tstack[i].planarity->sides[1].tops[1].depth!=lowval){
cur_tstack().planarity=std::unexpected(tstack_nonplanarity_t{});
return;
}
int next_top=tstack[i].planarity->sides[0].tops[0].depth;
if(next_top!=-1){
if(last_top>next_top){
cur_tstack().planarity=std::unexpected(tstack_nonplanarity_t{});
return;
}
last_top=tstack[i].planarity->sides[0].tops[1].depth;
}
}
}();
}
while(int(tstack.size())>orig_tstack+3){
merge_tstack_tops();
}
}
assert(int(tstack.size())==orig_tstack+3);
int item;
if(is_type_1){
item=maybe_unwrap_nxt(cur_tstack().top_depth==cur_depth?node_type::S:node_type::R,false);
}else{
item=-1;
}
merge_tstack_tops();
merge_tstack_tops();
cur_tstack().v_start=cur;
assert(cur_tstack().top_depth==lowval);
cur_tstack().spans=set_sides(!edge_dir,concat(cur_tstack().spans[0],cur_tstack().spans[1]),{});
if constexpr(with_planarity){
if(cur_tstack().planarity){
auto&sides=cur_tstack().planarity->sides;
auto&s0=sides[0];
auto&s1=sides[1];
quarter_edge_matches[s0.bot_ends[0]]=s1.bot_ends[0];
quarter_edge_matches[s1.bot_ends[0]]=s0.bot_ends[0];
s0.bot_ends[0]=s1.bot_ends[1];
if(s1.tops[0].end!=-1){
assert(s1.tops[1].depth==lowval);
assert(s1.tops[0].depth==lowval);
quarter_edge_matches[s0.tops[0].end]=s1.tops[0].end;
quarter_edge_matches[s1.tops[0].end]=s0.tops[0].end;
s0.tops[0].end=s1.tops[1].end;
assert(s0.tops[0].depth==lowval);
}
s1=tstack_planarity_side_t{};
}
}
if(is_type_1){
finish_tstack_top(item,false);
}
}
}else{
assert(is_type_1);
push_edge_tstack(cur,lowval,e,false);
setmin(first_occurrence[lowval],nxt_edge_idx++);
}
assert(int(tstack.size())>=orig_tstack+1);
if(is_type_1&&nxt_tstack().top_depth==lowval){
int item=maybe_unwrap_nxt(node_type::P,false);
merge_tstack_tops();
finish_tstack_top(item,false);
}
if(!s.has_vert_tstack){
push_vert_tstack(cur,cur_depth);
s.has_vert_tstack=true;
assert(!is_type_1);
}
};
auto pop_vert=[&][[gnu::always_inline]]()->void{
auto&s=stk.back();
assert(s.ch_idx==s.ch_end);
assert(s.has_vert_tstack);
stk.pop_back();
};
stack_dir[0]=true;
push_vert(rt);
while(true){
if(stk.back().ch_idx==stk.back().ch_end){
pop_vert();
if(stk.empty())break;
finish_edge();
}else if(std::optional<int>nxt=start_edge();nxt){
push_vert(*nxt);
}else{
finish_edge();
}
}
item_ch[ROOT_ITEM]=concat(item_ch[ROOT_ITEM],tstack.back().spans[1]);tstack.pop_back();
}
}
int tot_items=int(item_types.size());
{
std::vector<int>vert_index(NV,-1);
std::vector<int>edge_index(NE,-1);
std::vector<bool>edge_flipped(NE);
std::vector<int>par(tot_items,-1);
std::vector<int>subtree_end(tot_items,-1);
std::vector<node_type>types(tot_items,node_type::F);
std::vector<int>orig_id(tot_items,-1);
csr<int>ch;
ch.bounds.resize(tot_items+1,0);
ch.dat.resize(tot_items-1);
int tot_node_verts=NV+(tot_items-1-NV)*2-tot_blocks-tot_self_loops;
std::vector<node_vert_t>node_verts(tot_node_verts);
csr_index node_nvs;node_nvs.bounds.resize(tot_items+1);
std::vector<int>vert_par_nv(tot_items,-1);
int tot_node_edges=(tot_items-1-NV-tot_blocks)*2;
std::vector<node_edge_t>node_edges(tot_node_edges);
csr_index node_nes;node_nes.bounds.resize(tot_items+1);
csr<node_adj_t>node_adj;
node_adj.bounds.resize(tot_node_verts*2+1);
node_adj.dat.resize(tot_node_edges*2);
std::vector<bool>node_planar(with_planarity?tot_items:0);
std::vector<int>ne_rot_adj(with_planarity?4*tot_node_edges:0,-1);
std::vector<int>vert_pos_buf(NV,-1);
std::vector<int>cnts_buf(2*NV,-1);
struct ch_buf_t{
int loc;
int item_id;
};
std::vector<ch_buf_t>ch_buf(tot_items);
std::vector<int>rot_edge_ne(with_planarity?2*NE+1:0);
int nxt_unassigned_idx=0;
struct dfs_stack_t{
int cur_idx;
int ch_idx;
int ch_end;
int cur_nv;
int cur_ne;
};
std::vector<dfs_stack_t>stk;stk.reserve(tot_items);
auto push_item=[&][[gnu::always_inline]](int cur_item)->void{
int cur_idx=nxt_unassigned_idx++;
node_type cur_type=types[cur_idx]=item_types[cur_item];
bool planar=true;
if(cur_type==node_type::F){
assert(cur_item==0);
}else if(cur_type==node_type::V){
assert(1<=cur_item&&cur_item<1+NV);
int orig_vert=cur_item-1;
orig_id[cur_idx]=orig_vert;
vert_index[orig_vert]=cur_idx;
}else if(cur_type==node_type::Q){
assert(1+NV<=cur_item&&cur_item<1+NV+NE);
int orig_edge=cur_item-1-NV;
orig_id[cur_idx]=orig_edge;
edge_index[orig_edge]=cur_idx;
assert(item_vs[cur_item][0]!=-1);
edge_flipped[orig_edge]=item_vs[cur_item][0]!=edges[orig_edge][0];
}else{
assert(1+NV+NE<=cur_item);
if constexpr(with_planarity){
if(cur_type==node_type::O||cur_type==node_type::I){
}else if(cur_type==node_type::S||cur_type==node_type::P||cur_type==node_type::R){
const auto&p=node_planarity[cur_item-(1+NV+NE)];
if(p){
for(int s=0;s<4;s++){
int a=8*NE+s,b=(*p)[s];
quarter_edge_matches[a]=b;
quarter_edge_matches[b]=a;
}
}else{
planar=false;
}
}else assert(false);
}
}
if constexpr(with_planarity)node_planar[cur_idx]=planar;
int ch_st=ch.bounds[cur_idx];
int ch_en=ch_st;
int nv_st=node_nvs.bounds[cur_idx];
int nv_en=nv_st;
int n_edges=0;
if(item_vs[cur_item][0]!=-1){
node_verts[nv_en++]={cur_idx,item_vs[cur_item][0]};
}
if(!item_ch[cur_item].empty()){
bool planarity_flip=item_ch[cur_item].v[0]&1;
for(int ch_item=item_ch[cur_item].v[0]>>1;true;planarity_flip^=(ch_nxt[ch_item]&1),ch_item=ch_nxt[ch_item]>>1){
ch.dat[ch_en++]=ch_item;
assert(ch_item>=1);
if(ch_item<1+NV){
node_verts[nv_en++]={cur_idx,ch_item-1};
}else{
if constexpr(with_planarity){
if(cur_type!=node_type::R){
assert(!planarity_flip);
}else{
int ve=ch_item-(1+NV);
if(planarity_flip){
std::swap(quarter_edge_matches[4*ve+0],quarter_edge_matches[4*ve+1]);
std::swap(quarter_edge_matches[4*ve+2],quarter_edge_matches[4*ve+3]);
}
}
}
n_edges++;
}
if(ch_item==(item_ch[cur_item].v[1]>>1)){
assert(ch_nxt[ch_item]==-1);
break;
}
}
planarity_flip^=item_ch[cur_item].v[1]&1;
assert(!planarity_flip);
}
if(item_vs[cur_item][1]!=-1){
node_verts[nv_en++]={cur_idx,item_vs[cur_item][1]};
}
ch.bounds[cur_idx+1]=ch_en;
node_nvs.bounds[cur_idx+1]=nv_en;
int n_verts=nv_en-nv_st;
bool is_node=cur_type!=node_type::F&&cur_type!=node_type::V;
bool has_cap=is_node&&!(cur_type==node_type::Q&&ch_en-ch_st>0);
if(!is_node)n_edges=0;
if(has_cap)n_edges++;
int ne_st=node_nes.bounds[cur_idx];
int ne_en=node_nes.bounds[cur_idx+1]=ne_st+n_edges;
auto set_ne=[&][[gnu::always_inline]](int ne,std::array<int,2>nvs,std::array<int,2>nds,std::array<int,4>rot_adjs)->void{
node_edges[ne].node=cur_idx;
node_edges[ne].nvs=nvs;
node_adj.dat[nds[0]]={ne,nvs[1]};
node_adj.dat[nds[1]]={ne,nvs[0]};
if constexpr(with_planarity){
for(int z=0;z<4;z++)ne_rot_adj[4*ne+z]=rot_adjs[z];
}
};
if(cur_type==node_type::F){
for(int i=2*nv_st+1;i<=2*nv_en;i++){
node_adj.bounds[i]=2*ne_st;
}
}else if(cur_type==node_type::V){
}else if(n_verts==1){
assert(cur_type==node_type::Q||cur_type==node_type::O);
assert(n_edges==1);
node_adj.bounds[2*nv_st+1]=2*ne_st+1*n_edges;
node_adj.bounds[2*nv_st+2]=2*ne_st+2*n_edges;
set_ne(ne_st,{nv_st,nv_st},{2*ne_st+1,2*ne_st},{4*ne_st+3,4*ne_st+2,4*ne_st+1,4*ne_st+0});
}else if(cur_type==node_type::Q||cur_type==node_type::I){
assert(n_verts==2);
assert(n_edges==1);
node_adj.bounds[2*nv_st+1]=2*ne_st+0*n_edges;
node_adj.bounds[2*nv_st+2]=2*ne_st+1*n_edges;
node_adj.bounds[2*nv_st+3]=2*ne_st+2*n_edges;
node_adj.bounds[2*nv_st+4]=2*ne_st+2*n_edges;
set_ne(ne_st,{nv_st,nv_st+1},{2*ne_st,2*ne_st+1},{4*ne_st+1,4*ne_st+0,4*ne_st+3,4*ne_st+2});
}else if(cur_type==node_type::P){
assert(n_verts==2);
assert(n_edges>=3);
node_adj.bounds[2*nv_st+1]=2*ne_st+0*n_edges;
node_adj.bounds[2*nv_st+2]=2*ne_st+1*n_edges;
node_adj.bounds[2*nv_st+3]=2*ne_st+2*n_edges;
node_adj.bounds[2*nv_st+4]=2*ne_st+2*n_edges;
for(int ne=ne_st;ne<ne_en;ne++){
int ne_prv=(ne==ne_st?ne_en:ne)-1;
int ne_nxt=(ne+1==ne_en?ne_st:ne+1);
std::array<int,4>rot_adjs{4*ne_prv+1,4*ne_nxt+0,4*ne_nxt+3,4*ne_prv+2};
set_ne(ne,{nv_st,nv_st+1},{2*ne_st+(ne-ne_st),2*ne_en-1-(ne-ne_st)},rot_adjs);
}
}else if(cur_type==node_type::S){
assert(n_verts==n_edges);
assert(n_verts>=3);
for(int i=2*nv_st+1;i<=2*nv_en;i++){
node_adj.bounds[i]=i+2*(ne_st-nv_st);
}
node_adj.bounds[2*nv_st+1]--;
node_adj.bounds[2*nv_en-1]++;
set_ne(ne_st,{nv_st,nv_en-1},{2*ne_st,2*ne_en-1},{4*(ne_st+1)+1,4*(ne_st+1)+0,4*(ne_en-1)+3,4*(ne_en-1)+2});
for(int i=1;i<n_edges;i++){
int ne=ne_st+i;
std::array<int,4>rot_adjs{4*(ne-1)+3,4*(ne-1)+2,4*(ne+1)+1,4*(ne+1)+0};
if(ne-1==ne_st){rot_adjs[0]=4*ne_st+1,rot_adjs[1]=4*ne_st+0;}
if(ne+1==ne_en){rot_adjs[2]=4*ne_st+3,rot_adjs[3]=4*ne_st+2;}
set_ne(ne,{nv_st+i-1,nv_st+i},{2*ne-1,2*ne},rot_adjs);
}
}else if(cur_type==node_type::R){
for(int nv=nv_st;nv<nv_en;nv++){
vert_pos_buf[node_verts[nv].vert]=nv;
}
cnts_buf.assign(n_verts*2-1,0);
ch_buf.clear();
assert(has_cap);
node_adj.bounds[2*nv_st+2]++;
node_adj.bounds[2*nv_en-1]++;
for(int i=ch_st;i<ch_en;i++){
int item=ch.dat[i];
assert(item>=1);
std::array<int,2>nvs;
if(item<1+NV){
nvs={vert_pos_buf[item-1],vert_pos_buf[item-1]};
}else{
nvs={vert_pos_buf[item_vs[item][0]],vert_pos_buf[item_vs[item][1]]};
assert(nvs[0]<nvs[1]);
node_adj.bounds[2*nvs[0]+2]++;
node_adj.bounds[2*nvs[1]+1]++;
}
int loc=(nvs[0]-nv_st)+(nvs[1]-nv_st);
ch_buf.emplace_back(loc,item);
cnts_buf[loc]++;
}
int offset=ch_st;
for(auto&cnt:cnts_buf){
offset+=cnt;
cnt=offset;
}
for(auto[loc,n]:std::views::reverse(ch_buf)){
ch.dat[--cnts_buf[loc]]=n;
}
if constexpr(with_planarity){
int nxt_ne=ne_en;
for(int i=ch_en-1;i>=ch_st;i--){
int item=ch.dat[i];
assert(item>=1);
if(item<1+NV)continue;
nxt_ne--;
rot_edge_ne[item-(1+NV)]=nxt_ne;
}
assert(nxt_ne==ne_st+1);
rot_edge_ne[2*NE]=ne_st;
}
auto map_rot_edge=[&][[gnu::always_inline]](int ve)->std::array<int,4>{
if constexpr(!with_planarity)return{-1,-1,-1,-1};
if(!planar)return{-1,-1,-1,-1};
std::array<int,4>res{};
for(int z=0;z<4;z++){
int o=quarter_edge_matches[4*ve+z];
assert(o!=-1);
res[z]=(rot_edge_ne[o>>2]<<2)+(o&2)+!(z&1);
}
return res;
};
{
int off=2*ne_st;
for(int i=2*nv_st+1;i<=2*nv_en;i++){
off+=std::exchange(node_adj.bounds[i],off);
}
assert(off==2*ne_en);
}
{
node_adj.bounds[2*nv_st+2]++;
int nxt_ne=ne_en;
for(int i=ch_en-1;i>=ch_st;i--){
int item=ch.dat[i];
assert(item>=1);
if(item<1+NV)continue;
nxt_ne--;
auto[v0,v1]=item_vs[item];
std::array<int,2>nvs={vert_pos_buf[v0],vert_pos_buf[v1]};
set_ne(nxt_ne,nvs,{
node_adj.bounds[2*nvs[0]+2]++,
node_adj.bounds[2*nvs[1]+1]++,
},map_rot_edge(item-(1+NV)));
}
assert(nxt_ne==ne_st+1);
set_ne(ne_st,{nv_st,nv_en-1},{2*ne_st,2*ne_en-1},map_rot_edge(2*NE));
node_adj.bounds[2*nv_en-1]++;
}
}else assert(false);
int cur_nv=nv_st+(item_vs[cur_item][0]!=-1);
int cur_ne=ne_st+has_cap;
stk.push_back({cur_idx,ch_st,ch_en,cur_nv,cur_ne});
};
auto start_child=[&][[gnu::always_inline]]()->int{
auto&[cur_idx,ch_idx,ch_en,cur_nv,cur_ne]=stk.back();
assert(ch_idx<ch_en);
int nxt_item=ch.dat[ch_idx];
int nxt_idx=nxt_unassigned_idx;
ch.dat[ch_idx]=nxt_idx;
par[nxt_idx]=cur_idx;
int nxt_ne=node_nes.bounds[nxt_idx];
if(nxt_item<1+NV){
vert_par_nv[nxt_idx]=cur_nv++;
}else if(types[cur_idx]!=node_type::F&&types[cur_idx]!=node_type::V){
node_edges[cur_ne].twin_ne=nxt_ne;
node_edges[nxt_ne].twin_ne=cur_ne;
cur_ne++;
}
ch_idx++;
return nxt_item;
};
auto pop_item=[&][[gnu::always_inline]]()->void{
auto[cur_idx,ch_idx,ch_en,cur_nv,cur_ne]=stk.back();stk.pop_back();
assert(ch_idx==ch_en);
subtree_end[cur_idx]=nxt_unassigned_idx;
};
par[nxt_unassigned_idx]=-1;
push_item(ROOT_ITEM);
while(true){
if(stk.back().ch_idx==stk.back().ch_end){
pop_item();
if(stk.empty())break;
}else{
push_item(start_child());
}
}
assert(nxt_unassigned_idx==tot_items);
assert(ch.bounds.back()==int(ch.dat.size()));
assert(node_nvs.bounds.back()==int(node_verts.size()));
assert(node_nes.bounds.back()==int(node_edges.size()));
assert(node_adj.bounds.back()==int(node_adj.dat.size()));
for(auto&v:node_verts){
v.vert=vert_index[v.vert];
}
spqr_tree res{
std::move(vert_index),
std::move(edge_index),
std::move(edge_flipped),
std::move(par),
std::move(subtree_end),
std::move(types),
std::move(orig_id),
std::move(ch),
std::move(node_verts),
std::move(node_nvs),
std::move(vert_par_nv),
std::move(node_edges),
std::move(node_nes),
std::move(node_adj),
};
if constexpr(with_planarity){
return planar_spqr_tree{std::move(res),std::move(node_planar),{std::move(ne_rot_adj)}};
}else{
return res;
}
}
}
inline std::optional<planar_embedding>planar_embed(const planar_spqr_tree&tree){
using node_type=planar_spqr_tree::node_type;
if(!std::ranges::all_of(tree.node_planar,std::identity{})){
return std::nullopt;
}
int NE=int(tree.edge_index.size());
std::vector<int>rot_adj(4*NE,-1);
auto link=[&][[gnu::always_inline]](int a,int b)->void{
assert(a!=-1&&b!=-1);
assert(rot_adj[a]==-1&&rot_adj[b]==-1);
assert((a&1)!=(b&1));
rot_adj[a]=b;
rot_adj[b]=a;
};
std::vector<std::array<std::array<int,2>,2>>outer_e(tree.size(),{{{-1,-1},{-1,-1}}});
for(int i=tree.size()-1;i>=0;i--){
auto type=tree.types[i];
if(type==node_type::F){
for(int j:tree.ch[i]){
assert(tree.types[j]==node_type::V);
auto[a,b]=outer_e[j][0];
if(a!=-1){
link(a,b);
}
}
}else if(type==node_type::V){
std::array<int,2>qes{-1,-1};
for(int j:tree.ch[i]){
assert(tree.types[j]==node_type::Q);
if(qes[0]==-1){
qes=outer_e[j][0];
}else{
link(qes[1],outer_e[j][0][0]);
qes[1]=outer_e[j][0][1];
}
}
outer_e[i][0]=qes;
}else if(type==node_type::Q){
int e=tree.orig_id[i];
bool flip=tree.edge_flipped[e];
std::array<std::array<int,2>,2>qes={{{4*e+2*flip+0,4*e+2*flip+1},{4*e+2*!flip+0,4*e+2*!flip+1}}};
if(tree.ch[i].empty()){
outer_e[i]=qes;
}else{
int j=tree.ch[i][0];
if(tree.types[j]==node_type::O){
link(qes[0][1],qes[1][0]);
outer_e[i][0]={qes[0][0],qes[1][1]};
}else{
if(tree.types[j]!=node_type::I){
link(qes[0][1],outer_e[j][0][0]);
qes[0][1]=outer_e[j][0][1];
link(qes[1][0],outer_e[j][1][1]);
qes[1][0]=outer_e[j][1][0];
}
{
int k=tree.ch[i][1];
if(outer_e[k][0][0]!=-1){
link(qes[1][1],outer_e[k][0][0]);
link(qes[1][0],outer_e[k][0][1]);
}else{
link(qes[1][1],qes[1][0]);
}
}
outer_e[i][0]=qes[0];
}
}
}else if(type==node_type::O||type==node_type::I){
}else if(type==node_type::P||type==node_type::S||type==node_type::R){
for(int ta=4*tree.node_nes.bounds[i];ta<4*tree.node_nes.bounds[i+1];ta++){
int tb=tree.ne_embedding.rot_adj[ta];
if(tb<ta)continue;
auto tree_qe_to_qe=[&][[gnu::always_inline]](int t)->int{
return outer_e[tree.node_edges[tree.node_edges[t>>2].twin_ne].node][(t>>1)&1][t&1];
};
int qb=tree_qe_to_qe(tb);
if(ta<4*(tree.node_nes.bounds[i]+1)){
outer_e[i][(ta>>1)&1][!(ta&1)]=qb;
}else{
int qa=tree_qe_to_qe(ta);
if((ta&3)==2&&(tb&3)==1){
int v=tree.node_verts[tree.node_edges[ta>>2].nvs[1]].vert;
if(outer_e[v][0][0]!=-1){
link(qa,outer_e[v][0][1]);
link(qb,outer_e[v][0][0]);
}else{
link(qa,qb);
}
}else{
link(qa,qb);
}
}
}
}else assert(false);
}
return planar_embedding{std::move(rot_adj)};
}
inline std::optional<planar_embedding>planar_embed(
int NV,
const std::vector<std::array<int,2>>&edges,
std::span<const int>vert_order,
std::span<const int>edge_order
){
auto setmin=[](auto&a,auto b){if(b<a)a=b;};
int NE=int(edges.size());
auto[roots,outedges]=lowval_storted_skeleton_t::build(NV,edges,vert_order,edge_order);
std::vector<int>quarter_edge_matches(4*NE,-1);
{
int nxt_edge_idx=0;
std::vector<int>postorder_edges;postorder_edges.reserve(NE);
std::vector<bool>postorder_flip(NE+1,false);
std::vector<int>first_occurrence(NV);
std::vector<int>edge_top_depths(NE,-1);
struct tstack_planarity_side_t{
std::array<int,2>bot_ends{-1,-1};
struct top_t{
int end=-1;
int depth=-1;
};
std::array<top_t,2>tops{top_t{-1,-1},top_t{-1,-1}};
};
struct tstack_planarity_t{
std::array<tstack_planarity_side_t,2>sides;
};
struct tstack_nonplanarity_t{
};
auto merge_planarity=[&][[gnu::always_inline]](tstack_planarity_t&a,const tstack_planarity_t&b)->void{
for(int z=0;z<2;z++){
auto&as=a.sides[z];
const auto&bs=b.sides[z];
if(bs.bot_ends[0]==-1){
}else if(as.bot_ends[0]==-1){
as=bs;
}else{
quarter_edge_matches[as.bot_ends[1]]=bs.bot_ends[0];
quarter_edge_matches[bs.bot_ends[0]]=as.bot_ends[1];
as.bot_ends[1]=bs.bot_ends[1];
if(bs.tops[0].end==-1){
}else if(as.tops[0].end==-1){
as.tops=bs.tops;
}else{
assert(as.tops[1].depth<=bs.tops[0].depth);
quarter_edge_matches[as.tops[1].end]=bs.tops[0].end;
quarter_edge_matches[bs.tops[0].end]=as.tops[1].end;
as.tops[1]=bs.tops[1];
}
}
}
};
auto make_edge_planarity=[&][[gnu::always_inline]](int e_side,int top_depth,bool is_tree)->tstack_planarity_t{
edge_top_depths[e_side>>1]=top_depth;
tstack_planarity_t p;
if(is_tree){
p.sides[0].bot_ends={2*(e_side^1)+0,2*e_side+1};
p.sides[1].bot_ends={2*(e_side^1)+1,2*e_side+0};
}else{
p.sides[0].bot_ends={2*e_side+0,2*e_side+1};
p.sides[0].tops={{{2*(e_side^1)+1,top_depth},{2*(e_side^1)+0,top_depth}}};
}
return p;
};
struct tstack_t{
int top_depth=-1;
int first_idx=-1;
tstack_planarity_t planarity;
};
std::vector<tstack_t>tstack;tstack.reserve(NV+NE);
auto cur_tstack=[&][[gnu::always_inline]]()->tstack_t&{return tstack.end()[-1];};
auto nxt_tstack=[&][[gnu::always_inline]]()->tstack_t&{return tstack.end()[-2];};
auto push_tstack=[&][[gnu::always_inline]](int top_depth,tstack_planarity_t planarity)->void{
tstack.emplace_back(top_depth,nxt_edge_idx,planarity);
};
auto push_vert_tstack=[&][[gnu::always_inline]](int top_depth)->void{
push_tstack(top_depth,{});
};
auto push_edge_tstack=[&][[gnu::always_inline]](int top_depth,int e_side,bool is_tree)->int{
push_tstack(top_depth,make_edge_planarity(e_side,top_depth,is_tree));
postorder_edges.push_back(e_side>>1);
return nxt_edge_idx++;
};
auto flip_tstack_planarity=[&][[gnu::always_inline]](int i)->void{
tstack_t&a=tstack[i];
postorder_flip[a.first_idx].flip();
postorder_flip[(i+1==int(tstack.size()))?nxt_edge_idx:tstack[i+1].first_idx].flip();
std::swap(a.planarity.sides[0],a.planarity.sides[1]);
};
auto merge_tstack_tops=[&][[gnu::always_inline]]()->void{
tstack_t&a=nxt_tstack();
const tstack_t&b=cur_tstack();
setmin(a.top_depth,b.top_depth);
merge_planarity(a.planarity,b.planarity);
tstack.pop_back();
};
struct dfs_stack_t{
bool has_vert_tstack;
int ch_idx;
int ch_end;
int orig_tstack;
};
std::vector<dfs_stack_t>stk;stk.reserve(NV);
for(auto rt:roots){
auto push_vert=[&][[gnu::always_inline]](int cur)->void{
int cur_depth=int(stk.size());
int lo=outedges.bounds[cur];
int hi=outedges.bounds[cur+1];
bool has_vert_tstack;
{
int first_edge=lo;
while(first_edge<hi&&outedges.dat[first_edge].key.is_new_block())first_edge++;
if(first_edge<hi&&outedges.dat[first_edge].key.is_type_2()){
auto e=outedges.dat[first_edge];
std::move_backward(outedges.dat.begin()+lo,outedges.dat.begin()+first_edge,outedges.dat.begin()+first_edge+1);
outedges.dat[lo]=e;
has_vert_tstack=false;
}else{
push_vert_tstack(cur_depth);
has_vert_tstack=true;
}
}
stk.push_back({has_vert_tstack,lo,hi,-1});
};
auto start_edge=[&][[gnu::always_inline]]()->std::optional<int>{
int cur_depth=int(stk.size())-1;
auto&s=stk.back();
assert(s.ch_idx<s.ch_end);
auto[_,nxt,e_side,key]=outedges.dat[s.ch_idx];
auto[lowval,is_tree,is_type_1]=key.unpack(cur_depth);
if(lowval>=cur_depth||is_type_1)assert(s.has_vert_tstack);
s.orig_tstack=int(tstack.size());
if(is_tree){
first_occurrence[cur_depth]=NE;
return nxt;
}else{
return std::nullopt;
}
};
auto finish_edge=[&][[gnu::always_inline]][[nodiscard]]()->std::optional<tstack_nonplanarity_t>{
int cur_depth=int(stk.size())-1;
auto&s=stk.back();
assert(s.ch_idx<s.ch_end);
auto[_,nxt,e_side,key]=outedges.dat[s.ch_idx];
s.ch_idx++;
auto[lowval,is_tree,is_type_1]=key.unpack(cur_depth);
const int orig_tstack=s.orig_tstack;
auto join_backedges_to_top=[&][[gnu::always_inline]]()->void{
for(auto&side:cur_tstack().planarity.sides){
if(side.tops[1].end==-1)continue;
assert(side.bot_ends[1]!=-1);
assert(side.tops[0].depth==cur_depth);
assert(side.tops[1].depth==cur_depth);
quarter_edge_matches[side.bot_ends[1]]=side.tops[1].end;
quarter_edge_matches[side.tops[1].end]=side.bot_ends[1];
side.bot_ends[1]=side.tops[0].end;
side.tops={};
}
};
auto join_bottoms_to_empty=[&][[gnu::always_inline]]()->void{
auto&sides=cur_tstack().planarity.sides;
auto&s0=sides[0];
auto&s1=sides[1];
quarter_edge_matches[s0.bot_ends[0]]=s1.bot_ends[0];
quarter_edge_matches[s1.bot_ends[0]]=s0.bot_ends[0];
s0.bot_ends[0]=s1.bot_ends[1];
if(s1.tops[0].end!=-1){
assert(s1.tops[1].depth==lowval);
assert(s1.tops[0].depth==lowval);
quarter_edge_matches[s0.tops[0].end]=s1.tops[0].end;
quarter_edge_matches[s1.tops[0].end]=s0.tops[0].end;
s0.tops[0].end=s1.tops[1].end;
assert(s0.tops[0].depth==lowval);
}
s1=tstack_planarity_side_t{};
};
if(lowval>=cur_depth){
if(is_tree){
push_edge_tstack(cur_depth,e_side,true);
if(lowval==cur_depth){
merge_tstack_tops();
join_backedges_to_top();
}
merge_tstack_tops();
join_bottoms_to_empty();
}else{
push_edge_tstack(lowval,e_side,false);
join_backedges_to_top();
}
assert(s.has_vert_tstack);
merge_tstack_tops();
return std::nullopt;
}
if(is_tree){
push_edge_tstack(cur_depth,e_side,true);
while(nxt_tstack().top_depth>=cur_depth){
if(nxt_tstack().top_depth>cur_depth){
if(tstack.end()[-3].top_depth<cur_depth){
break;
}
merge_tstack_tops();
}
merge_tstack_tops();
join_backedges_to_top();
}
if(cur_tstack().first_idx>first_occurrence[cur_depth]){
int source=int(tstack.size())-2;
while(tstack[source].first_idx>first_occurrence[cur_depth])--source;
int last_top=cur_depth;
while(int(tstack.size())>source+2){
if(nxt_tstack().top_depth>cur_depth){
}else if(nxt_tstack().top_depth==cur_depth){
if(nxt_tstack().planarity.sides[1].tops[0].depth!=-1){
assert(last_top<cur_depth);
return tstack_nonplanarity_t{};
}
flip_tstack_planarity(int(tstack.size())-2);
}else{
if(nxt_tstack().planarity.sides[1].tops[0].depth!=-1&&nxt_tstack().planarity.sides[1].tops[0].depth!=cur_depth){
return tstack_nonplanarity_t{};
}
if(nxt_tstack().planarity.sides[0].tops[1].depth>last_top){
return tstack_nonplanarity_t{};
}
last_top=nxt_tstack().top_depth;
}
merge_tstack_tops();
}
int t0=nxt_tstack().planarity.sides[0].tops[1].depth;
int t1=nxt_tstack().planarity.sides[1].tops[1].depth;
assert(t0==cur_depth||t1==cur_depth);
if(std::min(t0,t1)>last_top){
assert(last_top<cur_depth);
return tstack_nonplanarity_t{};
}
if(t0==cur_depth){
flip_tstack_planarity(cur_tstack().top_depth<nxt_tstack().top_depth?int(tstack.size())-2:int(tstack.size())-1);
}
merge_tstack_tops();
for(auto&side:cur_tstack().planarity.sides){
assert(side.bot_ends[1]!=-1);
while(side.tops[1].depth==cur_depth){
{
quarter_edge_matches[side.bot_ends[1]]=side.tops[1].end;
quarter_edge_matches[side.tops[1].end]=side.bot_ends[1];
side.bot_ends[1]=side.tops[1].end^1;
}
side.tops[1].end=std::exchange(quarter_edge_matches[side.bot_ends[1]],-1);
if(side.tops[1].end!=-1){
quarter_edge_matches[side.tops[1].end]=-1;
side.tops[1].depth=edge_top_depths[side.tops[1].end>>2];
}else{
side.tops={};
}
}
}
}
if(is_type_1)assert(s.has_vert_tstack);
if(s.has_vert_tstack){
assert(int(tstack.size())>=orig_tstack+3);
if(!is_type_1){
auto&t=tstack[orig_tstack+2];
{
assert(t.planarity.sides[0].tops[0].depth==t.top_depth);
assert(t.planarity.sides[0].tops[1].depth!=-1);
if(t.planarity.sides[0].tops[1].depth==lowval){
flip_tstack_planarity(orig_tstack+2);
}else if(t.planarity.sides[1].tops[1].depth!=-1&&t.planarity.sides[1].tops[1].depth!=lowval){
return tstack_nonplanarity_t{};
}
assert(t.planarity.sides[0].tops[1].depth>lowval);
}
int last_top=t.planarity.sides[0].tops[1].depth;
for(int i=orig_tstack+3;i<int(tstack.size());i++){
if(tstack[i].top_depth==lowval){
flip_tstack_planarity(i);
}
if(tstack[i].planarity.sides[1].tops[1].depth!=-1&&tstack[i].planarity.sides[1].tops[1].depth!=lowval){
return tstack_nonplanarity_t{};
}
int next_top=tstack[i].planarity.sides[0].tops[0].depth;
if(next_top!=-1){
if(last_top>next_top)return tstack_nonplanarity_t{};
last_top=tstack[i].planarity.sides[0].tops[1].depth;
}
}
while(int(tstack.size())>orig_tstack+3){
merge_tstack_tops();
}
}
assert(int(tstack.size())==orig_tstack+3);
merge_tstack_tops();
merge_tstack_tops();
assert(cur_tstack().top_depth==lowval);
join_bottoms_to_empty();
}
}else{
assert(is_type_1);
int idx=push_edge_tstack(lowval,e_side,false);
setmin(first_occurrence[lowval],idx);
}
if(is_type_1&&nxt_tstack().top_depth==lowval){
assert(s.has_vert_tstack);
merge_tstack_tops();
}
if(!s.has_vert_tstack){
assert(!is_type_1);
push_vert_tstack(cur_depth);
s.has_vert_tstack=true;
}
return std::nullopt;
};
auto pop_vert=[&][[gnu::always_inline]]()->void{
auto&s=stk.back();
assert(s.ch_idx==s.ch_end);
assert(s.has_vert_tstack);
stk.pop_back();
};
push_vert(rt);
while(true){
if(stk.back().ch_idx==stk.back().ch_end){
pop_vert();
if(stk.empty())break;
if(auto res=finish_edge();res)return std::nullopt;
}else if(std::optional<int>nxt=start_edge();nxt){
push_vert(*nxt);
}else{
if(auto res=finish_edge();res)assert(false);
}
}
assert(int(tstack.size())==1);
auto s0=tstack.back().planarity.sides[0];
int a=s0.bot_ends[0];
int b=s0.bot_ends[1];
if(a!=-1){
quarter_edge_matches[a]=b;
quarter_edge_matches[b]=a;
}
tstack.pop_back();
}
assert(nxt_edge_idx==NE);
{
std::vector<bool>edge_flip(NE);
{
bool planarity_flip=false;
for(int e=0;e<NE;e++){
planarity_flip^=postorder_flip[e];
edge_flip[postorder_edges[e]]=planarity_flip;
}
planarity_flip^=postorder_flip[NE];
assert(!planarity_flip);
}
for(int e=0;e<NE;e++){
if(edge_flip[e]){
std::swap(quarter_edge_matches[4*e+0],quarter_edge_matches[4*e+1]);
std::swap(quarter_edge_matches[4*e+2],quarter_edge_matches[4*e+3]);
}
for(int z=0;z<4;z++){
quarter_edge_matches[4*e+z]=(quarter_edge_matches[4*e+z]>>1<<1)|!(z&1);
}
}
}
}
return planar_embedding{std::move(quarter_edge_matches)};
}
inline bool can_planar_embed(
int NV,
const std::vector<std::array<int,2>>&edges,
std::span<const int>vert_order,
std::span<const int>edge_order
){
auto setmin=[](auto&a,auto b){if(b<a)a=b;};
int NE=int(edges.size());
auto[roots,outedges]=lowval_storted_skeleton_t::build(NV,edges,vert_order,edge_order);
{
int nxt_edge_idx=0;
std::vector<int>first_occurrence(NV);
struct tstack_planarity_side_t{
struct top_t{
int end=-1;
int depth=-1;
};
std::array<top_t,2>tops{top_t{-1,-1},top_t{-1,-1}};
};
struct tstack_planarity_t{
std::array<tstack_planarity_side_t,2>sides;
};
std::vector<tstack_planarity_side_t::top_t>prev_edge(NE,{-1,-1});
auto merge_planarity_side=[&][[gnu::always_inline]](tstack_planarity_side_t&as,const tstack_planarity_side_t&bs)->void{
assert(as.tops[1].depth<=bs.tops[0].depth);
prev_edge[bs.tops[0].end]=as.tops[1];
as.tops[1]=bs.tops[1];
};
auto make_edge_planarity=[&][[gnu::always_inline]](int e,int top_depth,bool is_tree)->tstack_planarity_t{
tstack_planarity_t p;
if(!is_tree){
p.sides[0].tops={{{e,top_depth},{e,top_depth}}};
}
return p;
};
struct tstack_t{
int top_depth=-1;
int first_idx=-1;
tstack_planarity_t planarity;
};
std::vector<tstack_t>tstack;tstack.reserve(NV+NE);
auto cur_tstack=[&][[gnu::always_inline]]()->tstack_t&{return tstack.end()[-1];};
auto nxt_tstack=[&][[gnu::always_inline]]()->tstack_t&{return tstack.end()[-2];};
auto push_tstack=[&][[gnu::always_inline]](int top_depth,tstack_planarity_t planarity)->void{
tstack.emplace_back(top_depth,nxt_edge_idx,planarity);
};
auto push_vert_tstack=[&][[gnu::always_inline]](int top_depth)->void{
push_tstack(top_depth,{});
};
auto push_edge_tstack=[&][[gnu::always_inline]](int top_depth,int e,bool is_tree)->int{
push_tstack(top_depth,make_edge_planarity(e,top_depth,is_tree));
return nxt_edge_idx++;
};
struct dfs_stack_t{
bool has_vert_tstack;
int ch_idx;
int ch_end;
int orig_tstack;
};
std::vector<dfs_stack_t>stk;stk.reserve(NV);
for(auto rt:roots){
auto push_vert=[&][[gnu::always_inline]](int cur)->void{
int cur_depth=int(stk.size());
int lo=outedges.bounds[cur];
int hi=outedges.bounds[cur+1];
bool has_vert_tstack;
{
int first_edge=lo;
while(first_edge<hi&&outedges.dat[first_edge].key.is_new_block())first_edge++;
if(first_edge<hi&&outedges.dat[first_edge].key.is_type_2()){
auto e=outedges.dat[first_edge];
std::move_backward(outedges.dat.begin()+lo,outedges.dat.begin()+first_edge,outedges.dat.begin()+first_edge+1);
outedges.dat[lo]=e;
has_vert_tstack=false;
}else{
push_vert_tstack(cur_depth);
has_vert_tstack=true;
}
}
stk.push_back({has_vert_tstack,lo,hi,-1});
};
auto start_edge=[&][[gnu::always_inline]]()->std::optional<int>{
int cur_depth=int(stk.size())-1;
auto&s=stk.back();
assert(s.ch_idx<s.ch_end);
auto[_,nxt,e_side,key]=outedges.dat[s.ch_idx];
auto[lowval,is_tree,is_type_1]=key.unpack(cur_depth);
if(lowval>=cur_depth||is_type_1)assert(s.has_vert_tstack);
s.orig_tstack=int(tstack.size());
if(is_tree){
first_occurrence[cur_depth]=NE;
return nxt;
}else{
return std::nullopt;
}
};
auto finish_edge=[&][[gnu::always_inline]][[nodiscard]]()->bool{
int cur_depth=int(stk.size())-1;
auto&s=stk.back();
assert(s.ch_idx<s.ch_end);
auto[_,nxt,e_side,key]=outedges.dat[s.ch_idx];
int e=e_side>>1;
s.ch_idx++;
auto[lowval,is_tree,is_type_1]=key.unpack(cur_depth);
const int orig_tstack=s.orig_tstack;
if(lowval>=cur_depth){
if(is_tree){
if(lowval==cur_depth){
tstack.pop_back();
}
tstack.pop_back();
}else{
}
assert(s.has_vert_tstack);
assert(int(tstack.size())==orig_tstack);
return true;
}
if(is_tree){
push_edge_tstack(cur_depth,e,true);
while(nxt_tstack().top_depth>=cur_depth){
if(nxt_tstack().top_depth>cur_depth){
if(tstack.end()[-3].top_depth<cur_depth){
break;
}
tstack.pop_back();
}
tstack.pop_back();
}
cur_tstack().planarity=tstack_planarity_t{};
if(cur_tstack().first_idx>first_occurrence[cur_depth]){
int source=int(tstack.size())-2;
while(tstack[source].first_idx>first_occurrence[cur_depth])--source;
int last_top=cur_depth;
tstack_planarity_side_t cur_planarity={};
while(int(tstack.size())>source+2){
if(nxt_tstack().top_depth>cur_depth){
}else if(nxt_tstack().top_depth==cur_depth){
if(nxt_tstack().planarity.sides[1].tops[0].depth!=-1){
assert(last_top<cur_depth);
return false;
}
}else{
if(nxt_tstack().planarity.sides[1].tops[0].depth!=-1&&nxt_tstack().planarity.sides[1].tops[0].depth!=cur_depth){
return false;
}
if(nxt_tstack().planarity.sides[0].tops[1].depth>last_top){
return false;
}
if(last_top<cur_depth){
auto nxt_planarity=nxt_tstack().planarity.sides[0];
prev_edge[cur_planarity.tops[0].end]=nxt_planarity.tops[1];
cur_planarity.tops[0]=nxt_planarity.tops[0];
}else{
cur_planarity=nxt_tstack().planarity.sides[0];
}
last_top=nxt_tstack().top_depth;
}
tstack.pop_back();
}
int t0=nxt_tstack().planarity.sides[0].tops[1].depth;
int t1=nxt_tstack().planarity.sides[1].tops[1].depth;
assert(t0==cur_depth||t1==cur_depth);
if(std::min(t0,t1)>last_top){
assert(last_top<cur_depth);
return false;
}
if(last_top<cur_depth){
if(t0==cur_depth){
if(t1!=-1){
merge_planarity_side(nxt_tstack().planarity.sides[1],cur_planarity);
}else{
nxt_tstack().planarity.sides[1]=cur_planarity;
}
if(last_top<nxt_tstack().top_depth){
nxt_tstack().top_depth=last_top;
std::swap(nxt_tstack().planarity.sides[0],nxt_tstack().planarity.sides[1]);
}
}else{
assert(t0<cur_depth);
assert(t0<=last_top);
merge_planarity_side(nxt_tstack().planarity.sides[0],cur_planarity);
}
}
tstack.pop_back();
for(auto&side:cur_tstack().planarity.sides){
while(side.tops[1].depth==cur_depth){
side.tops[1]=prev_edge[side.tops[1].end];
if(side.tops[1].end==-1){
side.tops[0]={};
}
}
}
}
if(is_type_1)assert(s.has_vert_tstack);
if(s.has_vert_tstack){
assert(int(tstack.size())>=orig_tstack+3);
if(!is_type_1){
auto&t=tstack[orig_tstack+2];
{
assert(t.planarity.sides[0].tops[0].depth==t.top_depth);
assert(t.planarity.sides[0].tops[1].depth!=-1);
if(t.planarity.sides[0].tops[1].depth==lowval){
t.planarity.sides[0]=t.planarity.sides[1];
}else if(t.planarity.sides[1].tops[1].depth!=-1&&t.planarity.sides[1].tops[1].depth!=lowval){
return false;
}
assert(t.planarity.sides[0].tops[1].depth>lowval);
}
tstack_planarity_side_t cur_planarity=t.planarity.sides[0];
for(int i=orig_tstack+3;i<int(tstack.size());i++){
if(tstack[i].top_depth==lowval){
std::swap(tstack[i].planarity.sides[0],tstack[i].planarity.sides[1]);
}
if(tstack[i].planarity.sides[1].tops[1].depth!=-1&&tstack[i].planarity.sides[1].tops[1].depth!=lowval){
return false;
}
int next_top=tstack[i].planarity.sides[0].tops[0].depth;
if(next_top!=-1){
if(cur_planarity.tops[1].depth>next_top)return false;
merge_planarity_side(cur_planarity,tstack[i].planarity.sides[0]);
}
}
merge_planarity_side(tstack[orig_tstack+1].planarity.sides[0],cur_planarity);
tstack[orig_tstack+1].planarity.sides[1]=tstack_planarity_side_t{};
}else{
assert(int(tstack.size())==orig_tstack+3);
}
tstack[orig_tstack]=tstack[orig_tstack+1];
tstack.resize(orig_tstack+1);
assert(cur_tstack().top_depth==lowval);
}
}else{
assert(is_type_1);
int idx=push_edge_tstack(lowval,e,false);
setmin(first_occurrence[lowval],idx);
}
if(is_type_1&&nxt_tstack().top_depth==lowval){
assert(s.has_vert_tstack);
tstack.pop_back();
}
if(!s.has_vert_tstack){
assert(!is_type_1);
push_vert_tstack(cur_depth);
s.has_vert_tstack=true;
}
return true;
};
auto pop_vert=[&][[gnu::always_inline]]()->void{
auto&s=stk.back();
assert(s.ch_idx==s.ch_end);
assert(s.has_vert_tstack);
stk.pop_back();
};
push_vert(rt);
while(true){
if(stk.back().ch_idx==stk.back().ch_end){
pop_vert();
if(stk.empty())break;
if(auto res=finish_edge();!res)return false;
}else if(std::optional<int>nxt=start_edge();nxt){
push_vert(*nxt);
}else{
if(auto res=finish_edge();!res)assert(false);
}
}
assert(int(tstack.size())==1);
tstack.pop_back();
}
}
return true;
}
}
// verify/graph/two_edge_connected_components-spqr.test.cpp
int main(){
std::ios_base::sync_with_stdio(false),std::cin.tie(nullptr);
int N,M;std::cin>>N>>M;
std::vector<std::array<int,2>>edges(M);
for(auto&[x,y]:edges)std::cin>>x>>y;
auto spqr=wala::spqr_tree::build(N,edges);
using node_type=wala::spqr_tree::node_type;
std::vector<std::pair<int,int>>stk;stk.reserve(N);
std::vector<int>comp_verts(N,-1);
std::vector<int>comp_bounds;comp_bounds.reserve(N+1);comp_bounds.push_back(0);
for(int i=int(spqr.size())-1;i>=0;i--){
int comp_end=-1;
if(spqr.types[i]==node_type::V){
stk.push_back({i,spqr.orig_id[i]});
if(spqr.par[i]==0){
comp_end=spqr.subtree_end[i];
}
}else if(spqr.types[i]==node_type::I){
int p=spqr.par[i];
assert(p==i-1&&spqr.types[p]==node_type::Q);
comp_end=spqr.subtree_end[p];
}
if(comp_end!=-1){
int idx=comp_bounds.back();
while(!stk.empty()&&stk.back().first<comp_end){
comp_verts[idx++]=stk.back().second;
stk.pop_back();
}
comp_bounds.push_back(idx);
}
}
assert(stk.empty());
assert(comp_bounds.back()==N);
int K=int(comp_bounds.size())-1;
std::cout<<K<<'\n';
for(int i=0;i<K;i++){
std::cout<<comp_bounds[i+1]-comp_bounds[i];
for(int j=comp_bounds[i];j<comp_bounds[i+1];j++){
std::cout<<' '<<comp_verts[j];
}
std::cout<<'\n';
}
return 0;
}
#pragma GCC diagnostic pop
// clang-format on
// @formatter:on

Test cases

Env Name Status Elapsed Memory
g++-sanitizer example_00 :heavy_check_mark: AC 140 ms 13 MB
g++-sanitizer example_01 :heavy_check_mark: AC 23 ms 12 MB
g++-sanitizer example_02 :heavy_check_mark: AC 15 ms 11 MB
g++-sanitizer large_cycle_00 :heavy_check_mark: AC 248 ms 71 MB
g++-sanitizer max_random_00 :heavy_check_mark: AC 563 ms 110 MB
g++-sanitizer max_random_01 :heavy_check_mark: AC 484 ms 111 MB
g++-sanitizer max_random_02 :heavy_check_mark: AC 538 ms 110 MB
g++-sanitizer random_1_00 :heavy_check_mark: AC 340 ms 81 MB
g++-sanitizer random_1_01 :heavy_check_mark: AC 379 ms 86 MB
g++-sanitizer random_1_02 :heavy_check_mark: AC 213 ms 56 MB
g++-sanitizer random_2_00 :heavy_check_mark: AC 104 ms 36 MB
g++-sanitizer random_2_01 :heavy_check_mark: AC 40 ms 18 MB
g++-sanitizer random_2_02 :heavy_check_mark: AC 174 ms 46 MB
g++-sanitizer random_2_03 :heavy_check_mark: AC 219 ms 58 MB
g++-sanitizer random_2_04 :heavy_check_mark: AC 88 ms 32 MB
g++-sanitizer small_random_1_00 :heavy_check_mark: AC 17 ms 12 MB
g++-sanitizer small_random_1_01 :heavy_check_mark: AC 21 ms 12 MB
g++-sanitizer small_random_1_02 :heavy_check_mark: AC 20 ms 12 MB
g++-sanitizer small_random_2_00 :heavy_check_mark: AC 19 ms 12 MB
g++-sanitizer small_random_2_01 :heavy_check_mark: AC 18 ms 12 MB
g++-sanitizer small_random_2_02 :heavy_check_mark: AC 23 ms 12 MB
g++ example_00 :heavy_check_mark: AC 3 ms 4 MB
g++ example_01 :heavy_check_mark: AC 2 ms 4 MB
g++ example_02 :heavy_check_mark: AC 2 ms 4 MB
g++ large_cycle_00 :heavy_check_mark: AC 67 ms 36 MB
g++ max_random_00 :heavy_check_mark: AC 146 ms 69 MB
g++ max_random_01 :heavy_check_mark: AC 157 ms 69 MB
g++ max_random_02 :heavy_check_mark: AC 143 ms 69 MB
g++ random_1_00 :heavy_check_mark: AC 105 ms 49 MB
g++ random_1_01 :heavy_check_mark: AC 111 ms 53 MB
g++ random_1_02 :heavy_check_mark: AC 63 ms 31 MB
g++ random_2_00 :heavy_check_mark: AC 31 ms 18 MB
g++ random_2_01 :heavy_check_mark: AC 9 ms 7 MB
g++ random_2_02 :heavy_check_mark: AC 45 ms 25 MB
g++ random_2_03 :heavy_check_mark: AC 74 ms 34 MB
g++ random_2_04 :heavy_check_mark: AC 25 ms 15 MB
g++ small_random_1_00 :heavy_check_mark: AC 3 ms 4 MB
g++ small_random_1_01 :heavy_check_mark: AC 2 ms 4 MB
g++ small_random_1_02 :heavy_check_mark: AC 2 ms 4 MB
g++ small_random_2_00 :heavy_check_mark: AC 2 ms 4 MB
g++ small_random_2_01 :heavy_check_mark: AC 2 ms 4 MB
g++ small_random_2_02 :heavy_check_mark: AC 2 ms 4 MB
Back to top page