# Problem - 1578C - Codeforces Cactus lady loves h...
Creado el: 10 de julio de 2026
Respondido usando GPT-5.6 Thinking por Chat01
Creado el: 10 de julio de 2026
Respondido usando GPT-5.6 Thinking por Chat01
Cactus lady loves her cactuses very much. Especially she likes a small cactus named Cing. Cing can be seen as a connected undirected graph in which every vertex lies on at most one simple cycle. Intuitively, a cactus is a generalization of a tree where some cycles are allowed. Multiedges (multiple edges between a pair of vertices) and loops (edges that connect a vertex to itself) are not allowed.
She bought a special grid for her special little cactus Cing. This grid can be represented as a graph consisting of two paths of length , and , connected together by edges for each . In other words, a grid can be seen as a ladder.

Cactus lady wants to know whether she can embed Cing into this grid, i.e., map each vertex of the cactus onto a separate vertex of the grid while each edge of the cactus will be mapped onto some edge of the grid.
Input
The first line contains an integer — the number of test cases.
Each test case begins with a line containing two integers and — the number of vertices and the number of edges in a given cactus, respectively (; ).
Each of the following lines contains two integers and , describing the edges of the cactus ().
The total sum of all in the input doesn't exceed .
Output
Print an answer for each test case in the same order the cases appear in the input.
For each test case print "No" in the first line, if no layout exists.
Otherwise print "Yes" in the first line, and the following lines describing the layout. The -th of these lines should contain two integers and , the location of the -th vertex (; ).
Example
Input
text5 4 3 1 2 2 3 3 4 8 7 1 2 3 2 2 4 4 5 4 6 6 7 6 8 5 4 1 2 1 3 1 4 1 5 8 9 1 2 2 3 3 4 1 4 4 5 5 6 6 7 7 8 5 8 10 10 1 2 2 3 3 4 4 5 5 6 6 1 3 7 4 8 1 9 6 10
Output
textYes 0 0 0 1 1 1 1 2 Yes 0 3 1 3 1 4 1 2 0 2 1 1 0 1 1 0 No Yes 0 0 1 0 1 1 0 1 0 2 0 3 1 3 1 2 Yes 1 1 1 2 1 3 0 3 0 2 0 1 1 4 0 4 1 0 0 0
Note
Empty lines between test cases are for clarity. In real test cases there are no empty lines.
In these notes, we consider the embeddings for tests 2 and 4.
We start with the embedding for test 2.

