【学习笔记】优化建图相关(线段树优化,倍增优化)
优化建图
发现并没有人写得很详细的样子,那我也摆烂好惹
前言
众所周知,连边的时间复杂度一般是 ,但,当连边的对象是一个连续的树上区间的时候,我们或许有更优的连边方式:优化建图。
前置知识:
-
树链剖分
-
线段树
-
树上倍增
-
Dijkstra
线段树优化建图
线段树优化建图,借助线段树这种数据结构,达到“以空间换时间”的目的,往往与树链剖分一起使用。
线段树的边分两类:
-
向上连的边,表示连出。
-
向下连的边,表示连入。
为什么这样规定?
如图。
单点连区间
我觉得上图讲的还是一目了然的,就是这样连啦。
当然也不一定是连到外面:
(注意,这个图是我搬的,跟区间连区间以及下面的例题无关。)
在本图中,这样的连边就代表着连 到 的边。
区间连区间
建最开始的那个图的两个线段树,并建立一个虚点,上连线段树的区间连向虚点,虚点连向下连线段树。
(一定要读完这句话再去看下面的题啊……不是两个线段树直接相连的……)
最后,线段树优化建图,点数是 ,边数是 ,带入即可计算时间复杂度。
例题
[JOISC 2022 Day1] 监狱
题目背景
JOISC 2022 D1T1
题目描述
在 JOI 王国,安保最严格的地方就是 IOI 监狱。IOI 监狱中有 个房间,以 编号。其中有 条通道。第 条通道双向地连接房间 和 。任意两个房间都可以相互到达。
IOI 监狱中有 个囚犯,以 编号。第 个囚犯的卧室和工作室分别是房间 。一个囚犯可能在另一个囚犯的卧室工作。然而,每个房间最多成为一个囚犯的卧室,一个囚犯的工作室。
一天早上,这 个囚犯需要从他们的卧室移动到他们的工作室。典狱长 APIO 先生需要按如下方式指示囚犯移动:
- 指令:选择一个囚犯,然后命令他从当前所在的房间移动到一个与该房间有直接连边的房间。为了避免囚犯交流,不允许将囚犯移动到有其他囚犯在的房间。
为了尽早开始工作,APIO 先生想知道,是否存在一种给出任意条指令的方案使得每个囚犯以最短路径从卧室到达工作室。
请编写一个程序,在给定如上房间、通道和罪犯的所有信息后判断是否存在满足条件的方案。
输入格式
每个测试数据包含多组测试用例。
第一行一个整数 ,表示这个测试数据包含 组测试用例。
对于每组测试用例:
第一行一个整数 ,表示房间个数。
接下来 行每行两个整数 表示通道连接房间的编号。
接下来一行一个整数 表示囚犯个数。
接下来 行,每行两个整数 表示囚犯的卧室和工作室。
输出格式
输出 行,第 行表示对于第 组测试用例,如果存在一种满足题意的方案,输出 Yes
,否则输出 No
。
样例 #1
样例输入 #1
1 8 1 2 2 3 3 4 4 5 5 6 6 7 7 8 2 3 4 4 8
样例输出 #1
Yes
样例 #2
样例输入 #2
2 7 1 2 2 3 3 4 4 5 3 6 6 7 2 4 1 5 7 4 1 2 1 3 1 4 3 2 3 3 4 4 2
样例输出 #2
Yes No
提示
【样例解释 #1】
可以通过发送如下指令完成任务:
- 让囚犯 从 号房间移动到 号房间。
- 让囚犯 从 号房间移动到 号房间。
- 让囚犯 从 号房间移动到 号房间。
- 让囚犯 从 号房间移动到 号房间。
- 让囚犯 从 号房间移动到 号房间。
这组样例满足所有子任务的限制。
【样例解释 #2】
这组样例满足子任务 的限制。
【数据范围】
对于所有数据,满足:
- 。
- 。
- 。
- 。
- 。
- 互不相同。
- 互不相同。
- 。
- 任意两个房间之间可以通过给定道路互相到达。
- 对于所有测试用例, 的总和不超过 。
详细子任务附加限制及分值如下表所示:
子任务编号 | 附加限制 | 分值 |
---|---|---|
任意两个房间之间都可以通过不超过 条道路到达。 | ||
无附加限制 |
解题:
题意概括:
对于 个点的树,有 条起点与终点各不相同的行进路线形如 ,允许从某个点移动至相邻点,问能否在不存在某个点所在人数 的情况下完成所有行进路线。
首先有一个特别性质:
如果合法,一定有一种合法的方案使得每个人都是不停留地从头走到尾。
如果 走到道路一半需要等待 走完自己再走,那么我们自然也可以让 先走完,自己再走。
因此就有两限制:
-
起点在 时, 点先走。
-
终点在 时, 点先走。
而当存在冲突时,我们就输出 No
。
因此我们可以规定将 个行进路线是 个点, 先于 走是 向 连边,而存在冲突则是出现环。
需要判断一个图是否是有向无环图,拓扑排序即可。
考虑优化:
最主要的时间复杂度在于建图,就是判断上面的两条性质,对于每个点判断两条性质的时间复杂度是 ,我们考虑让一个行进计划一次性连好边:
-
复制原图的树为两棵新树 ,其中 是上行树, 是下行树。
-
将第 个行进计划在 上的对应点全都向 连边,表示起点在这些点的行进计划要先于 走。
-
将 向第 个行进计划在 上的所有对应点连边,表示终点在这些点的行进计划要晚于 走。
-
另外,我们要单独把起点和终点拿出来,让第 个行进计划连向起点,让终点连向第 个行进计划,其余的区间刨去起点终点,上行树连向第 个行进计划,第 个下行树,这样我们就有了“起点被包含的情况”,就可能出环了。
为什么这样就成环了?
( 是两个行进计划, 则是计划 的起点与终点在线段树中的叶子节点, 是刨去了起点终点后的区间,本图表示,第 个行进计划的起点与终点均被第 个包含,不合法)
因此可以进行树链剖分+线段树优化建图。
时间复杂度 。
(tips:对于普通 dfs 树上建边和各个行进计划之间的边,最好开两个结构体存边或者开一个容器开一个结构体,总之不要放在一起存,否则会冲突)
Miku's Code
#include<bits/stdc++.h> using namespace std; #define il inline #define rg register int #define next Miku typedef long double llf; typedef long long ll; typedef pair<int,int> PII; const double eps=1e-8; namespace io{ // char in[1<<20],*p1=in,*p2=in; // #define getchar() (p1==p2&&(p2=(p1=in)+fread(in,1,1<<20,stdin),p1==p2)?EOF:*p1++) il int read(){ char c=getchar(); int x=0,f=1; while(c<48)<%if(c=='-')f=-1;c=getchar();%> while(c>47)x=(x*10)+(c^48),c=getchar(); return x*f; } il void write(int x){ if(x<0)<%putchar('-');x=~x+1;%> if(x>9) write(x/10); putchar(x%10+'0'); } il int ins(char *str){ int len=0; while(1){ char c=getchar(); if(c!='\n' && c!='\0' && c!='\r') str[++len]=c; else break; } return len; } } namespace mystd{ il int Max(int a,int b)<%if(a<b) return b;return a; %> il int Min(int a,int b)<%if(a>b) return b;return a; %> il int Abs(int a)<% if(a<0) return a*(-1);return a; %> il double fMax(double a,double b)<%if(a<b) return b;return a; %> il double fMin(double a,double b)<%if(a>b) return b;return a; %> il double fAbs(double a)<% if(a<0) return a*(-1);return a; %> il int dcmp(double a){ if(a<-eps) return -1; if(a>eps) return 1; return 0; } }const int maxn=1200500; int T,n,m; int dep[maxn],myf[maxn],sonum[maxn],top[maxn],hson[maxn],in[maxn],myin[maxn],tim; int pos[maxn],du[maxn]; int t,head[maxn<<6]; vector<int> E[maxn]; struct edge{ int v,next; };edge e[maxn<<6]; il void add_edge(int u,int v){ e[++t].v=v; e[t].next=head[u]; head[u]=t; } namespace SegementTree{ #define lid (id<<1) #define rid (id<<1|1) int S[maxn<<2],T[maxn<<2]; void build_tree(int id,int l,int r){ if(l==r){ pos[myin[l]]=id; return; } add_edge(id,lid);++du[lid]; add_edge(id,rid);++du[rid]; add_edge((n<<2)+lid,(n<<2)+id);++du[(n<<2)+id]; add_edge((n<<2)+rid,(n<<2)+id);++du[(n<<2)+id]; int mid=(l+r)>>1; build_tree(lid,l,mid); build_tree(rid,mid+1,r); } void update(int id,int l,int r,int x,int y,int p){ if(x>r || y<l) return; if(x<=l && r<=y){ add_edge(p,id);++du[id]; add_edge((n<<2)+id,p);++du[p]; return; } int mid=(l+r)>>1; update(lid,l,mid,x,y,p); update(rid,mid+1,r,x,y,p); } void update1(int id,int l,int r,int x,int y,int p){ if(x>r || y<l || x>y) return; if(x<=l && r<=y){ add_edge(p,id); ++du[id]; return; } int mid=(l+r)>>1; update1(lid,l,mid,x,y,p); update1(rid,mid+1,r,x,y,p); } void update2(int id,int l,int r,int x,int y,int p){ if(x>r || y<l || x>y) return; if(x<=l && r<=y){ add_edge((n<<2)+id,p); ++du[p]; return; } int mid=(l+r)>>1; update2(lid,l,mid,x,y,p); update2(rid,mid+1,r,x,y,p); } #undef lid #undef rid } il void dfs1(int now,int fa){ myf[now]=fa; dep[now]=dep[fa]+1; sonum[now]=1; for(int to:E[now]){ if(to==fa) continue; dfs1(to,now); sonum[now]+=sonum[to]; if(sonum[hson[now]]<sonum[to]) hson[now]=to; } } il void dfs2(int now,int topp){ top[now]=topp; in[now]=++tim; myin[tim]=now; if(!hson[now]) return; dfs2(hson[now],topp); for(int to:E[now]){ if(to!=myf[now] && to!=hson[now]) dfs2(to,to); } } il void update(int u,int v,int id){ add_edge(n*8+id,(n<<2)+pos[u]);++du[(n<<2)+pos[u]]; add_edge(pos[v],n*8+id);++du[n*8+id]; int x=u,fx=top[x],y=v,fy=top[y]; while(fx!=fy){ if(dep[fx]>dep[fy]) swap(fx,fy),swap(x,y); if(y!=u && y!=v) SegementTree::update(1,1,n,in[fy],in[y],8*n+id); else{ SegementTree::update(1,1,n,in[fy],in[y]-1,8*n+id); if(y==u) add_edge(8*n+id,pos[u]),++du[pos[u]]; else add_edge(4*n+pos[v],8*n+id),++du[8*n+id]; } y=myf[top[y]],fy=top[y]; } if(dep[x]>dep[y]) swap(x,y); if(x!=v && y!=v) SegementTree::update1(1,1,n,in[x],in[y],8*n+id); else if(x!=y){ if(x==v) SegementTree::update1(1,1,n,in[x]+1,in[y],8*n+id); else SegementTree::update1(1,1,n,in[x],in[y]-1,8*n+id); } if(x!=u && y!=u) SegementTree::update2(1,1,n,in[x],in[y],8*n+id); else if(x!=y){ if(x==u) SegementTree::update2(1,1,n,in[x]+1,in[y],8*n+id); else SegementTree::update2(1,1,n,in[x],in[y]-1,8*n+id); } } queue<int> q; il void inttopo(){ for(int i=1;i<=n*8+m;++i){ if(du[i]==0) q.push(i); } while(!q.empty()){ int u=q.front(); q.pop(); for(rg i=head[u];i;i=e[i].next){ int to=e[i].v; --du[to]; if(du[to]==0) q.push(to); } } } il void clear(){ tim=0; t=0; for(rg i=1;i<=n;++i) <% myf[i]=dep[i]=sonum[i]=hson[i]=in[i]=myin[i]=top[i]=0;E[i].clear(); %> for(rg i=1;i<=n*8+m;++i) head[i]=du[i]=0; } il void input(){ n=io::read(); int u,v; for(rg i=1;i<=n-1;++i){ u=io::read(),v=io::read(); E[u].push_back(v); E[v].push_back(u); } } int main(){ // freopen("prison.in","r",stdin); // freopen("prison.out","w",stdout); T=io::read(); while(T--){ input(); dfs1(1,0); dfs2(1,1); SegementTree::build_tree(1,1,n); m=io::read(); int s,t; for(rg i=1;i<=m;++i){ scanf("%d %d",&s,&t); update(s,t,i); } // for(rg i=1;i<=n*8+m;++i){ // cout<<i<<' '<<du[i]<<endl; // } inttopo(); bool mj=true; for(int i=1;i<=8*n+m;++i){ if(du[i]) <% mj=false;break; %> } if(mj) printf("Yes\n"); else printf("No\n"); clear(); } return 0; } /* 1 8 1 2 2 3 3 4 4 5 5 6 6 7 7 8 4 1 5 2 6 3 7 4 8 */
倍增优化建图
对于这个,我们通过一道题来讲吧。
例题
【XR-1】逛森林
题目背景
NaCly_Fish 和 PinkRabbit 是好朋友。
有一天她去森林里游玩,回去跟 PinkRabbit 说:“我发现好多棵会动的树耶!”
PinkRabbit 动了动一只兔耳朵:“这有什么好稀奇的,我用一只兔耳朵就能维护每棵树的形态。”
NaCly_Fish 不服:“不止这样,我还看到有一些传送门,能从一条树枝跳到另一条树枝上呢!”
PinkRabbit 动了动另一只兔耳朵:“这有什么好稀奇的,我用两只兔耳朵就能统计每个传送门的信息。”
于是 NaCly_Fish 很郁闷,她向你求助,请帮帮她吧。
什么?你不愿意帮?
那她就不给你这题的分了。
题目描述
给你 个节点的森林,初始没有边。
有 个操作,分为两种:
:表示构建一个单向传送门,从 简单路径上的所有节点,可以花费 的代价,到达 简单路径上的所有节点。若 到 或 到 不连通(由 操作产生的边不连通),则忽略此次操作。
:表示将 和 节点间连一条花费为 的无向边,若 和 之间已连通(由 操作产生的边连通)则忽略此次操作。
经过这 次操作后,请你求出从 节点出发,到每个节点的最小花费。
输入格式
第一行三个正整数 ,分别表示树的节点数、操作数、和起始节点。
接下来 行,每行若干个正整数,表示一次操作。
输出格式
输出一行 个整数,第 个整数表示从 节点出发,到 节点的最小花费。特别地,若不能到达节点,则输出 -1
。
样例 #1
样例输入 #1
9 11 5 2 2 1 2 2 3 1 5 2 4 2 10 2 5 3 9 2 6 5 3 2 7 6 6 2 8 7 2 2 9 4 2 1 1 1 4 9 2 1 8 5 1 6 1 1 3 6 9 6 1
样例输出 #1
1 1 1 1 0 1 7 9 1
提示
【样例说明】
这是样例中给出的树(严格来讲,这棵树也是一条链):
有三个传送门,其中两个是这样的:
- 从 号点可以花费 的代价到达 简单路径上的所有节点(即 号点)。
- 从 简单路径上的所有节点(即 号点)可以花费 的代价到达 简单路径上的所有节点(即 号点)。
容易看出从 号节点出发,到达其它节点的最小花费分别为:。
【数据规模与约定】
对于第 个测试点,,。
对于第 个测试点,,。
对于 的数据,,,,。
对于第 ~ 个测试点,每个 分。
对于第 个测试点,每个 分。
解题:
题意概括:
有两个操作:
-
操作 :在连通的区间 和连通的区间 两个区间之间连一条权值为 的边(若区间不连通,忽略操作)。
-
操作 :若 之间没有连通,连一条权值为 的无向边。
求 次操作后,从节点 出发,到达每个节点的最小花费。
这道题,我们求的是 次操作后的结果,所以可以离线,也就是先把操作 的边连上。
而如何判断连通性呢?我们可以选择并查集。对于合法的操作 存起来,合法的操作 加边。
这是本题的特别之处,而剩下的,就是对操作 进行愉快的倍增优化建图了。
对于一条链来说,我们两个端点一定会跳很多次倍增数组,但是如果我们可以让跳一次倍增数组越过的点看作一个点,那么就可以用这个点连边而不用再走一遍倍增。
因此,我们可以对于倍增使用的 开两个虚点 与 , 用来存入边, 用来存出边,倍增的下一级 指向上一级 ,上一级 指向下一级 。
如图:
图上,#39C5BB 色的是树边,#ff407a 色的是倍增的数组,#9999ff 色的是新建的边。
这样,我们的节点数和边数就是 的水平。
那么如何相连 和 呢?
在跳倍增的时候,我们从 每跳一次倍增就让 这个虚节点向另一个新建虚节点 连边,而对于 则让新建虚节点 向 连边,最后将两个虚节点连一条权值为 的边。
如图。
最后建完图再跑一遍 Dijkstra 就可以直接查询。
时间复杂度 。
需要注意的是,当深度相同时,如果一步没跳,我们需要特别判断给 连一次边,否则 就没有外向边了,但是本题是求最短路,所以在开头直接连也可以。
如图。
(ps:经过测试,数组开70倍可以优雅的过这道题)
Miku's Code
#include<bits/stdc++.h> using namespace std; #define il inline #define rg register int typedef long double llf; typedef long long ll; typedef pair<int,int> PII; const double eps=1e-8; namespace io{ // char in[1<<20],*p1=in,*p2=in; // #define getchar() (p1==p2&&(p2=(p1=in)+fread(in,1,1<<20,stdin),p1==p2)?EOF:*p1++) il int read(){ char c=getchar(); int x=0,f=1; while(c<48)<%if(c=='-')f=-1;c=getchar();%> while(c>47)x=(x*10)+(c^48),c=getchar(); return x*f; } //快读 il void write(int x){ if(x<0)<%putchar('-');x=~x+1;%> if(x>9) write(x/10); putchar(x%10+'0'); } //快写 il int ins(char *str){ int len=0; while(1){ char c=getchar(); if(c!='\n' && c!='\0' && c!='\r') str[++len]=c; else break; } return len; } //getchar快速读入字符串并返回字符串长度,下标从1开始 } namespace mystd{ il int Max(int a,int b)<%if(a<b) return b;return a; %> il int Min(int a,int b)<%if(a>b) return b;return a; %> il int Abs(int a)<% if(a<0) return a*(-1);return a; %> il double fMax(double a,double b)<%if(a<b) return b;return a; %> il double fMin(double a,double b)<%if(a>b) return b;return a; %> il double fAbs(double a)<% if(a<0) return a*(-1);return a; %> il int dcmp(double a){ if(a<-eps) return -1; if(a>eps) return 1; return 0; } }const int maxn=5e4+50,maxm=1e6+50,inf=0x3f3f3f3f; #define x1 MD #define x2 IA #define x3 KY #define x4 UO #define next Miku struct QUERY{ int opt,x1,y1,x2,y2,w; };QUERY Q[maxm]; vector<int> E[maxn]; int head[maxm*70],t; struct edge{ int v,w; int next; };edge e[maxm*70]; il void add_edge(int u,int v,int w){ e[++t].v=v; e[t].w=w; e[t].next=head[u]; head[u]=t; } int qt; int n,m,st,len; int dep[maxn],myf[maxn][20],in[maxn][20],out[maxn][20],cnt; int fa[maxn]; int getfa(int x){ if(x==fa[x]) return x; else return fa[x]=getfa(fa[x]); } il void dfs(int now,int ffa){ myf[now][0]=ffa; dep[now]=dep[ffa]+1; in[now][0]=++cnt;add_edge(cnt,now,0);add_edge(cnt,ffa,0); out[now][0]=++cnt;add_edge(now,cnt,0);add_edge(ffa,cnt,0); for(rg j=1;j<=len;++j){ myf[now][j]=myf[myf[now][j-1]][j-1]; in[now][j]=++cnt;add_edge(cnt,in[now][j-1],0);add_edge(cnt,in[myf[now][j-1]][j-1],0); out[now][j]=++cnt;add_edge(out[now][j-1],cnt,0);add_edge(out[myf[now][j-1]][j-1],cnt,0); } int siz=E[now].size()-1; for(rg i=siz;i>=0;--i){ int to=E[now][i]; if(to==ffa) continue; dfs(to,now); } } il void link1(int x,int y,int k){ //out外连虚点 if(dep[x]<dep[y]) swap(x,y); add_edge(y,k,0); for(rg i=len;i>=0;i--){ if(dep[myf[x][i]]>=dep[y]) <% add_edge(out[x][i],k,0),x=myf[x][i]; %> } if(x==y) return; for(rg i=len;i>=0;i--){ if(myf[x][i]!=myf[y][i]){ add_edge(out[x][i],k,0); x=myf[x][i]; add_edge(out[y][i],k,0); y=myf[y][i]; } } add_edge(out[x][0],k,0); } il void link2(int x,int y,int k){ //虚点连in if(dep[x]<dep[y]) swap(x,y); add_edge(k,y,0); for(rg i=len;i>=0;i--){ if(dep[myf[x][i]]>=dep[y])<% add_edge(k,in[x][i],0);x=myf[x][i]; %> } if(x==y) return; for(rg i=len;i>=0;i--){ if(myf[x][i]!=myf[y][i]){ add_edge(k,in[x][i],0); x=myf[x][i]; add_edge(k,in[y][i],0); y=myf[y][i]; } } add_edge(k,in[x][0],0); } int dis[maxn*70]; il void dijkstra(int now){ memset(dis,0x3f,sizeof(dis)); priority_queue<PII,vector<PII>,greater<PII> > heap; dis[now]=0; heap.push(make_pair(0,now)); PII s; while(!heap.empty()){ s=heap.top(); heap.pop(); int distance=s.first,temp=s.second; if(distance>dis[temp]) continue; for(int i=head[temp];i;i=e[i].next){ int to=e[i].v; // cout<<"###"<<now<<' '<<to<<endl; if(dis[to]>distance+e[i].w){ dis[to]=distance+e[i].w; heap.push(make_pair(dis[to],to)); } } } } il void pre(){ while((1<<len)<=n) ++len; --len; for(rg i=1;i<=n;++i) fa[i]=i; int opt,x1,y1,x2,y2,w; while(m--){ opt=io::read(),x1=io::read(),y1=io::read(); if(opt==1){ x2=io::read(),y2=io::read(),w=io::read(); if(getfa(x1)!=getfa(y1) || getfa(x2)!=getfa(y2)) continue; Q[++qt]=(QUERY)<% opt,x1,y1,x2,y2,w %>; } else{ w=io::read(); int fx=getfa(x1),fy=getfa(y1); if(fx==fy) continue; // cout<<"@@@"<<x1<<' '<<y1<<endl; E[x1].push_back(y1); E[y1].push_back(x1); add_edge(x1,y1,w); add_edge(y1,x1,w); fa[fx]=fy; } } } int main(){ n=io::read(),m=io::read(),st=io::read(); pre(); cnt=n; for(rg i=1;i<=n;++i) if(!dep[i]) dfs(i,0); for(rg i=1;i<=qt;++i){ link1(Q[i].x1,Q[i].y1,++cnt); link2(Q[i].x2,Q[i].y2,++cnt); add_edge(cnt-1,cnt,Q[i].w); } dijkstra(st); for(rg i=1;i<=n;++i){ if(dis[i]==0x3f3f3f3f) printf("-1 "); else printf("%d ",dis[i]); } return 0; } #undef x1 #undef x2 #undef y1 #undef y2 #undef next
[JOISC 2022 Day1] 监狱
还是这道题,我们现在要用倍增做。
感觉好像板子
最后没调出来,不会,呜。
然后去洛谷,cnblogs,csdn都找了,没有题解……有提到一嘴的,但是都没有详细写如何实现……
不是,给咱一个时间复杂度咱也胡不出来咱的 AC code 啊。
我尽力,如果以后有人写了的话我一定写。
有一份代码,是从 loj 找到的,但是不知道写的什么意思,我尝试写了一些注释可能是误解,这份代码在 loj 的原链接找不到了,对原作者说一声抱歉。
Other's Code
#include <bits/stdc++.h> using namespace std; const int N = 5e5 + 5; int n, m; vector<int> T[N], G[N * 40]; int fa[N][20], dep[N], tim, st[N], ed[N]; int cnt, id1[N][20], id2[N][20]; void dfs(int u, int f) { dep[u] = dep[f] + 1, st[u] = ++tim; fa[u][0] = f; id1[u][0] = ++cnt; id2[u][0] = ++cnt; for(int i = 1; i <= 17; i++) { fa[u][i] = fa[fa[u][i - 1]][i - 1]; if(!fa[u][i]) break; id1[u][i] = ++cnt; G[id1[u][i - 1]].push_back(id1[u][i]); G[id1[fa[u][i - 1]][i - 1]].push_back(id1[u][i]); id2[u][i] = ++cnt; G[id2[u][i]].push_back(id2[u][i - 1]); G[id2[u][i]].push_back(id2[fa[u][i - 1]][i - 1]); } for(auto v : T[u]) if(v != f) dfs(v, u); ed[u] = tim; return; } bool isanc(int x, int y) {return st[x] <= st[y] && st[y] <= ed[x];} //如果 y 的起点在 x 的路径上 void upd1(int x, int y) { int s = x, v = id2[y][0]; if(!isanc(x, y)) x = fa[x][0]; //如果x与y不在一条链上?如果y不在x的子树里,将x向上走一格子 if(dep[x] < dep[y]) swap(x, y); //保证x在下方 for(int i = 17; i >= 0; i--) if(dep[fa[x][i]] >= dep[y]) G[id1[x][i]].push_back(v), x = fa[x][i]; //id1是out,向外连 if(x == y) return s ==x ? void() : G[id1[x][0]].push_back(v); for(int i = 17; i >= 0; i--) if(fa[x][i] != fa[y][i]) { G[id1[x][i]].push_back(v), x = fa[x][i]; G[id1[y][i]].push_back(v), y = fa[y][i]; } G[id1[x][0]].push_back(v), G[id1[y][0]].push_back(v); G[id1[fa[x][0]][0]].push_back(v); return; } void upd2(int x, int y) { int t = y, u = id1[x][0]; if(!isanc(y, x)) y = fa[y][0]; //如果y的起点不在x的子树上,将y向上走 if(dep[x] < dep[y]) swap(x, y); //保持x在下方 for(int i = 17; i >= 0; i--) if(dep[fa[x][i]] >= dep[y]) G[u].push_back(id2[x][i]), x = fa[x][i]; if(x == y) return t == x ? void() : G[u].push_back(id2[x][0]); for(int i = 17; i >= 0; i--) if(fa[x][i] != fa[y][i]) { G[u].push_back(id2[x][i]), x = fa[x][i]; G[u].push_back(id2[y][i]), y = fa[y][i]; } G[u].push_back(id2[x][0]), G[u].push_back(id2[y][0]); G[u].push_back(id2[fa[x][0]][0]); return; } int deg[N * 40]; queue<int> que; bool topo() { for(int i = 1; i <= cnt; i++) if(!deg[i]) que.push(i); while(!que.empty()) { int u = que.front(); que.pop(); for(auto v : G[u]) if(!(--deg[v])) que.push(v); } for(int i = 1; i <= cnt; i++) if(deg[i]) return false; return true; } void clear() { memset(fa, 0, sizeof(fa)); for(int i = 1; i <= n; i++) T[i].clear(); for(int i = 1; i <= cnt; i++) G[i].clear(), deg[i] = 0; return; } int K; void solve(){ cin >> n; cnt = 0; for(int i = 1, u, v; i < n; i++){ cin >> u >> v, T[u].push_back(v), T[v].push_back(u); } tim=0,dfs(1,0); cin >> m; for(int i = 1, x, y; i <= m; i++){ cin >> x >> y, upd1(x, y), upd2(x, y); G[id2[y][0]].push_back(id1[x][0]); } for(int i = 1; i <= cnt; i++) for(auto j : G[i]) deg[j]++; //, cerr << i << " " << j << "\n"; cout << (topo() ? "Yes\n" : "No\n"); return clear(); } int main(){ ios::sync_with_stdio(false); cin.tie(nullptr), cout.tie(nullptr); int T; cin >> T; while(T--) solve(); // for(K = 1; K <= T; K++) solve(); return 0; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!