图论——拓扑排序
图论——拓扑排序
拓扑排序概述
对 DAG 进行拓扑排序,得到结点序列
一个朴素的想法是,每次取入度为
因为原图无环,所以在拓扑排序没有结束时,必然存在入度为
通过 BFS 或 DFS 实现拓扑排序的过程:
-
对于 BFS,用队列维护所有入度为
的点。取出队首 ,删去从 出发的所有边 。如果删边导致某个点 从入度大于 变成入度等于 ,那么将 入队。初始将所有入度为 的点入队。 -
对于 DFS,对于当前点
,若 在删去 后从入度大于 变成入度等于 ,那么向 DFS。初始从每个入度为 的点开始搜索。
时间复杂度为
合理性证明
考虑一个图,删掉某个入度为
应用
拓扑排序可以判断图中是否有环,还可以用来判断图是否是一条链。拓扑排序可以用来求 AOE 网中的关键路径,估算工程完成的最短时间。
求字典序最大/最小的拓扑排序
将 BFS 算法中的队列替换成最大堆/最小堆实现的优先队列即可,此时总的时间复杂度为
例 :排水系统(不是很模板的模板题)
思路
模板题,但是要进行分数约分,所以又不是很模板直接进行计算即可。注意计算过程中很可能爆
#include<bits/stdc++.h>
#define int long long
using namespace std;
typedef long long ll;
const int N = 3e5 + 50;
int n,m,cnt;
int head[N],in[N],deg[N],res[N],tot;
__int128 gcd(__int128 x,__int128 y)
{
if(y == 0)
return x;
return gcd(y,x%y);
}
struct edge{
int to,nxt;
}e[N];
struct ans{
__int128 p,q;
}ans[N];
void add(int u,int v)
{
e[++cnt].to = v;
e[cnt].nxt = head[u];
head[u] = cnt;
return;
}
void write(__int128 n) {
if(n >= 10) write(n/10);
putchar(n%10 + '0');
}
signed main()
{
scanf("%lld %lld",&n,&m);
for(int i = 1;i <= n;i++)
{
int x;
scanf("%lld",&x);
if(x == 0)
res[++tot] = i;
deg[i] = x;
while(x--)
{
int y;
scanf("%lld",&y);
add(i,y);
in[y]++;
}
}
queue<int> q;
for(int i = 1;i <= n;i++)
{
ans[i].q = 1;
if(in[i] == 0)
q.push(i),ans[i].p = 1;
}
while(!q.empty())
{
int x = q.front();q.pop();
for(int i = head[x];i;i=e[i].nxt)
{
int y = e[i].to;
__int128 yy = ans[x].p * ans[y].q;
ans[y].p = ans[y].p * ans[x].q * deg[x];
ans[y].q = ans[y].q * ans[x].q * deg[x];
ans[y].p = ans[y].p + yy;
__int128 xx = gcd(ans[y].q,ans[y].p);
if(xx != 0)
ans[y].q /= xx;ans[y].p /= xx;
in[y]--;
if(in[y] == 0)q.push(y);
}
}
for(int i = 1;i <= tot;i++)
write(ans[res[i]].p),putchar(' '),write(ans[res[i]].q),putchar('\n');
return 0;
}
例 :菜肴制作
思路
套路题,考虑正面直接模拟不是很好做,现在考虑在反图上用贪心的思想做。
我们发现一个性质,如果现在有一个菜品编号较大的菜,那么我们就要尽量让菜品编号比它小的菜在它的前面,这样做肯定是满足题意的最优解。
利用上述性质就可以完成题目了,具体地,考虑反图代表的实际意义为,做这个菜之前要做哪些菜,所以进行拓扑的意义就为确定从最后依次往前做那些菜,那么我们就先在反图上求字典序最大的拓扑序,最后再反过来就一定为最终的解。
下面我们来进行证明这个贪心的正确性:
定义两个拓扑序中更优的一个为“最小序”更小的拓扑序。
求证:一个 DAG 的拓扑序中“最小序”最小的的一个拓扑序,是反向字典序最大的一个。
证明:
首先,当
其次,假设结论对于
特别地,有
证明:由于
所以
现将整个图可以分成三个子图,其点集分别为
由于
因为
所以对于任意的 DAG,由数学归纳法得结论均成立。
证毕。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e5 + 50;
int T;
int n,m,cnt,head[N],in[N],ans[N],tot,cnt2,head2[N];
struct edge{
int to,nxt;
}e2[N];
void add2(int u,int v)
{
e2[++cnt2].to = v;
e2[cnt2].nxt = head2[u];
head2[u] = cnt2;
return;
}
int main()
{
scanf("%d",&T);
while(T--)
{
tot = 0;cnt2 = 0;
scanf("%d %d",&n,&m);
for(int i = 1;i <= n;i++)in[i] = 0,ans[i] = 0,head2[i] = 0;
for(int i = 1;i <= m;i++)
{
int u,v;
scanf("%d %d",&u,&v);
add2(v,u);
in[u]++;
}
priority_queue<int> q;
for(int i = 1;i <= n;i++)
if(in[i] == 0)
q.push(i);
int cntt = 0;
while(!q.empty())
{
int x = q.top();q.pop();
cntt++;
ans[++tot] = x;
for(int i = head2[x];i;i = e2[i].nxt)
{
int y = e2[i].to;
in[y]--;
if(in[y] == 0)q.push(y);
}
}
if(cntt != n)
{
printf("Impossible!\n");
continue;
}
for(int i = tot;i >= 1;i--)
printf("%d ",ans[i]);
printf("\n");
}
return 0;
}
例 :PUS (Pustynia)
思路
题目似乎看起来像数学题,其实不然,这与图论最短路中的一个经典应用有关——差分约束。
我们利用最短路的性质,如果一个整数 开心地根据题意模拟对每个点进行建边即可。
显然直接按照题意模拟建边的复杂度就为
考虑进行一个小优化,每一个区间创立一个虚拟节点,其中
思考还可以怎么优化,看到一个点向一个区间连边,可以使用线段树高效解决此类建图问题,最终时间复杂度为
#include<bits/stdc++.h>
using namespace std;
const int N = 6e6 + 50,K = 6e5 + 50;
int n,s,m,cnt,tot;
int head[K],a[K],b[K],in[K];
bool vis[K];
int dis[K];
struct edge{
int to,nxt,w;
}e[N];
void add(int u,int v,int f)
{
e[++cnt].to = v;
e[cnt].w = f;
e[cnt].nxt = head[u];
head[u] = cnt;
in[v]++;
return;
}
void build(int p,int l,int r)
{
tot = max(tot,p);
if(l == r)
{
a[l] = p;
return;
}
add(p,p * 2,0);add(p,p * 2 + 1,0);
int mid = (l + r) >> 1;
build(p * 2,l,mid);build(p * 2 + 1,mid + 1,r);
return;
}
void update(int p,int l,int r,int x,int y,int k)
{
if(x <= l && r <= y)
{
add(k,p,0);
return;
}
int mid = (l + r) >> 1;
if(x <= mid)update(p * 2,l,mid,x,y,k);
if(y > mid)update(p * 2 + 1,mid + 1,r,x,y,k);
return;
}
int main()
{
cnt = 0;
scanf("%d %d %d",&n,&s,&m);
build(1,1,n);
for(int i = 1;i <= s;i++)
{
int p,d;
scanf("%d %d",&p,&d);
b[a[p]] = d;dis[a[p]] = d;
}
for(int i = 1;i <= m;i++)
{
int l,r,k;
scanf("%d %d %d",&l,&r,&k);
tot++;
int last = l;
for(int j = 1;j <= k;j++)
{
int x;
scanf("%d",&x);
add(a[x],tot,-1);
if(last < x)update(1,1,n,last,x - 1,tot);
last = x + 1;
}
if(last <= r)update(1,1,n,last,r,tot);
}
queue<int> q;
for(int i = 1;i <= tot;i++)
{
if(in[i] == 0)q.push(i);
if(dis[i] == 0)dis[i] = 1e9;
}
while(!q.empty())
{
int x = q.front();q.pop();vis[x] = 1;
for(int i = head[x];i;i = e[i].nxt)
{
int v = e[i].to;
dis[v] = min(dis[v],dis[x] + e[i].w);
if(b[v] && dis[v] < b[v])
{
printf("NIE\n");
return 0;
}
in[v]--;
if(in[v] == 0)q.push(v);
}
}
for(int i = 1;i <= tot;i++)
{
if(dis[i] < 1 || vis[i] == 0)
{
printf("NIE\n");
return 0;
}
}
printf("TAK\n");
for(int i = 1;i <= n;i++)
printf("%d ",dis[a[i]]);
return 0;
}
例 :Elaxia的路线
思路
先分别把关于两对起点和终点的最短路图求出来,再取交集,然后进行
那么可以考虑对求出来的最短路图的交集搞个方向,这样就能进行
那么我们就可以进行分类讨论,固定一对起点和终点的最短路图的方向,另外一对起点和终点再判断是并行还是相遇的类型。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 2050,M = 6e5 + 50;
int n,m,cnt,cnt2;
int s1,s2,t1,t2;
int head[N],head2[N],in[N];
ll dis1[N],dis2[N],dis3[N],dis4[N],F[N];
bool vis1[N],vis2[N],mark[N];
struct edge{
int to,nxt,w;
}e[M],e2[M];
void add(int u,int v,int f)
{
e[++cnt].w = f;
e[cnt].nxt = head[u];
e[cnt].to = v;
head[u] = cnt;
return;
}
void add2(int u,int v,int f)
{
e2[++cnt2].w = f;
e2[cnt2].nxt = head2[u];
e2[cnt2].to = v;
head2[u] = cnt2;
return;
}
struct node{
int pos,val;
bool operator > (const node &x)const{
return val > x.val;
}
};
void dij(int s)
{
priority_queue<node,vector<node>,greater<node> >q;
dis1[s] = 0;
q.push(node{s,dis1[s]});
while(!q.empty())
{
node t = q.top();q.pop();
int x = t.pos;
if(vis1[x])continue;
vis1[x] = 1;
for(int i = head[x];i;i = e[i].nxt)
{
int v = e[i].to;
if(dis1[v] > dis1[x] + e[i].w)
{
dis1[v] = dis1[x] + e[i].w;
q.push(node{v,dis1[v]});
}
}
}
return;
}
void dij2(int s)
{
priority_queue<node,vector<node>,greater<node> >q;
dis2[s] = 0;
q.push(node{s,dis2[s]});
while(!q.empty())
{
node t = q.top();q.pop();
int x = t.pos;
if(vis2[x])continue;
vis2[x] = 1;
for(int i = head[x];i;i = e[i].nxt)
{
int v = e[i].to;
if(dis2[v] > dis2[x] + e[i].w)
{
dis2[v] = dis2[x] + e[i].w;
q.push(node{v,dis2[v]});
}
}
}
return;
}
int main()
{
cnt = 0;cnt2 = 0;
scanf("%d %d",&n,&m);
scanf("%d %d %d %d",&s1,&t1,&s2,&t2);
for(int i = 1;i <= n;i++)
{
dis1[i] = 1e18;
dis2[i] = 1e18;
head[i] = 0;
head2[i] = 0;
vis1[i] = 0;
vis2[i] = 0;
}
for(int i = 1,u,v,w;i <= m;i++)
{
scanf("%d %d %d",&u,&v,&w);
add(u,v,w);
add(v,u,w);
}
dij(s1);dij2(s2);
for(int i = 1;i <= n;i++)
{
dis3[i] = dis1[i];
dis4[i] = dis2[i];
dis1[i] = 1e18;
dis2[i] = 1e18;
vis1[i] = 0;
vis2[i] = 0;
}
dij(t1);dij2(t2);
for(int i = 1;i <= n;i++)
{
swap(dis1[i],dis3[i]);
swap(dis2[i],dis4[i]);
}
queue<int> q;
ll ans = -1e18;
for(int i = 1;i <= n;i++)
{
for(int j = head[i];j;j = e[j].nxt)
{
int v = e[j].to,w = e[j].w;
if(dis1[i] + dis3[v] + w == dis1[t1])
if(dis2[i] + dis4[v] + w == dis2[t2])
add2(i,v,w),in[v]++,mark[i] = 1,mark[v] = 1;
}
}
for(int i = 1;i <= n;i++)
if(in[i] == 0 && mark[i])q.push(i);
while(!q.empty())
{
int x = q.front();q.pop();
for(int i = head2[x];i;i = e2[i].nxt)
{
int v = e2[i].to;
F[v] = max(F[v],F[x] + e2[i].w);
in[v]--;
if(in[v] == 0)q.push(v);
}
}
for(int i = 1;i <= n;i++)
ans = max(ans,F[i]);
memset(head2,0,sizeof(head2));
memset(F,0,sizeof(F));
memset(in,0,sizeof(in));
memset(mark,0,sizeof(mark));
cnt2 = 0;
for(int i = 1;i <= n;i++)
{
for(int j = head[i];j;j = e[j].nxt)
{
int v = e[j].to,w = e[j].w;
if(dis1[i] + dis3[v] + w == dis1[t1])
if(dis2[v] + dis4[i] + w == dis2[t2])
add2(i,v,w),in[v]++,mark[i] = 1,mark[v] = 1;
}
}
for(int i = 1;i <= n;i++)
if(in[i] == 0 && mark[i])q.push(i);
while(!q.empty())
{
int x = q.front();q.pop();
for(int i = head2[x];i;i = e2[i].nxt)
{
int v = e2[i].to;
F[v] = max(F[v],F[x] + e2[i].w);
in[v]--;
if(in[v] == 0)q.push(v);
}
}
for(int i = 1;i <= n;i++)
ans = max(ans,F[i]);
printf("%lld\n",ans);
return 0;
}
例 :数列恢复
思路
考虑可以把
实际上本质就是差分约束。
当然无解情况之一就是图中有环存在,即
方法一:构造+判定
先不管
具体地,注意我们最后的
又因为图中没有环,所以可以进行拓扑排序求出
当然到这一步还没有结束,我们的做法似乎是假的,因为我们没有考虑
#include<bits/stdc++.h>
using namespace std;
const int N = 550;
string s[N];
int n,head[N],cnt,in[N],color[N],a[N],f[N];
struct edge{
int to,nxt;
}e[N * N];
void add(int u,int v)
{
e[++cnt].to = v;
e[cnt].nxt = head[u];
head[u] = cnt;
return;
}
int main()
{
while(cin >> n)
{
head[0] = 0;cnt = 0;in[0] = 0;color[0] = 0;f[0] = 0;a[0] = 0;
for(int i = 1;i <= n;i++)
{
head[i] = 0;
color[i] = 0;
in[i] = 0;
f[i] = 0;
a[i] = 0;
s[i].clear();
cin >> s[i];
s[i] = " " + s[i];
}
for(int i = 1;i <= n;i++)
{
int len = s[i].length();
int x = i;
for(int j = 1;j <= len;j++)
{
if(s[i][j] == '+')
{
add(i - 1,j + x - 1);
in[j + x - 1]++;
}
else if(s[i][j] == '-')
{
add(j + x - 1,i - 1);
in[i - 1]++;
}
else continue;
}
}
queue<int> q;
for(int i = 0;i <= n;i++)
{
if(in[i] == 0)
q.push(i);
}
int tot = -1,cntt = 0;
while(!q.empty())
{
int x = q.size();
tot++;
cntt += x;
while(x--)
{
int y = q.front();q.pop();
color[y] = tot;
for(int i = head[y];i;i = e[i].nxt)
{
int v = e[i].to;
in[v]--;
if(in[v] == 0)q.push(v);
}
}
}
if(cntt != n + 1)
{
cout << "NO" << endl;
continue;
}
for(int i = 1;i <= n;i++)
a[i] = color[i] - color[i - 1],f[i] = f[i - 1] + a[i];
bool flag = true;
for(int i = 1;i <= n;i++)
{
int len = s[i].length();
int x = i;
for(int j = 1;j <= len;j++)
{
if(s[i][j] != '0')continue;
if(f[j + x - 1] - f[i - 1] != 0)
{
flag = false;
break;
}
}
if(flag == false)
break;
}
if(flag)
{
cout << "YES" << endl;
for(int i = 1;i <= n;i++)
cout << a[i] << " ";
cout << endl;
}
else
cout << "NO" << endl;
}
return 0;
}
方法二:并查集
当
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=110,M=1e4+10;
struct node{
int x,y,nxt;
}e[M];
bool flag;
int t,n,elast[N],d[N],f[N],idx,fa[N];
char b[N][N];
queue<int> q;
inline int find(register int x){
return fa[x]==x?x:fa[x]=find(fa[x]);
}
signed main(){
while(scanf("%lld",&n)!=EOF){
flag=true;
idx=0;
for(int i=0;i<=n;i=-~i){
fa[i]=i;
f[i]=-1e18;
d[i]=0;
elast[i]=0;
}
for(int i=1;i<=n;i=-~i){
scanf("%s",b[i]+1);
for(int j=i;j<=n;j=-~j){
if(b[i][j-i+1]=='0'){
int x=find(i-1);
int y=find(j);
if(x!=y){
fa[x]=y;
}
}
}
}
for(int i=1;i<=n;i=-~i){
for(int j=i;j<=n;j=-~j){
if(b[i][j-i+1]=='+'){
int x=find(i-1);
int y=find(j);
++idx;
e[idx].x=x;
e[idx].y=y;
e[idx].nxt=elast[x];
elast[x]=idx;
d[y]++;
}
if(b[i][j-i+1]=='-'){
int x=find(j);
int y=find(i-1);
++idx;
e[idx].x=x;
e[idx].y=y;
e[idx].nxt=elast[x];
elast[x]=idx;
d[y]++;
}
}
}
for(int i=0;i<=n;i=-~i){
if(d[i]==0){
f[i]=0;
q.push(i);
}
}
while(!q.empty()){
int t=q.front();
q.pop();
for(int i=elast[t];i>0;i=e[i].nxt){
int y=e[i].y;
f[y]=max(f[y],f[t]+1);
--d[y];
if(d[y]==0){
q.push(y);
}
}
}
for(int i=0;i<=n;i=-~i){
if(f[i]==-1e18){
flag=false;
break;
}
}
for(int i=1;i<=n;i=-~i){
int tmp=f[find(i)]-f[find(i-1)];
if(abs(tmp)>n){
flag=false;
break;
}
}
if(!flag){
printf("NO\n");
continue;
}
printf("YES\n");
for(int i=1;i<=n;i=-~i){
printf("%lld ",f[find(i)]-f[find(i-1)]);
}
puts("");
}
return 0;
}
例 :[HNOI2015] 落忆枫音
思路
如果没加入这条边的话,那么形成一棵树的方案数应为
而如果加入这条边,就会形成多个环,但是直接求出所有环是不可能的,要采用
#include<bits/stdc++.h>
typedef long long ll;
using namespace std;
const int P = 1e9 + 7,N = 1e5 + 50,M = 2e5 + 50;
queue<int> q;
ll n,m,s,t,cnt;
ll in[N],dp[N],inv[N],calc[N],all = 1;
ll head[N];
struct edge{
int to,nxt;
}e[M];
void addEdge(int u,int v)
{
e[++cnt].to = v;
e[cnt].nxt = head[u];
head[u] = cnt;
return;
}
void topo() {
for (int i = 1; i <= n; i ++) if (!in[i]) q.push(i);
while (!q.empty()) {
int u = q.front(); q.pop();
dp[u] = dp[u] * inv[calc[u]] % P;
for (int i = head[u]; i; i = e[i].nxt) {
int v = e[i].to; dp[v] = (dp[v] + dp[u]) % P;
if (-- in[v] == 0) q.push(v);
}
}
cout << (all - dp[s] + P) % P << endl;
return;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin >> n >> m >> s >> t;
inv[0] = inv[1] = 1, calc[t] ++;
for (int i = 2; i <= n; i ++) inv[i] = inv[P % i] * (P - P / i) % P;
int u,v;
for (int i = 1; i <= m; i ++) {
cin >> u >> v;
addEdge(u, v), in[v] ++, calc[v] ++;
}
for (int i = 2; i <= n; i ++) all = all * calc[i] % P;
if (t == 1) cout << all << endl;
else dp[t] = all, topo();
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】