Here goes the embedding for test 4.
. T YOU ARE PROHIBITED FROM LOOKING FOR THE SOLUTION ON THE INTERNET.
pythonimport networkx as nx, itertools, math, collections, sys, time def ladder(k): G=nx.Graph() for r in range(2): for c in range(k): G.add_node((r,c)) if c: G.add_edge((r,c-1),(r,c)) for c in range(k): G.add_edge((0,c),(1,c)) return G def full_binary(depth): # levels 0..depth G=nx.Graph() nxt=1 cur=[0] for d in range(depth): new=[] for u in cur: for _ in range(2): G.add_edge(u,nxt); new.append(nxt); nxt+=1 cur=new return G for d in [2,3]: H=full_binary(d) print("nodes",len(H)) for k in range((len(H)+1)//2, len(H)+3): GM=nx.algorithms.isomorphism.GraphMatcher(ladder(k), H) if GM.subgraph_is_monomorphic(): print("embed k",k) # mapping host->H m=next(GM.subgraph_monomorphisms_iter()) inv={v:u for u,v in m.items()} print(inv) break else: print("no")
pythondef pathwidth_vertex_sep(G): n=len(G); nodes=list(G); idx={v:i for i,v in enumerate(nodes)} neigh=[0]*n for i,v in enumerate(nodes): mask=0 for w in G[v]: mask|=1<<idx[w] neigh[i]=mask N=1<<n # boundary count for subset S: vertices in S with neighbor outside b=[0]*N full=N-1 for S in range(N): cnt=0 x=S while x: l=x & -x; i=l.bit_length()-1 if neigh[i] & (full^S): cnt+=1 x-=l b[S]=cnt dp=[99]*N; dp[0]=0 for S in range(1,N): x=S best=99 while x: l=x&-x prev=S^l val=max(dp[prev],b[S]) if val<best:best=val x-=l dp[S]=best return dp[-1] for d in range(1,4): print(d, pathwidth_vertex_sep(full_binary(d)))
pythondef embed_backtrack(H,k, timeout=10): host=ladder(k) hnodes=sorted(H.nodes(), key=lambda v:-H.degree(v)) candidates={v:[x for x in host if host.degree(x)>=H.degree(v)] for v in H} # use recursive MRV dynamic assign={} used=set() start=time.time() def rec(): if time.time()-start>timeout: raise TimeoutError if len(assign)==len(H): return assign.copy() # choose unassigned with max assigned neighbors then degree, min candidates best=None; bestcand=None for v in H: if v in assign: continue cand=[] for x in candidates[v]: if x in used: continue ok=True for w in H[v]: if w in assign and not host.has_edge(x,assign[w]): ok=False; break if ok: cand.append(x) key=(sum(w in assign for w in H[v]), H.degree(v), -len(cand)) if best is None or key>best: best=key; bestcand=(v,cand) v,cand=bestcand # symmetry first root maybe for x in cand
pythondef deg3_on_path(T): D=[v for v in T if T.degree(v)==3] if len(D)<=2:return True # in tree, D all on a path iff choose farthest pair among D and all D lie path a=D[0] dist=nx.single_source_shortest_path_length(T,a) b=max(D,key=dist.get) distb=nx.single_source_shortest_path_length(T,b) c=max(D,key=distb.get) path=set(nx.shortest_path(T,b,c)) return all(v in path for v in D) def ladder_embed_exists(T): n=len(T) # K=n enough GM=nx.algorithms.isomorphism.GraphMatcher(ladder(n),T) return GM.subgraph_is_monomorphic() for n in range(2,11): bad=[] cnt=0 for T in nx.generators.nonisomorphic_trees(n): if max(dict(T.degree()).values())>3: continue e=ladder_embed_exists(T) c=deg3_on_path(T) if e!=c: bad.append((T.copy(),e,c)) cnt+=1 print(n,cnt,len(bad), [(x[1],x[2]) for x in bad[:3]]) if bad: print(nx.to_graph6_bytes(bad[0][0]).strip()) break
pythonT=bad[0][0] print(dict(T.degree())) print(sorted(T.edges())) print("deg3", [v for v in T if T.degree(v)==3]) print("diam", nx.diameter(T))
pythondef bandwidth_brute(G): nodes=list(G) n=len(nodes) # brute perms n9 362k okay best=n bestp=None for p in itertools.permutations(nodes): pos={v:i for i,v in enumerate(p)} b=max(abs(pos[u]-pos[v]) for u,v in G.edges()) if b<best: best=b;bestp=p if best<=2:return best,bestp return best,bestp print(bandwidth_brute(T)[0])
pythonbw,p=bandwidth_brute(T) print(p) print([(u,v,abs(p.index(u)-p.index(v))) for u,v in T.edges()])
python# tree with deg3 chain 1-0-4, endpoints each 2 leaves, center one leaf S=nx.Graph() S.add_edges_from([(1,0),(0,4),(1,2),(1,3),(4,5),(4,6),(0,7)]) print(ladder_embed_exists(S))
pythondef spine_embed_tree(T, get=False): n=len(T) # all simple paths endpoints incl same nodes=list(T) for a in nodes: for b in nodes: if a>b: continue P=nx.shortest_path(T,a,b) pset=set(P) # components outside must be paths and attach unique hairs=[]; ok=True seen=set() for v in nodes: if v in pset or v in seen: continue comp=set(nx.node_connected_component(T.subgraph(set(nodes)-pset),v)) seen|=comp # unique edge to P att=[] for x in comp: for y in T[x]: if y in pset:att.append((x,y)) if len(att)!=1: ok=False;break H=T.subgraph(comp) if max(dict(H.degree()).values(),default=0)>2 or len(comp)>1 and sum(dict(H.degree()).values())!=2*(len(comp)-1): ok=False;break root,anchor=att[0] # root must endpoint of path comp if H.degree(root)>1: ok=False;break hairs.append((P.index(anchor),len(comp),comp,root)) if not ok:continue # choose L/R intervals no overlap, but intervals include anchor bottom root, length len(comp) # brute 2^h for small h=len(hairs) for mask in range(1<<h): occ=set(); good=True choices=[] for i,(pos,L,*_) in enumerate(hairs): if (mask>>i)&1: ints=range(pos,pos+L) dir=1 else: ints=range(pos-L+1,pos+1) dir=-1 if any(x in occ for x in ints): good=False;break occ.update(ints); choices.append(dir) if good: return (P,hairs,choices) if get else True return None if get else False for n in range(2,11): mism=0 ex=None for Tn in nx.generators.nonisomorphic_trees(n): if max(dict(Tn.degree()).values())>3: continue e=ladder_embed_exists(Tn) c=spine_embed_tree(Tn) if e!=c: mism+=1; ex=(Tn.copy(),e,c) print(n,mism) if ex: print(ex[1:], sorted(ex[0].edges()));break
pythondef is_cactus(G): if not nx.is_connected(G): return False # each vertex in at most one simple cycle; use biconnected comps: each block edge or cycle for comp in nx.biconnected_components(G): H=G.subgraph(comp) if len(comp)>=3: if H.number_of_edges()!=len(comp) or any(H.degree(v)!=2 for v in comp): return False return True def path_spine_model(G,get=False): nodes=list(G) # enumerate all simple paths via endpoints shortest? In cactus, between endpoints may multiple paths due cycles. # Enumerate all simple paths with nx.all_simple_paths for small. paths=[] for a in nodes: # singleton paths.append([a]) for b in nodes: if a<b: paths += list(nx.all_simple_paths(G,a,b)) seenp=set() for P in paths: tup=tuple(P) # reverse dup okay pset=set(P) if len(pset)!=len(P): continue pos={v:i for i,v in enumerate(P)} rem=set(nodes)-pset comps=[] ok=True seen=set() for v in list(rem): if v in seen: continue comp=set(nx.node_connected_component(G.subgraph(rem),v)); seen|=comp H=G.subgraph(comp) if not nx.is_tree(H) or max(dict(H.degree()).values(),default=0)>2: ok=False;break # path order if len(comp)==1: order=[next(iter(comp))] else: end=next(x for x in comp if H.degree(x)==1) order=list(nx.dfs_preorder_nodes(H,end)) # in path should order # better walk order=[];prev=None;cur=end while True: order.append(cur) nxt=[w for w in H[cur] if w!=prev] if not nxt:break prev,cur=cur,nxt[0] idx={x:i for i,x in enumerate(order)} at=[] for x in comp: for y in G[x]: if y in pset: at.append((pos[y],idx[x],x,y)) if len(at)>2: ok=False;break # A connected G with P not all means at least1 if len(at)==0: ok=False;break intervals=[] L=len(order) if len(at)==1: pp,ii,*_=at[0] intervals=[(pp-ii, pp+(L-1-ii), 1), (pp-(L-1-ii), pp+ii, -1)] else: (p1,i1,*_),(p2,i2,*_)=at if p1==p2 or i1==i2: ok=False;break # orientation + means coord = off + idx, constraints p1-i1=p2-i2 intervals=[] if p1-i1==p2-i2: o=p1-i1;intervals.append((o,o+L-1,1)) # reversed coord=off + (L-1-idx); off = p-(L-1-i) if p1-(L-1-i1)==p2-(L-1-i2): o=p1-(L-1-i1); intervals.append((o,o+L-1,-1)) if not intervals: ok=False;break comps.append(intervals) if not ok: continue # choose one interval per comp disjoint; brute def rec(j,occ,sel): if j==len(comps):return sel[:] for iv in comps[j]: a,b,_=iv if all(x not in occ for x in range(a,b+1)): for x in range(a,b+1):occ.add(x) sel.append(iv) z=rec(j+1,occ,sel) if z:return z sel.pop() for x in range(a,b+1):occ.remove(x) return None sel=rec(0,set(),[]) if sel is not None: return (P,sel) if get else True return None if get else False atlas=nx.graph_atlas_g() for n in range(1,8): mism=[] for G in atlas: if len(G)!=n or n==0:continue if not is_cactus(G):continue if max(dict(G.degree()).values(),default=0)>3: # can skip but model false pass e=nx.algorithms.isomorphism.GraphMatcher(ladder(n),G).subgraph_is_monomorphic() c=path_spine_model(G) if e!=c: mism.append((G.copy(),e,c)) print("n",n,"mism",len(mism),"total",sum(1 for G in atlas if len(G)==n and is_cactus(G))) if mism: G,e,c=mism[0];print(e,c,sorted(G.edges()),dict(G.degree()));break
pythondef path_spine_model(G,get=False): nodes=list(G) paths=[] for a in nodes: paths.append([a]) for b in nodes: if a<b: paths += list(nx.all_simple_paths(G,a,b)) for P in paths: if len(set(P))!=len(P): continue pos={v:i for i,v in enumerate(P)} # every edge inside P must be consecutive bad=False for u,v in G.edges(): if u in pos and v in pos and abs(pos[u]-pos[v])!=1: bad=True;break if bad: continue pset=set(P) rem=set(nodes)-pset comps=[] ok=True seen=set() comp_orders=[] for v in list(rem): if v in seen: continue comp=set(nx.node_connected_component(G.subgraph(rem),v)); seen|=comp H=G.subgraph(comp) if not nx.is_tree(H) or max(dict(H.degree()).values(),default=0)>2: ok=False;break if len(comp)==1: order=[next(iter(comp))] else: ends=[x for x in comp if H.degree(x)==1] if len(ends)!=2: ok=False;break order=[];prev=None;cur=ends[0] while True: order.append(cur) nxt=[w for w in H[cur] if w!=prev] if not nxt:break prev,cur=cur,nxt[0] idx={x:i for i,x in enumerate(order)} at=[] for x in comp: for y in G[x]: if y in pset: at.append((pos[y],idx[x],x,y)) if not (1<=len(at)<=2): ok=False;break L=len(order) intervals=[] if len(at)==1: pp,ii,*_=at[0] intervals=[(pp-ii, pp+(L-1-ii), 1), (pp-(L-1-ii), pp+ii, -1)] else: (p1,i1,*_),(p2,i2,*_)=at if p1==p2 or i1==i2: ok=False;break if p1-i1==p2-i2: o=p1-i1; intervals.append((o,o+L-1,1)) if p1-(L-1-i1)==p2-(L-1-i2): o=p1-(L-1-i1); intervals.append((o,o+L-1,-1)) # dedupe intervals=list(dict.fromkeys(intervals)) if not intervals: ok=False;break comps.append(intervals) comp_orders.append(order) if not ok: continue def rec(j,occ,sel): if j==len(comps):return sel[:] for iv in comps[j]: a,b,_=iv if all(x not in occ for x in range(a,b+1)): occ.update(range(a,b+1)); sel.append(iv) z=rec(j+1,occ,sel) if z:return z sel.pop() for x in range(a,b+1):occ.remove(x) return None sel=rec(0,set(),[]) if sel is not None: return (P,comp_orders,sel) if get else True return None if get else False for n in range(1,8): mism=[] total=0 for G in atlas: if len(G)!=n:continue if not is_cactus(G):continue total+=1 e=nx.algorithms.isomorphism.GraphMatcher(ladder(n),G).subgraph_is_monomorphic() c=path_spine_model(G) if e!=c: mism.append((G.copy(),e,c)) print("n",n,"mism",len(mism),"total",total) if mism: G,e,c=mism[0];print(e,c,sorted(G.edges()),dict(G.degree()));break
pythondef model_with_longest_path(G): # get all simple paths max len small; adapt validate a given P allp=[] maxl=0 nodes=list(G) for a in nodes: for b in nodes: if a<=b: ps=[[a]] if a==b else list(nx.all_simple_paths(G,a,b)) for p in ps: if len(p)>maxl:maxl=len(p);allp=[p] elif len(p)==maxl:allp.append(p) # use helper by force? copy validator for P in allp: # build temporary function logic manually via closure from above impossible; implement return valid P if validate_spine(G,P): return True,P return False,None def validate_spine(G,P,get=False): nodes=list(G); pos={v:i for i,v in enumerate(P)} for u,v in G.edges(): if u in pos and v in pos and abs(pos[u]-pos[v])!=1:return False pset=set(P); rem=set(nodes)-pset; comps=[]; orders=[] seen=set() for v in list(rem): if v in seen:continue comp=set(nx.node_connected_component(G.subgraph(rem),v));seen|=comp H=G.subgraph(comp) if not nx.is_tree(H) or max(dict(H.degree()).values(),default=0)>2:return False if len(comp)==1:order=[next(iter(comp))] else: ends=[x for x in comp if H.degree(x)==1] if len(ends)!=2:return False order=[];prev=None;cur=ends[0] while True: order.append(cur);nxt=[w for w in H[cur] if w!=prev] if not nxt:break prev,cur=cur,nxt[0] idx={x:i for i,x in enumerate(order)} at=[] for x in comp: for y in G[x]: if y in pos:at.append((pos[y],idx[x])) if not 1<=len(at)<=2:return False L=len(order);ivs=[] if len(at)==1: p,i=at[0] ivs=[(p-i,p+L-1-i,1),(p-(L-1-i),p+i,-1)] else: (p1,i1),(p2,i2)=at if p1==p2 or i1==i2:return False if p1-i1==p2-i2: o=p1-i1;ivs.append((o,o+L-1,1)) if p1-(L-1-i1)==p2-(L-1-i2): o=p1-(L-1-i1);ivs.append((o,o+L-1,-1)) if not ivs:return False comps.append(ivs);orders.append(order) # interval choices can be solved brute small def rec(j,occ): if j==len(comps):return [] for iv in comps[j]: a,b,_=iv if not any(x in occ for x in range(a,b+1)): z=rec(j+1,occ|set(range(a,b+1))) if z is not None:return [iv]+z return None sel=rec(0,set()) if sel is None:return False return (orders,sel) if get else True for n in range(1,8): cnt=0;bad=[] for G in atlas: if len(G)!=n or not is_cactus(G):continue e=nx.algorithms.isomorphism.GraphMatcher(ladder(n),G).subgraph_is_monomorphic() if not e:continue ok,p=model_with_longest_path(G) if not ok:bad.append(G.copy()) print(n,len(bad)) if bad: print(sorted(bad[0].edges()));break
python# sample2 G=nx.Graph() G.add_edges_from([(1,2),(3,2),(2,4),(4,5),(4,6),(6,7),(6,8)]) for k in range(4,9): print(k,nx.algorithms.isomorphism.GraphMatcher(ladder(k),G).subgraph_is_monomorphic())
pythonfor n in range(2,11): bad=[] for T in nx.generators.nonisomorphic_trees(n): if max(dict(T.degree()).values())>3: continue e=ladder_embed_exists(T) if not e: continue diam=nx.diameter(T) found=False for a in T: for b in T: if a<b and nx.shortest_path_length(T,a,b)==diam: if validate_spine(T,nx.shortest_path(T,a,b)): found=True;break if found:break if not found:bad.append(T.copy()) print(n,len(bad)) if bad: print(sorted(bad[0].edges()));break
pythondef validate_tree_diameter_simple(T,P,get=False): pos={v:i for i,v in enumerate(P)}; pset=set(P) comps=[];orders=[] rem=set(T)-pset;seen=set() for v in rem: if v in seen:continue comp=set(nx.node_connected_component(T.subgraph(rem),v));seen|=comp H=T.subgraph(comp) if not nx.is_tree(H) or max(dict(H.degree()).values())>2:return False at=[] for x in comp: for y in T[x]: if y in pset:at.append((x,y)) if len(at)!=1:return False root,anchor=at[0] if len(comp)>1 and H.degree(root)!=1:return False # order from root order=[];prev=None;cur=root while True: order.append(cur);nxt=[w for w in H[cur] if w!=prev] if not nxt:break prev,cur=cur,nxt[0] comps.append((pos[anchor],len(order),order)) # each interval either starts anchor to right or ends anchor to left # solve brute small def rec(i,occ): if i==len(comps):return [] p,L,_=comps[i] for d in [-1,1]: ints=range(p-L+1,p+1) if d==-1 else range(p,p+L) if not any(x in occ for x in ints): z=rec(i+1,occ|set(ints)) if z is not None:return [d]+z return None z=rec(0,set()) if z is None:return False return (comps,z) if get else True for n in range(2,11): bad=[] for T in nx.generators.nonisomorphic_trees(n): if max(dict(T.degree()).values())>3 or not ladder_embed_exists(T):continue d=nx.diameter(T);found=False for a in T: for b in T: if a<b and nx.shortest_path_length(T,a,b)==d: P=nx.shortest_path(T,a,b) if validate_tree_diameter_simple(T,P): found=True;break if found:break if not found:bad.append(T.copy()) print(n,len(bad)) if bad: print(sorted(bad[0].edges()));break
pythondef arbitrary_diameter(T): a=next(iter(T)) dist=nx.single_source_shortest_path_length(T,a) u=max(dist,key=lambda x:(dist[x],x)) dist2=nx.single_source_shortest_path_length(T,u) v=max(dist2,key=lambda x:(dist2[x],x)) return nx.shortest_path(T,u,v) for n in range(2,13): bad=0; ex=None for T in nx.generators.nonisomorphic_trees(n): if max(dict(T.degree()).values())>3:continue # use path model rather than expensive brute as ground truth valid=path_spine_model(T) if valid and not validate_spine(T,arbitrary_diameter(T)): bad+=1;ex=T.copy();break print(n,bad) if ex: print(sorted(ex.edges()), arbitrary_diameter(ex));break
python# recreate counterexample n9 T=nx.Graph() T.add_edges_from([(0,4),(0,7),(1,0),(1,2),(1,3),(4,5),(4,6),(7,8)]) edges=list(T.edges()) def matchings_cover_deg3(G): es=list(G.edges()); D=[v for v in G if G.degree(v)==3] out=[] for mask in range(1<<len(es)): M=[es[i] for i in range(len(es)) if mask>>i&1] used=[] if len(set(sum(([u,v] for u,v in M),[])))<2*len(M):continue if all(any(v in e for e in M) for v in D): H=G.copy();H.remove_edges_from(M) if max(dict(H.degree()).values())<=2 and nx.is_forest(H): out.append(M) return out Ms=matchings_cover_deg3(T) len(Ms),Ms[:10] (12, [[(0, 1), (4, 5)], [(0, 1), (4, 6)], [(0, 1), (4, 5), (7, 8)], [(0, 1), (4, 6), (7, 8)], [(0, 4), (1, 2)], [(0, 7), (4, 5), (1, 2)], [(0, 7), (4, 6), (1, 2)], [(0, 4), (7, 8), (1, 2)], [(0, 4), (1, 3)], [(0, 7), (4, 5), (1, 3)]])
pythondef max_weight_path_tree(T): # weight B for deg3 then 1; brute endpoints for small best=None; bp=None for a in T: for b in T: if a<=b: p=nx.shortest_path(T,a,b) score=(sum(T.degree(v)==3 for v in p),len(p)) if best is None or score>best: best=score;bp=p return bp,best for n in range(2,13): bad=[] for Tn in nx.generators.nonisomorphic_trees(n): if max(dict(Tn.degree()).values())>3: continue valid=path_spine_model(Tn) if not valid: continue p,_=max_weight_path_tree(Tn) if not validate_spine(Tn,p): bad.append(Tn.copy());break print(n,len(bad)) if bad: print(sorted(bad[0].edges()),max_weight_path_tree(bad[0]));break
pythonG=bad[0] res=path_spine_model(G,get=True) res[0],res[1],res[2] ([3, 2, 1, 0, 5, 6], [[4], [7], [9, 8, 10]], [(1, 1, 1), (5, 5, 1), (2, 4, 1)])
python# chain deg3 A-B-C, A,C each two leaves; B side arm length L def tree_counter(L): G=nx.Graph() # A=0 B=1 C=2 G.add_edges_from([(0,1),(1,2),(0,3),(0,4),(2,5),(2,6)]) prev=1 for i in range(L): x=7+i;G.add_edge(prev,x);prev=x return G for L in range(1,8): G=tree_counter(L) print(L,path_spine_model(G), "brute" if len(G)<=11 else "")
pythonfrom functools import lru_cache def tree_dp_embeddable(T, construct=False): if len(T)==1:return True @lru_cache(None) def H(v,p): ch=[u for u in T[v] if u!=p] if len(ch)>2:return False if len(ch)==0:return True if len(ch)==1:return H(ch[0],v) a,b=ch return branch_ok(v,a,b) or branch_ok(v,b,a) @lru_cache(None) def Pair(v,p,w,q): # canonical maybe rows ordered matters but symmetric cv=[u for u in T[v] if u!=p] cw=[u for u in T[w] if u!=q] if len(cv)>1 or len(cw)>1: return False if not cv and not cw:return True if not cv: # w must have exactly1; at next col v row free, w child alone return H(cw[0],w) if not cw: return H(cv[0],v) return Pair(cv[0],v,cw[0],w) def branch_ok(v,right,vert): # right starts next col H(right,v) cv=[u for u in T[vert] if u!=v] if len(cv)>1:return False if len(cv)==0:return H(right,v) return Pair(right,v,cv[0],vert) for l in T: if T.degree(l)==1: v=next(iter(T[l])) if H(v,l):return True return False for n in range(2,13): mism=[] for Tn in nx.generators.nonisomorphic_trees(n): e=path_spine_model(Tn) # exact small d=tree_dp_embeddable(Tn) if e!=d: mism.append((Tn.copy(),e,d));break print(n,len(mism)) if mism: G,e,d=mism[0];print(e,d,dict(G.degree()),sorted(G.edges()));break
pythonfrom functools import lru_cache def tree_dp2(T): n=len(T) if n==1:return True if max(dict(T.degree()).values())>3:return False @lru_cache(None) def H(v,p): ch=[u for u in T[v] if u!=p] if len(ch)>2:return False if len(ch)==0:return True if len(ch)==1:return H(ch[0],v) a,b=ch return branch(v,a,b) or branch(v,b,a) def branch(v,right,vert): cv=[u for u in T[vert] if u!=v] if len(cv)>1:return False if len(cv)==0:return H(right,v) return PP(right,v,cv[0],vert) @lru_cache(None) def PP(v,p,w,q): cv=[u for u in T[v] if u!=p] cw=[u for u in T[w] if u!=q] if len(cv)>1 or len(cw)>1:return False if not cv and not cw:return True if not cv:return H(cw[0],w) if not cw:return H(cv[0],v) return PP(cv[0],v,cw[0],w) def assignments(u,v): rem=[x for x in T[u] if x!=v] if len(rem)==0:return [(None,None)] # L,R if len(rem)==1:return [(rem[0],None),(None,rem[0])] if len(rem)==2:return [(rem[0],rem[1]),(rem[1],rem[0])] return [] def side(a,u,b,v): if a is None and b is None:return True if a is None:return H(b,v) if b is None:return H(a,u) return PP(a,u,b,v) for u,v in T.edges(): for lu,ru in assignments(u,v): for lv,rv in assignments(v,u): if side(lu,u,lv,v) and side(ru,u,rv,v): return True return False for n in range(2,14): mism=[] for Tn in nx.generators.nonisomorphic_trees(n): exact=path_spine_model(Tn) if n<=11 else tree_dp2(Tn) # for >11 no exact expensive d=tree_dp2(Tn) if n<=11 and exact!=d: mism.append((Tn.copy(),exact,d));break print(n,len(mism)) if mism: G,e,d=mism[0];print(e,d,dict(G.degree()),sorted(G.edges()));break
pythondef decompose_cactus(G): cycles=nx.cycle_basis(G) # cycle_basis order is cyclic path likely cyc_id={} cyc_orders=[] cyc_edges=set() for cid,cy in enumerate(cycles): # networkx cycle_basis returns order where consecutive and last-first edges cyc_orders.append(cy) for v in cy: if v in cyc_id: return None cyc_id[v]=cid for i in range(len(cy)): a,b=cy[i],cy[(i+1)%len(cy)] cyc_edges.add(tuple(sorted((a,b)))) bridge_nei={v:[] for v in G} for u,v in G.edges(): if tuple(sorted((u,v))) not in cyc_edges: bridge_nei[u].append(v);bridge_nei[v].append(u) return cycles,cyc_id,cyc_edges,bridge_nei def cactus_dp_bool(G): if len(G)==1:return True if max(dict(G.degree()).values())>3:return False dec=decompose_cactus(G) if dec is None:return False cycles,cyc_id,cyc_edges,bn=dec # ensure all biconnected are cycles / cactus assumed from functools import lru_cache @lru_cache(None) def H(v,p): # p-v must bridge if v in cyc_id: cid=cyc_id[v]; C=cycles[cid]; L=len(C) if L%2:return False # v should have p as only bridge neighbor if p not in bn[v]: return False # no other bridge at v due deg anyway idx=C.index(v) # two choices for c1 = next or prev; build oriented c0=v,c1... for step in (1,-1): seq=[C[(idx+step*i)%L] for i in range(L)] if cycle_half_ok(seq, left_single=True): return True return False ch=[u for u in bn[v] if u!=p] if len(ch)>2:return False if len(ch)==0:return True if len(ch)==1:return H(ch[0],v) a,b=ch return branch(v,a,b) or branch(v,b,a) def branch(v,right,vert): if vert in cyc_id:return False cv=[u for u in bn[vert] if u!=v] if len(cv)>1:return False if len(cv)==0:return H(right,v) return PP(right,v,cv[0],vert) @lru_cache(None) def PP(v,p,w,q): # symmetric but memo order not normalized if v in cyc_id or w in cyc_id:return False cv=[u for u in bn[v] if u!=p] cw=[u for u in bn[w] if u!=q] if len(cv)>1 or len(cw)>1:return False if not cv and not cw:return True if not cv:return H(cw[0],w) if not cw:return H(cv[0],v) return PP(cv[0],v,cw[0],w) def side(a,pa,b,pb): if a is None and b is None:return True if a is None:return H(b,pb) if b is None:return H(a,pa) return PP(a,pa,b,pb) def cycle_half_ok(seq,left_single): # seq length 2k: rung seq0-seq1, opposite seq[k]-seq[k+1] L=len(seq);k=L//2 corners={seq[0],seq[1],seq[k],seq[k+1]} # interiors no bridges for x in seq: if x not in corners and bn[x]: return False # for H, seq0 has parent bridge, seq1 must no bridge; # right corners can have 0/1 child if left_single: if len(bn[seq[0]])!=1: # parent only return False if len(bn[seq[1]])!=0: return False a=bn[seq[k]][0] if bn[seq[k]] else None b=bn[seq[k+1]][0] if bn[seq[k+1]] else None return side(a,seq[k],b,seq[k+1]) else: # central cycle: all corner bridges handled outward la=bn[seq[0]][0] if bn[seq[0]] else None lb=bn[seq[1]][0] if bn[seq[1]] else None ra=bn[seq[k]][0] if bn[seq[k]] else None rb=bn[seq[k+1]][0] if bn[seq[k+1]] else None return side(la,seq[0],lb,seq[1]) and side(ra,seq[k],rb,seq[k+1]) # central vertical bridge for u,v in G.edges(): if tuple(sorted((u,v))) in cyc_edges:continue if u in cyc_id or v in cyc_id:continue remu=[x for x in bn[u] if x!=v] remv=[x for x in bn[v] if x!=u] def assigns(rem): if len(rem)==0:return [(None,None)] if len(rem)==1:return [(rem[0],None),(None,rem[0])] if len(rem)==2:return [(rem[0],rem[1]),(rem[1],rem[0])] return [] for lu,ru in assigns(remu): for lv,rv in assigns(remv): if side(lu,u,lv,v) and side(ru,u,rv,v): return True # central cycle for C in cycles: L=len(C) if L%2:continue for i in range(L): seq=[C[(i+j)%L] for j in range(L)] if cycle_half_ok(seq,left_single=False): return True return False for n in range(1,8): mism=[];tot=0 for G in atlas: if len(G)!=n or not is_cactus(G):continue tot+=1 e=nx.algorithms.isomorphism.GraphMatcher(ladder(n),G).subgraph_is_monomorphic() d=cactus_dp_bool(G) if e!=d: mism.append((G.copy(),e,d));break print("n",n,"tot",tot,"mism",len(mism)) if mism: G,e,d=mism[0];print(e,d,dict(G.degree()),sorted(G.edges()),nx.cycle_basis(G));break
python# build graph G=nx.Graph() # A cycle 0-1-2-3-0 where 0 topL,1 topR,2 botR,3 botL G.add_edges_from([(0,1),(1,2),(2,3),(3,0)]) # B 4 topL,5 topR,6 botR,7 botL G.add_edges_from([(4,5),(5,6),(6,7),(7,4)]) # bridge topR A1 - 8 - 9 - topL B4 G.add_edges_from([(1,8),(8,9),(9,4)]) # inner bottom leaves G.add_edge(2,10) G.add_edge(7,11) print(is_cactus(G), cactus_dp_bool(G)) # verify brute with ladder 12 maybe potentially expensive print(nx.algorithms.isomorphism.GraphMatcher(ladder(12),G).subgraph_is_monomorphic())
pythonG=nx.Graph() G.add_edges_from([(0,1),(1,2),(2,3),(3,0)]) G.add_edges_from([(4,5),(5,6),(6,7),(7,4)]) # connect A topR=1 to B topL=4 through 8,9 G.add_edges_from([(1,8),(8,9),(9,4)]) # leaves on other vertices nxt=10 for v in [0,2,3,5,6,7]: G.add_edge(v,nxt);nxt+=1 print(len(G),is_cactus(G),cactus_dp_bool(G)) # explicit embedding maybe need map: # A cols0,1. B cols4,5. # A vertices:0 top0,1 top1,2 bot1,3 bot0 # B:4 top4,5 top5,6 bot5,7 bot4 # path 1-8-9-4 top cols2,3 # leaves: at0 outward top-1; at3 outward bottom-1; at2 bottom2; at7 bottom3; at5 top6; at6 bottom6 coords={0:(0,0),1:(0,1),2:(1,1),3:(1,0),4:(0,4),5:(0,5),6:(1,5),7:(1,4),8:(0,2),9:(0,3)} for v,c in zip([10,11,12,13,14,15],[(-1 if False else 0, -1),(1,-1),(1,2),(0,6),(1,6),(1,3)]): coords[v]=c # correct assignments zip order vertices [0,2,3,5,6,7] # I assigned 10 top-1,11 bottom-1(to2? wrong order) print(sorted(G.edges())) # verify coords adjacency ok=True for u,v in G.edges(): xu,yu=coords[u];xv,yv=coords[v] if abs(xu-xv)+abs(yu-yv)!=1: print("bad",u,v,coords[u],coords[v]);ok=False print("coord ok",ok, len(set(coords.values()))==len(coords))
pythoncoords={0:(0,0),1:(0,1),2:(1,1),3:(1,0),4:(0,4),5:(0,5),6:(1,5),7:(1,4),8:(0,2),9:(0,3), 10:(0,-1),11:(1,2),12:(1,-1),13:(0,6),14:(1,6),15:(1,3)} ok=True for u,v in G.edges(): xu,yu=coords[u];xv,yv=coords[v] if abs(xu-xv)+abs(yu-yv)!=1: print("bad",u,v,coords[u],coords[v]);ok=False print(ok,len(set(coords.values()))==len(coords))
pythondef tree_spine_candidates(T): nodes=list(T) # brute directed side path for now sidepath={} side_nodes={} for u,v in T.edges(): for a,b in [(u,v),(v,u)]: H=T.copy();H.remove_edge(a,b) comp=set(nx.node_connected_component(H,b)) HH=T.subgraph(comp) ok=max(dict(HH.degree()).values(),default=0)<=2 sidepath[(a,b)]=ok side_nodes[(a,b)]=comp mandatory=[(u,v) for u,v in T.edges() if not sidepath[(u,v)] and not sidepath[(v,u)]] C=nx.Graph();C.add_edges_from(mandatory) candidates=[] def extension(e, nb): comp=side_nodes[(e,nb)] H=T.subgraph(comp) # path, start at nb, choose each endpoint direction ends=[x for x in comp if H.degree(x)<=1] out=[] for z in ends: out.append(nx.shortest_path(H,nb,z)) # starts nb # dedupe uniq=[] for p in out: if p not in uniq:uniq.append(p) return uniq if mandatory: if not nx.is_connected(C) or max(dict(C.degree()).values())>2: return [] ends=[v for v in C if C.degree(v)==1] if len(C)==1: core=[next(iter(C))] else: core=nx.shortest_path(C,ends[0],ends[1]) # extension options each end: [] or branch path opts=[] for idx,e in enumerate([core[0],core[-1]]): core_nb=core[1] if len(core)>1 and idx==0 else (core[-2] if len(core)>1 else None) o=[[]] for nb in T[e]: if nb==core_nb:continue if sidepath.get((e,nb),False): o += extension(e,nb) opts.append(o) for L in opts[0]: for R in opts[1]: # L starts neighbor outward from core[0], want reverse to endpoint then core p=list(reversed(L))+core+R # if L/R empty okay; avoid duplication core endpoint? extension excludes e yes starts nb if len(set(p))==len(p):candidates.append(p) else: deg3=[v for v in T if T.degree(v)==3] centers=deg3 if deg3 else [nodes[0]] for c in centers: dirs=[None]+list(T[c]) # choose ordered distinct left/right dirs for dl in dirs: for dr in dirs: if dl is not None and dr is not None and dl==dr:continue # all unchosen neighbor sides must be path ok=True for nb in T[c]: if nb not in (dl,dr) and not sidepath[(c,nb)]: ok=False if not ok:continue Lopts=[[]] if dl is None else (extension(c,dl) if sidepath[(c,dl)] else []) Ropts=[[]] if dr is None else (extension(c,dr) if sidepath[(c,dr)] else []) for L in Lopts: for R in Ropts: p=list(reversed(L))+[c]+R if len(set(p))==len(p):candidates.append(p) # dedupe tuples out=[];seen=set() for p in candidates: t=tuple(p) if t not in seen and tuple(reversed(p)) not in seen: seen.add(t);out.append(p) return out for n in range(2,13): bad=[] for Tn in nx.generators.nonisomorphic_trees(n): exact=path_spine_model(Tn) cand=any(validate_spine(Tn,p) for p in tree_spine_candidates(Tn)) if exact!=cand: bad.append((Tn.copy(),exact,cand,tree_spine_candidates(Tn)));break print(n,len(bad)) if bad: G,e,c,ps=bad[0];print(e,c,dict(G.degree()),sorted(G.edges()),ps[:5]);break
pythonINF=10**9 def cactus_gap_dp(G): n=len(G) if n==1:return True if max(dict(G.degree()).values())>3:return False dec=decompose_cactus(G) if dec is None:return False cycles,cyc_id,cyc_edges,bn=dec # path side info brute pathlen={} for u,v in G.edges(): if tuple(sorted((u,v))) in cyc_edges: continue H=G.copy();H.remove_edge(u,v) for a,b in [(u,v),(v,u)]: comp=set(nx.node_connected_component(H,b)) HH=G.subgraph(comp) ok=nx.is_tree(HH) and max(dict(HH.degree()).values(),default=0)<=2 pathlen[(a,b)]=len(comp) if ok else None from functools import lru_cache def vert_plans(v,p): # v placed vertical from p; cycle impossible. if v in cyc_id:return [] ch=[u for u in bn[v] if u!=p] plans=[] # assignments back,forward at most one each, all children assigned if len(ch)==0: plans=[(0,None)] elif len(ch)==1: a=ch[0] plans.append((0,a)) # forward L=pathlen.get((v,a)) if L is not None: plans.append((L,None)) elif len(ch)==2: a,b=ch La=pathlen.get((v,a)); Lb=pathlen.get((v,b)) if La is not None: plans.append((La,b)) if Lb is not None: plans.append((Lb,a)) return plans @lru_cache(None) def need(v,p): # minimum g>=1 if v in cyc_id: C=cycles[cyc_id[v]];L=len(C) if L%2:return INF idx=C.index(v) best=INF for step in (1,-1): seq=[C[(idx+step*i)%L] for i in range(L)] k=L//2 corners={seq[0],seq[1],seq[k],seq[k+1]} if any(bn[x] for x in seq if x not in corners): continue # v must only have parent bridge if bn[seq[0]] != [p] and set(bn[seq[0]])!={p}: continue # left partner backward branch optional if len(bn[seq[1]])>1: continue back=0 if bn[seq[1]]: z=bn[seq[1]][0] back=pathlen.get((seq[1],z)) if back is None: continue # right children ra=bn[seq[k]][0] if bn[seq[k]] else None rb=bn[seq[k+1]][0] if bn[seq[k+1]] else None if side_forward(ra,seq[k],rb,seq[k+1]): best=min(best,back+1) return best ch=[u for u in bn[v] if u!=p] if len(ch)>2:return INF best=INF if len(ch)==0:return 1 if len(ch)==1: a=ch[0] r=need(a,v) if r<INF:best=min(best,max(1,r-1)) # vertical child only for back,fwd in vert_plans(a,v): if fwd is None: best=min(best,back+1) else: if need(fwd,a)<=1: best=min(best,back+1) return best a,b=ch for h,vert in [(a,b),(b,a)]: for back,fwd in vert_plans(vert,v): if fwd is None: if need(h,v)<=1: best=min(best,back+1) else: if pair(h,v,fwd,vert): best=min(best,back+1) return best @lru_cache(None) def pair(v,p,w,q): if v in cyc_id or w in cyc_id:return False cv=[u for u in bn[v] if u!=p] cw=[u for u in bn[w] if u!=q] if len(cv)>1 or len(cw)>1:return False if not cv and not cw:return True if not cv:return need(cw[0],w)<=1 if not cw:return need(cv[0],v)<=1 return pair(cv[0],v,cw[0],w) def side_forward(a,pa,b,pb): if a is None and b is None:return True if a is None:return need(b,pb)<=1 if b is None:return need(a,pa)<=1 return pair(a,pa,b,pb) # central bridge def assigns(rem): if len(rem)==0:return [(None,None)] if len(rem)==1:return [(rem[0],None),(None,rem[0])] if len(rem)==2:return [(rem[0],rem[1]),(rem[1],rem[0])] return [] for u,v in G.edges(): if tuple(sorted((u,v))) in cyc_edges:continue if u in cyc_id or v in cyc_id:continue ru=[x for x in bn[u] if x!=v] rv=[x for x in bn[v] if x!=u] for lu,rru in assigns(ru): for lv,rrv in assigns(rv): if side_forward(lu,u,lv,v) and side_forward(rru,u,rrv,v): return True # central cycle for C in cycles: L=len(C) if L%2:continue k=L//2 for i in range(L): seq=[C[(i+j)%L] for j in range(L)] corners={seq[0],seq[1],seq[k],seq[k+1]} if any(bn[x] for x in seq if x not in corners):continue la=bn[seq[0]][0] if bn[seq[0]] else None lb=bn[seq[1]][0] if bn[seq[1]] else None ra=bn[seq[k]][0] if bn[seq[k]] else None rb=bn[seq[k+1]][0] if bn[seq[k+1]] else None if side_forward(la,seq[0],lb,seq[1]) and side_forward(ra,seq[k],rb,seq[k+1]): return True return False print("two cycles",cactus_gap_dp(G)) # compare atlas for n in range(1,8): mism=[];tot=0 for A in atlas: if len(A)!=n or not is_cactus(A):continue tot+=1 e=nx.algorithms.isomorphism.GraphMatcher(ladder(n),A).subgraph_is_monomorphic() d=cactus_gap_dp(A) if e!=d: mism.append((A.copy(),e,d));break print(n,tot,len(mism)) if mism: A,e,d=mism[0];print(e,d,dict(A.degree()),sorted(A.edges()),nx.cycle_basis(A));break
pythonimport random, itertools, time, math, collections def random_cactus(n, trials=100): # random tree then maybe add edges creating vertex-disjoint cycles T=nx.random_labeled_tree(n, seed=random.randrange(1<<30)) G=T.copy() used=set() # add random nonedges if path vertices unused and adding keeps deg<= maybe no constraint pairs=list(itertools.combinations(range(n),2));random.shuffle(pairs) for u,v in pairs: if G.has_edge(u,v):continue path=nx.shortest_path(G,u,v) # path in current cactus could pass cycles; require all vertices not used and all path edges bridges if any(x in used for x in path):continue if random.random()<0.08: G.add_edge(u,v);used.update
pythondef cactus_gap_dp2(G): n=len(G) if n==1:return True if max(dict(G.degree()).values())>3:return False dec=decompose_cactus(G) if dec is None:return False cycles,cyc_id,cyc_edges,bn=dec raylen={} for u,v in G.edges(): if tuple(sorted((u,v))) in cyc_edges: continue H=G.copy();H.remove_edge(u,v) for a,b in [(u,v),(v,u)]: comp=set(nx.node_connected_component(H,b)) ok=all(x not in cyc_id and G.degree(x)<=2 for x in comp) raylen[(a,b)]=len(comp) if ok else None from functools import lru_cache def vert_plans(v,p): if v in cyc_id:return [] ch=[u for u in bn[v] if u!=p] plans=[] if len(ch)==0: plans=[(0,None)] elif len(ch)==1: a=ch[0];plans.append((0,a)) L=raylen.get((v,a)) if L is not None:plans.append((L,None)) elif len(ch)==2: a,b=ch La=raylen.get((v,a));Lb=raylen.get((v,b)) if La is not None:plans.append((La,b)) if Lb is not None:plans.append((Lb,a)) return plans @lru_cache(None) def need(v,p): if v in cyc_id: C=cycles[cyc_id[v]];L=len(C) if L%2:return INF idx=C.index(v);best=INF;k=L//2 for step in (1,-1): seq=[C[(idx+step*i)%L] for i in range(L)] corners={seq[0],seq[1],seq[k],seq[k+1]} if any(bn[x] for x in seq if x not in corners):continue if set(bn[seq[0]])!={p}:continue if len(bn[seq[1]])>1:continue back=0 if bn[seq[1]]: z=bn[seq[1]][0];back=raylen.get((seq[1],z)) if back is None:continue ra=bn[seq[k]][0] if bn[seq[k]] else None rb=bn[seq[k+1]][0] if bn[seq[k+1]] else None if sf(ra,seq[k],rb,seq[k+1]):best=min(best,back+1) return best ch=[u for u in bn[v] if u!=p] if len(ch)>2:return INF if not ch:return 1 best=INF if len(ch)==1: a=ch[0];r=need(a,v) if r<INF:best=min(best,max(1,r-1)) for back,fwd in vert_plans(a,v): if fwd is None or need(fwd,a)<=1:best=min(best,back+1) else: a,b=ch for h,vert in [(a,b),(b,a)]: for back,fwd in vert_plans(vert,v): ok=need(h,v)<=1 if fwd is None else pair(h,v,fwd,vert) if ok:best=min(best,back+1) return best @lru_cache(None) def pair(v,p,w,q): if v in cyc_id or w in cyc_id:return False cv=[u for u in bn[v] if u!=p];cw=[u for u in bn[w] if u!=q] if len(cv)>1 or len(cw)>1:return False if not cv and not cw:return True if not cv:return need(cw[0],w)<=1 if not cw:return need(cv[0],v)<=1 return pair(cv[0],v,cw[0],w) def sf(a,pa,b,pb): if a is None and b is None:return True if a is None:return need(b,pb)<=1 if b is None:return need(a,pa)<=1 return pair(a,pa,b,pb) def assigns(rem): if not rem:return [(None,None)] if len(rem)==1:return [(rem[0],None),(None,rem[0])] if len(rem)==2:return [(rem[0],rem[1]),(rem[1],rem[0])] return [] for u,v in G.edges(): if tuple(sorted((u,v))) in cyc_edges or u in cyc_id or v in cyc_id:continue for lu,ru in assigns([x for x in bn[u] if x!=v]): for lv,rv in assigns([x for x in bn[v] if x!=u]): if sf(lu,u,lv,v) and sf(ru,u,rv,v):return True for C in cycles: L=len(C) if L%2:continue k=L//2 for i in range(L): seq=[C[(i+j)%L] for j in range(L)] corners={seq[0],seq[1],seq[k],seq[k+1]} if any(bn[x] for x in seq if x not in corners):continue la=bn[seq[0]][0] if bn[seq[0]] else None lb=bn[seq[1]][0] if bn[seq[1]] else None ra=bn[seq[k]][0] if bn[seq[k]] else None rb=bn[seq[k+1]][0] if bn[seq[k+1]] else None if sf(la,seq[0],lb,seq[1]) and sf(ra,seq[k],rb,seq[k+1]):return True return False for n in range(1,8): mism=[] for A in atlas: if len(A)!=n or not is_cactus(A):continue e=nx.algorithms.isomorphism.GraphMatcher(ladder(n),A).subgraph_is_monomorphic() d=cactus_gap_dp2(A) if e!=d:mism.append((A.copy(),e,d));break print(n,len(mism)) if mism: A,e,d=mism[0];print(e,d,dict(A.degree()),sorted(A.edges()),nx.cycle_basis(A));break for n in range(8,11): mism=0 for it in range(300): A=random_cactus(n) e=nx.algorithms.isomorphism.GraphMatcher(ladder(n),A).subgraph_is_monomorphic() d=cactus_gap_dp2(A) if e!=d: print("rand mismatch",n,e,d,sorted(A.edges()),nx.cycle_basis(A),dict(A.degree()));mism=1;break print("r",n,mism)
pythonfrom functools import lru_cache def solve_construct_py(G): n=len(G) if n==1:return {next(iter(G)):(0,0)} if max(dict(G.degree()).values())>3:return None dec=decompose_cactus(G) if dec is None:return None cycles,cyc_id,cyc_edges,bn=dec # use tuple key directed (from,to) ray={} for u,v in G.edges(): if tuple(sorted((u,v))) in cyc_edges:continue H=G.copy();H.remove_edge(u,v) for a,b in [(u,v),(v,u)]: comp=set(nx.node_connected_component(H,b)) ok=all(x not in cyc_id and G.degree(x)<=2 for x in comp) ray[(a,b)]=len(comp) if ok else None need_choice={} pair_choice={} def vertplans(v,p): if v in cyc_id:return [] ch=[u for u in bn[v] if u!=p] if len(ch)==0:return [(None,None,0)] # back,fwd,len if len(ch)==1: a=ch[0];out=[(None,a,0)] if ray[(v,a)] is not None:out.append((a,None,ray[(v,a)])) return out if len(ch)==2: a,b=ch;out=[] if ray[(v,a)] is not None:out.append((a,b,ray[(v,a)])) if ray[(v,b)] is not None:out.append((b,a,ray[(v,b)])) return out return [] def side(a,pa,b,pb): if a is None and b is None:return True if a is None:return need(b,pb)<=1 if b is None:return need(a,pa)<=1 return pair(a,pa,b,pb) @lru_cache(None) def need(v,p): best=INF;choice=None if v in cyc_id: C=cycles[cyc_id[v]];L=len(C) if L%2: need_choice[(v,p)]=None;return INF k=L//2;idx=C.index(v) for step in (1,-1): seq=[C[(idx+step*i)%L] for i in range(L)] corners={seq[0],seq[1],seq[k],seq[k+1]} if any(bn[x] for x in seq if x not in corners):continue if set(bn[seq[0]])!={p}:continue if len(bn[seq[1]])>1:continue back=None;bl=0 if bn[seq[1]]: back=bn[seq[1]][0];bl=ray[(seq[1],back)] if bl is None:continue ra=bn[seq[k]][0] if bn[seq[k]] else None rb=bn[seq[k+1]][0] if bn[seq[k+1]] else None if side(ra,seq[k],rb,seq[k+1]): val=bl+1 if val<best: best=val;choice=('cycle',step,back,ra,rb) need_choice[(v,p)]=choice;return best ch=[u for u in bn[v] if u!=p] if not ch: need_choice[(v,p)]=('end',);return 1 if len(ch)>2: need_choice[(v,p)]=None;return INF if len(ch)==1: a=ch[0] r=need(a,v) if r<INF: val=max(1,r-1) if val<best:best=val;choice=('horiz',a) for back,fwd,bl in vertplans(a,v): if fwd is None or need(fwd,a)<=1: val=bl+1 if val<best:best=val;choice=('vert',a,back,fwd) else: a,b=ch for h,ve in [(a,b),(b,a)]: for back,fwd,bl in vertplans(ve,v): ok=need(h,v)<=1 if fwd is None else pair(h,v,fwd,ve) if ok: val=bl+1 if val<best:best=val;choice=('branch',h,ve,back,fwd) need_choice[(v,p)]=choice;return best @lru_cache(None) def pair(v,p,w,q): if v in cyc_id or w in cyc_id: pair_choice[(v,p,w,q)]=None;return False cv=[u for u in bn[v] if u!=p] cw=[u for u in bn[w] if u!=q] if len(cv)>1 or len(cw)>1: pair_choice[(v,p,w,q)]=None;return False if not cv and not cw: pair_choice[(v,p,w,q)]=('end',);return True if not cv: ok=need(cw[0],w)<=1 pair_choice[(v,p,w,q)]=('one2',cw[0]) if ok else None return ok if not cw: ok=need(cv[0],v)<=1 pair_choice[(v,p,w,q)]=('one1',cv[0]) if ok else None return ok ok=pair(cv[0],v,cw[0],w) pair_choice[(v,p,w,q)]=('both',cv[0],cw[0]) if ok else None return ok def assigns(rem): if not rem:return [(None,None)] if len(rem)==1:return [(rem[0],None),(None,rem[0])] if len(rem)==2:return [(rem[0],rem[1]),(rem[1],rem[0])] return [] central=None for u,v in G.edges(): if tuple(sorted((u,v))) in cyc_edges or u in cyc_id or v in cyc_id:continue for lu,ru in assigns([x for x in bn[u] if x!=v]): for lv,rv in assigns([x for x in bn[v] if x!=u]): if side(lu,u,lv,v) and side(ru,u,rv,v): central=('bridge',u,v,lu,ru,lv,rv);break if central:break if central:break if not central: for cid,C in enumerate(cycles): L=len(C) if L%2:continue k=L//2 for i in range(L): seq=[C[(i+j)%L] for j in range(L)] corners={seq[0],seq[1],seq[k],seq[k+1]} if any(bn[x] for x in seq if x not in corners):continue la=bn[seq[0]][0] if bn[seq[0]] else None lb=bn[seq[1]][0] if bn[seq[1]] else None ra=bn[seq[k]][0] if bn[seq[k]] else None rb=bn[seq[k+1]][0] if bn[seq[k+1]] else None if side(la,seq[0],lb,seq[1]) and side(ra,seq[k],rb,seq[k+1]): central=('cycle',seq,la,lb,ra,rb);break if central:break if not central:return None coord={} def put(v,r,c): if v in coord: assert coord[v]==(r,c),(v,coord[v],(r,c)) coord[v]=(r,c) def place_ray(a,b,row,col,step): prev=a;cur=b while True: put(cur,row,col) nxt=[x for x in bn[cur] if x!=prev] if not nxt:break assert len(nxt)==1 prev,cur=cur,nxt[0];col+=step sys.setrecursionlimit(1000000) def pl_need(v,p,row,col,sgn,g): put(v,row,col) ch=need_choice[(v,p)] assert ch and need(v,p)<=g,(v,p,ch,need(v,p),g) typ=ch[0] if typ=='end':return if typ=='horiz': a=ch[1];pl_need(a,v,row,col+sgn,sgn,g+1);return if typ in ('vert','branch'): if typ=='vert': ve,back,fwd=ch[1:] h=None else: h,ve,back,fwd=ch[1:] rr=1-row;put(ve,rr,col) if back is not None:place_ray(ve,back,rr,col-sgn,-sgn) if h is not None: if fwd is not None:pl_pair(h,v,fwd,ve,row,col+sgn,sgn) else:pl_need(h,v,row,col+sgn,sgn,1) elif fwd is not None: pl_need(fwd,ve,rr,col+sgn,sgn,1) return if typ=='cycle': _,stepcy,back,ra,rb=ch C=cycles[cyc_id[v]];L=len(C);k=L//2;idx=C.index(v) seq=[C[(idx+stepcy*i)%L] for i in range(L)] rr=1-row for j in range(1,k+1):put(seq[j],rr,col+sgn*(j-1)) for j in range(k+1,L):put(seq[j],row,col+sgn*(L-j)) if back is not None:place_ray(seq[1],back,rr,col-sgn,-sgn) outcol=col+sgn*k # ra from seq[k] row rr; rb from seq[k+1] row if ra is not None and rb is not None: pl_pair(rb,seq[k+1],ra,seq[k],row,outcol,sgn) elif ra is not None:pl_need(ra,seq[k],rr,outcol,sgn,1) elif rb is not None:pl_need(rb,seq[k+1],row,outcol,sgn,1) return def pl_pair(v,p,w,q,row,col,sgn): put(v,row,col);put(w,1-row,col) ch=pair_choice[(v,p,w,q)] assert ch if ch[0]=='end':return if ch[0]=='one1':pl_need(ch[1],v,row,col+sgn,sgn,1) elif ch[0]=='one2':pl_need(ch[1],w,1-row,col+sgn,sgn,1) else:pl_pair(ch[1],v,ch[2],w,row,col+sgn,sgn) def pl_side(a,pa,b,pb,rowa,col,sgn): if a is not None and b is not None:pl_pair(a,pa,b,pb,rowa,col,sgn) elif a is not None:pl_need(a,pa,rowa,col,sgn,1) elif b is not None:pl_need(b,pb,1-rowa,col,sgn,1) if central[0]=='bridge': _,u,v,lu,ru,lv,rv=central put(u,0,0);put(v,1,0) pl_side(lu,u,lv,v,0,-1,-1) pl_side(ru,u,rv,v,0,1,1) else: _,seq,la,lb,ra,rb=central L=len(seq);k=L//2 put(seq[0],0,0) for j in range(1,k+1):put(seq[j],1,j-1) for j in range(k+1,L):put(seq[j],0,L-j) pl_side(la,seq[0],lb,seq[1],0,-1,-1) # right: rb belongs top seq[k+1], ra bottom seq[k] pl_side(rb,seq[k+1],ra,seq[k],0,k,1) assert len(coord)==n,(len(coord),n,central) assert len(set(coord.values()))==n,("collision",central,coord) for u,v in G.edges(): x1,y1=coord[u];x2,y2=coord[v] assert abs(x1-x2)+abs(y1-y2)==1,("edge bad",u,v,coord[u],coord[v],central) return coord # test atlas for n in range(1,8): for A in atlas: if len(A)!=n or not is_cactus(A):continue e=nx.algorithms.isomorphism.GraphMatcher(ladder(n),A).subgraph_is_monomorphic() sol=solve_construct_py(A) if e!=(sol is not None): print("mismatch construct",n,e,sorted(A.edges()),nx.cycle_basis(A));raise SystemExit print("atlas construction ok") for n in range(8,12): for it in range(500): A=random_cactus(n) e=nx.algorithms.isomorphism.GraphMatcher(ladder(n),A).subgraph_is_monomorphic() sol=solve_construct_py(A) if e!=(sol is not None): print("rand mismatch construct",n,e,sorted(A.edges()),nx.cycle_basis(A),dict(A.degree()));raise SystemExit print("random construction ok")
pythonimport inspect, textwrap try: src=inspect.getsource(solve_construct_py) print(src[:500]) except Exception as e: print(e)
pythonsrc=inspect.getsource(solve_construct_py) src2=src.replace("pl_side(rb,seq[k+1],ra,seq[k],0,outcol,sgn)", "pl_side(ra,seq[k],rb,seq[k+1],1,outcol,sgn)") src2=src2.replace("pl_side(rb,seq[k+1],ra,seq[k],0,k,1)", "pl_side(ra,seq[k],rb,seq[k+1],1,k,1)") # rename to overwrite exec(src2, globals()) print("redefined", "pl_side(ra" in inspect.getsource(solve_construct_py))
pythonexec(src2, globals()) # test atlas for n in range(1,8): for A in atlas: if len(A)!=n or not is_cactus(A): continue e=nx.algorithms.isomorphism.GraphMatcher(ladder(n),A).subgraph_is_monomorphic() sol=solve_construct_py(A) if e!=(sol is not None): print("mismatch construct",n,e,sorted(A.edges()),nx.cycle_basis(A)) raise SystemExit print("atlas construction ok") for n in range(8,12): for it in range(300): A=random_cactus(n) e=nx.algorithms.isomorphism.GraphMatcher(ladder(n),A).subgraph_is_monomorphic() sol=solve_construct_py(A) if e!=(sol is not None): print("rand mismatch construct",n,e,sorted(A.edges()),nx.cycle_basis(A),dict(A.degree())) raise SystemExit print("random construction ok")
python# Rebuild patched source from the original captured source. patched = src patched = patched.replace( "pl_side(rb,seq[k+1],ra,seq[k],0,outcol,sgn)", "pl_side(ra,seq[k],rb,seq[k+1],1,outcol,sgn)" ) patched = patched.replace( "pl_side(rb,seq[k+1],ra,seq[k],0,k,1)", "pl_side(ra,seq[k],rb,seq[k+1],1,k,1)" ) patched = patched.replace( "ch=need_choice[(v,p)]", "need(v,p)\n ch=need_choice[(v,p)]" ) patched = patched.replace( "ch=pair_choice[(v,p,w,q)]", "pair(v,p,w,q)\n ch=pair_choice[(v,p,w,q)]" ) exec(patched, globals()) for n in range(1,8): for A in atlas: if len(A)!=n or not is_cactus(A): continue e=nx.algorithms.isomorphism.GraphMatcher(ladder(n),A).subgraph_is_monomorphic() sol=solve_construct_py(A) if e!=(sol
pythonimport random, itertools, subprocess, textwrap, networkx as nx, os, json, math, collections, time def gen_random_cactus(n): T=nx.random_labeled_tree(n, seed=random.randrange(1<<30)) G=T.copy() used=set() pairs=list(itertools.combinations(range(n),2));random.shuffle(pairs) for u,v in pairs: if G.has_edge(u,v): continue # only add if current unique path all unused and no cycle vertices try: path=nx.shortest_path(G,u,v) except: continue if any(x in used for x in path): continue if random.random()<0.07: G.add_edge(u,v); used.update(path) return G cases=[] truth=[] for n in range(1,11): for _ in range(150): G=gen_random_cactus(n) if n>1 else nx.empty_graph(1) cases.append(G) host
这题不能只判断“最大度数不超过 3、所有环都是偶环”。
例如某些最大度数为 3 的树,仍然无法放进两行梯子中。真正的限制来自:沿着梯子的横坐标扫描时,每一列最多只有两个顶点,而且某一行被占用后,另一行能够向后利用的连续空位是有限的。
梯子中的任意简单环一定是一个 矩形的边界,因此:
设环按顺序为
若 是左侧竖边,则右侧竖边为
把尚未绘制的部分向右展开。
need(p -> v)边 已经被水平放置, 位于当前列;另一行当前为空。
定义:
表示为了完成 一侧的整个连通分量,另一行在当前位置之前至少需要多少个连续空单元格。
如果只有一个后继并继续水平前进,空白长度增加 1,因此:
在分叉处:
如果遇到环,则当前顶点必须是环的一个角。枚举环的两个方向,并检查:
pair(p -> v, q -> w)当前列上下两格都已经被 占用。
此时两顶点都不能:
因此每条轨道最多只能水平继续一个顶点:
当一个分支向后放入另一行时,它所在的另一侧行已经被主干占用,因此它只能沿唯一的一行一直延伸。
所以该方向上的整个连通分量必须:
将每个环收缩成一个结点后,所有桥组成一棵树。通过树上子树统计,可以在线性时间内判断每条有向桥的一侧是否是这种纯路径,并得到路径长度。
任何可行布局都可以选择一条竖边作为中心:
中心竖边有两种情况。
桥的两个端点不能位于环上。把两个端点剩余的边分别分配到左、右两侧,每侧最多有上下两个分量,然后使用单轨或双轨状态检查。
枚举环上的每条边作为左侧竖边,另一条对边自动确定。检查四个角向左右延伸的分量。
所有状态都会记住实现最优值的具体选择,最后反向构造坐标。
每条有向桥状态只计算一次,双轨状态沿确定的桥链推进。
空间复杂度同样为:
cpp#include <bits/stdc++.h> using namespace std; static const int INF = 1e9; struct Adj { int to, id; }; struct NeedChoice { enum Type { FAIL, END, HORIZ, VERT, BRANCH, CYCLE } type = FAIL; int nextDir = -1; int hDir = -1; int vertDir = -1; int backDir = -1; int fwdDir = -1; int cycStep = 0; }; struct PairChoice { enum Type { PFAIL, PEND, ONE1, ONE2, BOTH } type = PFAIL; int d1 = -1; int d2 = -1; }; struct PairData { unsigned char status = 0; bool value = false; PairChoice choice; }; struct State { int type; // 0 = need, 1 = pair int a, b; }; struct Task { int type; // 0 = need, 1 = pair int d1, d2; int row; int col; int sign; int gap; }; struct CentralChoice { int type = -1; // 0 = bridge, 1 = cycle int edge = -1; int lu = -1, ru = -1; int lv = -1, rv = -1; int cycle = -1; int start = -1; }; class Solver { public: int n, m; vector<int> edgeU, edgeV; vector<vector<Adj>> graph; vector<vector<Adj>> bridgeGraph; vector<char> isCycleEdge; vector<vector<int>> cycles; vector<int> cycleId; vector<int> cyclePos; vector<int> cycleAttachmentCount; vector<int> componentId; vector<vector<pair<int, int>>> componentTree; vector<int> componentSize; vector<int> componentCycles; vector<int> componentDegreeThree; vector<int> componentParent; vector<int> componentParentEdge; vector<int> componentOrder; vector<int> subtreeSize; vector<int> subtreeCycles; vector<int> subtreeDegreeThree; // rayLength[d] != -1 iff the component in direction d // is a path whose attachment vertex is an endpoint. vector<int> rayLength; vector<int> needValue; vector<unsigned char> needStatus; vector<NeedChoice> needChoice; unordered_map<unsigned long long, PairData> pairMemo; vector<int> answerX; vector<int> answerY; CentralChoice central; Solver(int n, int m) : n(n), m(m) { edgeU.resize(m); edgeV.resize(m); graph.assign(n, {}); bridgeGraph.assign(n, {}); isCycleEdge.assign(m, false); cycleId.assign(n, -1); cyclePos.assign(n, -1); rayLength.assign(2 * m, -1); needValue.assign(2 * m, -1); needStatus.assign(2 * m, 0); needChoice.resize(2 * m); answerX.assign(n, -1); answerY.assign(n, INT_MAX); pairMemo.reserve(max(16, 8 * n)); pairMemo.max_load_factor(0.7f); } void addEdge(int id, int u, int v) { edgeU[id] = u; edgeV[id] = v; graph[u].push_back({v, id}); graph[v].push_back({u, id}); } int directionId(int edge, int from) const { return edgeU[edge] == from ? 2 * edge : 2 * edge + 1; } int directionFrom(int direction) const { int edge = direction / 2; return direction % 2 == 0 ? edgeU[edge] : edgeV[edge]; } int directionTo(int direction) const { int edge = direction / 2; return direction % 2 == 0 ? edgeV[edge] : edgeU[edge]; } unsigned long long pairKey(int d1, int d2) const { return (static_cast<unsigned long long>( static_cast<unsigned int>(d1) ) << 32) | static_cast<unsigned int>(d2); } bool findCycles() { vector<int> state(n, 0); vector<int> parent(n, -1); vector<int> parentEdge(n, -1); vector<int> depth(n, 0); vector<int> iteratorIndex(n, 0); for (int root = 0; root < n; ++root) { if (state[root] != 0) { continue; } vector<int> stack; stack.push_back(root); state[root] = 1; while (!stack.empty()) { int vertex = stack.back(); if (iteratorIndex[vertex] == static_cast<int>(graph[vertex].size())) { state[vertex] = 2; stack.pop_back(); continue; } Adj edge = graph[vertex][iteratorIndex[vertex]++]; if (edge.id == parentEdge[vertex]) { continue; } int to = edge.to; if (state[to] == 0) { parent[to] = vertex; parentEdge[to] = edge.id; depth[to] = depth[vertex] + 1; state[to] = 1; stack.push_back(to); } else if (state[to] == 1 && depth[to] < depth[vertex]) { vector<int> cycle; int current = vertex; while (current != to) { cycle.push_back(current); current = parent[current]; } cycle.push_back(to); reverse(cycle.begin(), cycle.end()); int id = static_cast<int>(cycles.size()); for (int i = 0; i < static_cast<int>(cycle.size()); ++i) { int v = cycle[i]; if (cycleId[v] != -1) { return false; } cycleId[v] = id; cyclePos[v] = i; } current = vertex; while (current != to) { isCycleEdge[parentEdge[current]] = true; current = parent[current]; } isCycleEdge[edge.id] = true; cycles.push_back(move(cycle)); } } } return true; } void buildBridgeGraph() { for (int edge = 0; edge < m; ++edge) { if (isCycleEdge[edge]) { continue; } int u = edgeU[edge]; int v = edgeV[edge]; bridgeGraph[u].push_back({v, edge}); bridgeGraph[v].push_back({u, edge}); } cycleAttachmentCount.assign(cycles.size(), 0); for (int id = 0; id < static_cast<int>(cycles.size()); ++id) { int count = 0; for (int vertex : cycles[id]) { if (!bridgeGraph[vertex].empty()) { ++count; } } cycleAttachmentCount[id] = count; } } void buildComponentTree() { componentId.assign(n, -1); componentSize.clear(); componentCycles.clear(); componentDegreeThree.clear(); for (int id = 0; id < static_cast<int>(cycles.size()); ++id) { int component = static_cast<int>(componentSize.size()); int degreeThree = 0; for (int vertex : cycles[id]) { componentId[vertex] = component; if (graph[vertex].size() == 3) { ++degreeThree; } } componentSize.push_back(cycles[id].size()); componentCycles.push_back(1); componentDegreeThree.push_back(degreeThree); } for (int vertex = 0; vertex < n; ++vertex) { if (componentId[vertex] != -1) { continue; } int component = static_cast<int>(componentSize.size()); componentId[vertex] = component; componentSize.push_back(1); componentCycles.push_back(0); componentDegreeThree.push_back( graph[vertex].size() == 3 ? 1 : 0 ); } int components = componentSize.size(); componentTree.assign(components, {}); for (int edge = 0; edge < m; ++edge) { if (isCycleEdge[edge]) { continue; } int left = componentId[edgeU[edge]]; int right = componentId[edgeV[edge]]; componentTree[left].push_back({right, edge}); componentTree[right].push_back({left, edge}); } componentParent.assign(components, -1); componentParentEdge.assign(components, -1); componentOrder.clear(); componentOrder.reserve(components); vector<int> stack = {0}; componentParent[0] = -2; while (!stack.empty()) { int vertex = stack.back(); stack.pop_back(); componentOrder.push_back(vertex); for (auto [to, edge] : componentTree[vertex]) { if (to == componentParent[vertex]) { continue; } componentParent[to] = vertex; componentParentEdge[to] = edge; stack.push_back(to); } } subtreeSize = componentSize; subtreeCycles = componentCycles; subtreeDegreeThree = componentDegreeThree; for (int i = components - 1; i > 0; --i) { int vertex = componentOrder[i]; int parent = componentParent[vertex]; subtreeSize[parent] += subtreeSize[vertex]; subtreeCycles[parent] += subtreeCycles[vertex]; subtreeDegreeThree[parent] += subtreeDegreeThree[vertex]; } int totalSize = subtreeSize[0]; int totalCycles = subtreeCycles[0]; int totalDegreeThree = subtreeDegreeThree[0]; auto setRay = [&](int from, int edge, int size, int cycleCount, int degreeThreeCount) { int direction = directionId(edge, from); if (cycleCount == 0 && degreeThreeCount == 0) { rayLength[direction] = size; } }; for (int edge = 0; edge < m; ++edge) { if (isCycleEdge[edge]) { continue; } int u = edgeU[edge]; int v = edgeV[edge]; int componentU = componentId[u]; int componentV = componentId[v]; if (componentParent[componentV] == componentU) { setRay( u, edge, subtreeSize[componentV], subtreeCycles[componentV], subtreeDegreeThree[componentV] ); setRay( v, edge, totalSize - subtreeSize[componentV], totalCycles - subtreeCycles[componentV], totalDegreeThree - subtreeDegreeThree[componentV] ); } else { setRay( v, edge, subtreeSize[componentU], subtreeCycles[componentU], subtreeDegreeThree[componentU] ); setRay( u, edge, totalSize - subtreeSize[componentU], totalCycles - subtreeCycles[componentU], totalDegreeThree - subtreeDegreeThree[componentU] ); } } } vector<int> childrenDirections(int vertex, int parent) const { vector<int> result; for (Adj edge : bridgeGraph[vertex]) { if (edge.to != parent) { result.push_back(directionId(edge.id, vertex)); } } return result; } struct VerticalPlan { int back = -1; int forward = -1; int length = 0; }; vector<VerticalPlan> getVerticalPlans(int verticalDirection) const { int parent = directionFrom(verticalDirection); int vertex = directionTo(verticalDirection); if (cycleId[vertex] != -1) { return {}; } vector<int> children = childrenDirections(vertex, parent); vector<VerticalPlan> result; if (children.empty()) { result.push_back({-1, -1, 0}); } else if (children.size() == 1) { result.push_back({-1, children[0], 0}); if (rayLength[children[0]] != -1) { result.push_back({ children[0], -1, rayLength[children[0]] }); } } else if (children.size() == 2) { if (rayLength[children[0]] != -1) { result.push_back({ children[0], children[1], rayLength[children[0]] }); } if (rayLength[children[1]] != -1) { result.push_back({ children[1], children[0], rayLength[children[1]] }); } } return result; } int getCycleVertex(int id, int position, int step, int offset) const { int length = cycles[id].size(); long long index = position + 1LL * step * offset; index %= length; if (index < 0) { index += length; } return cycles[id][index]; } bool getCycleOrientation( int id, int position, int step, int incomingParent, int& c0, int& c1, int& ck, int& ck1, int& backDirection, int& bottomOutput, int& topOutput, bool hasIncoming ) const { int length = cycles[id].size(); if (length % 2 != 0) { return false; } int half = length / 2; c0 = getCycleVertex(id, position, step, 0); c1 = getCycleVertex(id, position, step, 1); ck = getCycleVertex(id, position, step, half); ck1 = getCycleVertex(id, position, step, half + 1); int attachments = static_cast<int>(!bridgeGraph[c0].empty()) + static_cast<int>(!bridgeGraph[c1].empty()) + static_cast<int>(!bridgeGraph[ck].empty()) + static_cast<int>(!bridgeGraph[ck1].empty()); if (attachments != cycleAttachmentCount[id]) { return false; } if (hasIncoming) { if (bridgeGraph[c0].size() != 1 || bridgeGraph[c0][0].to != incomingParent) { return false; } } backDirection = -1; bottomOutput = -1; topOutput = -1; if (!bridgeGraph[c1].empty()) { backDirection = directionId(bridgeGraph[c1][0].id, c1); if (rayLength[backDirection] == -1) { return false; } } if (!bridgeGraph[ck].empty()) { bottomOutput = directionId(bridgeGraph[ck][0].id, ck); } if (!bridgeGraph[ck1].empty()) { topOutput = directionId(bridgeGraph[ck1][0].id, ck1); } return true; } int getStatus(const State& state) { if (state.type == 0) { return needStatus[state.a]; } return pairMemo[pairKey(state.a, state.b)].status; } void setStatus(const State& state, int value) { if (state.type == 0) { needStatus[state.a] = static_cast<unsigned char>(value); } else { pairMemo[pairKey(state.a, state.b)].status = static_cast<unsigned char>(value); } } void addSideDependency(vector<State>& dependencies, int d1, int d2) { if (d1 == -1 && d2 == -1) { return; } if (d1 == -1) { dependencies.push_back({0, d2, -1}); } else if (d2 == -1) { dependencies.push_back({0, d1, -1}); } else { dependencies.push_back({1, d1, d2}); } } vector<State> collectDependencies(const State& state) { vector<State> dependencies; if (state.type == 1) { int d1 = state.a; int d2 = state.b; int v = directionTo(d1); int p = directionFrom(d1); int w = directionTo(d2); int q = directionFrom(d2); if (cycleId[v] != -1 || cycleId[w] != -1) { return dependencies; } vector<int> first = childrenDirections(v, p); vector<int> second = childrenDirections(w, q); if (first.size() > 1 || second.size() > 1) { return dependencies; } if (first.empty() && second.empty()) { return dependencies; } if (first.empty()) { dependencies.push_back({0, second[0], -1}); } else if (second.empty()) { dependencies.push_back({0, first[0], -1}); } else { dependencies.push_back({ 1, first[0], second[0] }); } return dependencies; } int direction = state.a; int parent = directionFrom(direction); int vertex = directionTo(direction); if (cycleId[vertex] != -1) { int id = cycleId[vertex]; int position = cyclePos[vertex]; for (int step : {1, -1}) { int c0, c1, ck, ck1; int back, bottom, top; if (getCycleOrientation( id, position, step, parent, c0, c1, ck, ck1, back, bottom, top, true )) { addSideDependency( dependencies, bottom, top ); } } return dependencies; } vector<int> children = childrenDirections(vertex, parent); if (children.size() == 1) { dependencies.push_back({ 0, children[0], -1 }); for (VerticalPlan plan : getVerticalPlans(children[0])) { if (plan.forward != -1) { dependencies.push_back({ 0, plan.forward, -1 }); } } } else if (children.size() == 2) { for (int choice = 0; choice < 2; ++choice) { int horizontal = children[choice]; int vertical = children[choice ^ 1]; for (VerticalPlan plan : getVerticalPlans(vertical)) { if (plan.forward == -1) { dependencies.push_back({ 0, horizontal, -1 }); } else { dependencies.push_back({ 1, horizontal, plan.forward }); } } } } return dependencies; } bool sideAlreadyCalculated(int d1, int d2) { if (d1 == -1 && d2 == -1) { return true; } if (d1 == -1) { return needValue[d2] <= 1; } if (d2 == -1) { return needValue[d1] <= 1; } return pairMemo[pairKey(d1, d2)].value; } void calculatePair(int d1, int d2) { PairData& data = pairMemo[pairKey(d1, d2)]; data.value = false; data.choice = PairChoice(); int v = directionTo(d1); int p = directionFrom(d1); int w = directionTo(d2); int q = directionFrom(d2); if (cycleId[v] != -1 || cycleId[w] != -1) { return; } vector<int> first = childrenDirections(v, p); vector<int> second = childrenDirections(w, q); if (first.size() > 1 || second.size() > 1) { return; } if (first.empty() && second.empty()) { data.value = true; data.choice.type = PairChoice::PEND; return; } if (first.empty()) { if (needValue[second[0]] <= 1) { data.value = true; data.choice.type = PairChoice::ONE2; data.choice.d1 = second[0]; } return; } if (second.empty()) { if (needValue[first[0]] <= 1) { data.value = true; data.choice.type = PairChoice::ONE1; data.choice.d1 = first[0]; } return; } PairData& next = pairMemo[pairKey(first[0], second[0])]; if (next.value) { data.value = true; data.choice.type = PairChoice::BOTH; data.choice.d1 = first[0]; data.choice.d2 = second[0]; } } void calculateNeed(int direction) { int parent = directionFrom(direction); int vertex = directionTo(direction); int best = INF; NeedChoice bestChoice; auto consider = [&](int value, const NeedChoice& choice) { if (value < best) { best = value; bestChoice = choice; } }; if (cycleId[vertex] != -1) { int id = cycleId[vertex]; int position = cyclePos[vertex]; for (int step : {1, -1}) { int c0, c1, ck, ck1; int back, bottom, top; if (!getCycleOrientation( id, position, step, parent, c0, c1, ck, ck1, back, bottom, top, true )) { continue; } if (!sideAlreadyCalculated(bottom, top)) { continue; } NeedChoice choice; choice.type = NeedChoice::CYCLE; choice.cycStep = step; int backwardLength = back == -1 ? 0 : rayLength[back]; consider(backwardLength + 1, choice); } needValue[direction] = best; needChoice[direction] = bestChoice; return; } vector<int> children = childrenDirections(vertex, parent); if (children.empty()) { best = 1; bestChoice.type = NeedChoice::END; } else if (children.size() == 1) { int child = children[0]; if (needValue[child] < INF) { NeedChoice choice; choice.type = NeedChoice::HORIZ; choice.nextDir = child; consider( max(1, needValue[child] - 1), choice ); } for (VerticalPlan plan : getVerticalPlans(child)) { if (plan.forward != -1 && needValue[plan.forward] > 1) { continue; } NeedChoice choice; choice.type = NeedChoice::VERT; choice.vertDir = child; choice.backDir = plan.back; choice.fwdDir = plan.forward; consider(plan.length + 1, choice); } } else if (children.size() == 2) { for (int selected = 0; selected < 2; ++selected) { int horizontal = children[selected]; int vertical = children[selected ^ 1]; for (VerticalPlan plan : getVerticalPlans(vertical)) { bool possible; if (plan.forward == -1) { possible = needValue[horizontal] <= 1; } else { possible = pairMemo[ pairKey( horizontal, plan.forward ) ].value; } if (!possible) { continue; } NeedChoice choice; choice.type = NeedChoice::BRANCH; choice.hDir = horizontal; choice.vertDir = vertical; choice.backDir = plan.back; choice.fwdDir = plan.forward; consider(plan.length + 1, choice); } } } needValue[direction] = best; needChoice[direction] = bestChoice; } void ensureCalculated(State root) { vector<State> stack; stack.push_back(root); while (!stack.empty()) { State state = stack.back(); int status = getStatus(state); if (status == 2) { stack.pop_back(); continue; } if (status == 0) { setStatus(state, 1); } vector<State> dependencies = collectDependencies(state); bool pushed = false; for (State dependency : dependencies) { if (getStatus(dependency) == 0) { stack.push_back(dependency); pushed = true; break; } } if (pushed) { continue; } if (state.type == 0) { calculateNeed(state.a); } else { calculatePair(state.a, state.b); } setStatus(state, 2); stack.pop_back(); } } bool oneSidePossible(int direction) { ensureCalculated({0, direction, -1}); return needValue[direction] <= 1; } bool pairPossible(int d1, int d2) { ensureCalculated({1, d1, d2}); return pairMemo[pairKey(d1, d2)].value; } bool sidePossible(int d1, int d2) { if (d1 == -1 && d2 == -1) { return true; } if (d1 == -1) { return oneSidePossible(d2); } if (d2 == -1) { return oneSidePossible(d1); } return pairPossible(d1, d2); } vector<pair<int, int>> getAssignments(const vector<int>& directions) const { if (directions.empty()) { return {{-1, -1}}; } if (directions.size() == 1) { return { {directions[0], -1}, {-1, directions[0]} }; } if (directions.size() == 2) { return { {directions[0], directions[1]}, {directions[1], directions[0]} }; } return {}; } int onlyBridgeDirection(int vertex) const { if (bridgeGraph[vertex].empty()) { return -1; } return directionId( bridgeGraph[vertex][0].id, vertex ); } bool findCentralObject() { for (int edge = 0; edge < m; ++edge) { if (isCycleEdge[edge]) { continue; } int u = edgeU[edge]; int v = edgeV[edge]; if (cycleId[u] != -1 || cycleId[v] != -1) { continue; } vector<int> directionsU; vector<int> directionsV; for (Adj adjacent : bridgeGraph[u]) { if (adjacent.id != edge) { directionsU.push_back( directionId(adjacent.id, u) ); } } for (Adj adjacent : bridgeGraph[v]) { if (adjacent.id != edge) { directionsV.push_back( directionId(adjacent.id, v) ); } } for (auto [leftU, rightU] : getAssignments(directionsU)) { for (auto [leftV, rightV] : getAssignments(directionsV)) { if (sidePossible(leftU, leftV) && sidePossible(rightU, rightV)) { central.type = 0; central.edge = edge; central.lu = leftU; central.ru = rightU; central.lv = leftV; central.rv = rightV; return true; } } } } for (int id = 0; id < static_cast<int>(cycles.size()); ++id) { int length = cycles[id].size(); if (length % 2 != 0) { continue; } int half = length / 2; for (int start = 0; start < length; ++start) { int c0 = cycles[id][start]; int c1 = cycles[id][(start + 1) % length]; int ck = cycles[id][(start + half) % length]; int ck1 = cycles[id][(start + half + 1) % length]; int attachments = static_cast<int>( !bridgeGraph[c0].empty() ) + static_cast<int>( !bridgeGraph[c1].empty() ) + static_cast<int>( !bridgeGraph[ck].empty() ) + static_cast<int>( !bridgeGraph[ck1].empty() ); if (attachments != cycleAttachmentCount[id]) { continue; } int d0 = onlyBridgeDirection(c0); int d1 = onlyBridgeDirection(c1); int dk = onlyBridgeDirection(ck); int dk1 = onlyBridgeDirection(ck1); if (sidePossible(d0, d1) && sidePossible(dk, dk1)) { central.type = 1; central.cycle = id; central.start = start; return true; } } } return false; } void put(int vertex, int row, int column) { if (answerY[vertex] != INT_MAX) { if (answerX[vertex] != row || answerY[vertex] != column) { throw runtime_error( "Inconsistent placement" ); } return; } answerX[vertex] = row; answerY[vertex] = column; } void placeRay(int direction, int row, int column, int step) { int previous = directionFrom(direction); int current = directionTo(direction); while (true) { put(current, row, column); int next = -1; for (Adj edge : bridgeGraph[current]) { if (edge.to == previous) { continue; } if (next != -1) { throw runtime_error("Invalid ray"); } next = edge.to; } if (next == -1) { break; } previous = current; current = next; column += step; } } void pushSide(vector<Task>& tasks, int d1, int d2, int firstRow, int column, int sign) { if (d1 == -1 && d2 == -1) { return; } if (d1 == -1) { tasks.push_back({ 0, d2, -1, 1 - firstRow, column, sign, 1 }); } else if (d2 == -1) { tasks.push_back({ 0, d1, -1, firstRow, column, sign, 1 }); } else { tasks.push_back({ 1, d1, d2, firstRow, column, sign, 0 }); } } void construct() { vector<Task> tasks; if (central.type == 0) { int edge = central.edge; int u = edgeU[edge]; int v = edgeV[edge]; put(u, 0, 0); put(v, 1, 0); pushSide( tasks, central.lu, central.lv, 0, -1, -1 ); pushSide( tasks, central.ru, central.rv, 0, 1, 1 ); } else { int id = central.cycle; int length = cycles[id].size(); int half = length / 2; int start = central.start; auto at = [&](int offset) { return cycles[id][ (start + offset) % length ]; }; put(at(0), 0, 0); for (int i = 1; i <= half; ++i) { put(at(i), 1, i - 1); } for (int i = half + 1; i < length; ++i) { put(at(i), 0, length - i); } int d0 = onlyBridgeDirection(at(0)); int d1 = onlyBridgeDirection(at(1)); int dk = onlyBridgeDirection(at(half)); int dk1 = onlyBridgeDirection(at(half + 1)); pushSide(tasks, d0, d1, 0, -1, -1); pushSide(tasks, dk, dk1, 1, half, 1); } while (!tasks.empty()) { Task task = tasks.back(); tasks.pop_back(); if (task.type == 1) { int first = task.d1; int second = task.d2; int v = directionTo(first); int w = directionTo(second); put(v, task.row, task.col); put(w, 1 - task.row, task.col); PairChoice choice = pairMemo[ pairKey(first, second) ].choice; if (choice.type == PairChoice::PEND) { continue; } if (choice.type == PairChoice::ONE1) { tasks.push_back({ 0, choice.d1, -1, task.row, task.col + task.sign, task.sign, 1 }); } else if ( choice.type == PairChoice::ONE2 ) { tasks.push_back({ 0, choice.d1, -1, 1 - task.row, task.col + task.sign, task.sign, 1 }); } else if ( choice.type == PairChoice::BOTH ) { tasks.push_back({ 1, choice.d1, choice.d2, task.row, task.col + task.sign, task.sign, 0 }); } else { throw runtime_error( "Invalid pair choice" ); } continue; } int direction = task.d1; int parent = directionFrom(direction); int vertex = directionTo(direction); put(vertex, task.row, task.col); NeedChoice choice = needChoice[direction]; if (needValue[direction] > task.gap) { throw runtime_error( "Insufficient free space" ); } if (choice.type == NeedChoice::END) { continue; } if (choice.type == NeedChoice::HORIZ) { tasks.push_back({ 0, choice.nextDir, -1, task.row, task.col + task.sign, task.sign, task.gap + 1 }); continue; } if (choice.type == NeedChoice::VERT || choice.type == NeedChoice::BRANCH) { int vertical = choice.vertDir; int verticalVertex = directionTo(vertical); int otherRow = 1 - task.row; put( verticalVertex, otherRow, task.col ); if (choice.backDir != -1) { placeRay( choice.backDir, otherRow, task.col - task.sign, -task.sign ); } if (choice.type == NeedChoice::BRANCH) { if (choice.fwdDir != -1) { tasks.push_back({ 1, choice.hDir, choice.fwdDir, task.row, task.col + task.sign, task.sign, 0 }); } else { tasks.push_back({ 0, choice.hDir, -1, task.row, task.col + task.sign, task.sign, 1 }); } } else if (choice.fwdDir != -1) { tasks.push_back({ 0, choice.fwdDir, -1, otherRow, task.col + task.sign, task.sign, 1 }); } continue; } if (choice.type == NeedChoice::CYCLE) { int id = cycleId[vertex]; int position = cyclePos[vertex]; int length = cycles[id].size(); int half = length / 2; int step = choice.cycStep; int otherRow = 1 - task.row; auto at = [&](int offset) { return getCycleVertex( id, position, step, offset ); }; for (int i = 1; i <= half; ++i) { put( at(i), otherRow, task.col + task.sign * (i - 1) ); } for (int i = half + 1; i < length; ++i) { put( at(i), task.row, task.col + task.sign * (length - i) ); } int back = -1; int bottom = -1; int top = -1; if (!bridgeGraph[at(1)].empty()) { back = directionId( bridgeGraph[at(1)][0].id, at(1) ); } if (!bridgeGraph[at(half)].empty()) { bottom = directionId( bridgeGraph[at(half)][0].id, at(half) ); } if (!bridgeGraph[ at(half + 1) ].empty()) { top = directionId( bridgeGraph[ at(half + 1) ][0].id, at(half + 1) ); } if (back != -1) { placeRay( back, otherRow, task.col - task.sign, -task.sign ); } pushSide( tasks, bottom, top, otherRow, task.col + task.sign * half, task.sign ); continue; } throw runtime_error("Invalid need choice"); } } bool solve() { if (n == 1) { answerX[0] = 0; answerY[0] = 0; return true; } for (int vertex = 0; vertex < n; ++vertex) { if (graph[vertex].size() > 3) { return false; } } if (!findCycles()) { return false; } buildBridgeGraph(); buildComponentTree(); if (!findCentralObject()) { return false; } construct(); vector<pair<int, int>> occupied; occupied.reserve(n); for (int vertex = 0; vertex < n; ++vertex) { if (answerY[vertex] == INT_MAX) { return false; } if (answerY[vertex] < -200000 || answerY[vertex] > 200000) { return false; } occupied.push_back({ answerX[vertex], answerY[vertex] }); } sort(occupied.begin(), occupied.end()); if (adjacent_find( occupied.begin(), occupied.end() ) != occupied.end()) { return false; } for (int edge = 0; edge < m; ++edge) { int u = edgeU[edge]; int v = edgeV[edge]; int distance = abs(answerX[u] - answerX[v]) + abs(answerY[u] - answerY[v]); if (distance != 1) { return false; } } return true; } }; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int testCases; cin >> testCases; while (testCases--) { int n, m; cin >> n >> m; Solver solver(n, m); for (int edge = 0; edge < m; ++edge) { int u, v; cin >> u >> v; --u; --v; solver.addEdge(edge, u, v); } bool possible = false; try { possible = solver.solve(); } catch (...) { possible = false; } if (!possible) { cout << "No\n"; continue; } cout << "Yes\n"; for (int vertex = 0; vertex < n; ++vertex) { cout << solver.answerX[vertex] << ' ' << solver.answerY[vertex] << '\n'; } } return 0; }