图论总结——拓扑排序
例 \(1\) :排水系统(不是很模板的模板题)
思路
模板题,但是要进行分数约分,所以又不是很模板直接进行计算即可。注意计算过程中很可能爆 \(long ~~ long\) 类型范围,需要用 \(int128\) 类型进行存储。
\(Code\)
#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;
}
例 \(2\) :菜肴制作
思路
套路题,考虑正面直接模拟不是很好做,现在考虑在反图上用贪心的思想做。
我们发现一个性质,如果现在有一个菜品编号较大的菜,那么我们就要尽量让菜品编号比它小的菜在它的前面,这样做肯定是满足题意的最优解。
利用上述性质就可以完成题目了,具体地,考虑反图代表的实际意义为,做这个菜之前要做哪些菜,所以进行拓扑的意义就为确定从最后依次往前做那些菜,那么我们就先在反图上求字典序最大的拓扑序,最后再反过来就一定为最终的解。
下面我们来进行证明这个贪心的正确性:
定义两个拓扑序中更优的一个为“最小序”更小的拓扑序。
求证:一个 DAG 的拓扑序中“最小序”最小的的一个拓扑序,是反向字典序最大的一个。
证明:
首先,当 \(∣V∣=1\) 时结论显然。
其次,假设结论对于 \(∣V∣<n\) 均成立。设图 \(G=(V,E)\) 中最小点编号为 \(x\),其中 \(∣V∣=n\),则整个点集分为两部分:
- \(S =\left \{ z|存在一条路径~z \to x \right \}\)
- \(T = V - S\)
特别地,有 \(x \in S\)。
\(Lemma~1\):令 \(m:=|S|\)。则最小序最小的拓扑序 \(a\) 一定满足 \(\left \{a_x|1 \le x \le m\right \} = S\) 且 \(a_m = x\)。
证明:由于 \(x\) 在拓扑序上的位置越小,最小序就越小,所以 \(x\) 尽可能靠前更优。由拓扑序的定义,\(S - \left \{ x \right \}\) 中的所有点在拓扑序上一定排在 \(x\) 之前。
所以 \(a_m = x\) 是 \(p \ge m\) 的充分条件。
现将整个图可以分成三个子图,其点集分别为 \(\left \{ x \right \}\) 、\(S-\left \{ x \right \}\) 和 \(T\)。
由于 \(|S|+|T|=n\),所以 \(|S− \left \{ x \right \}|,|T| < n\),由假设得其结论成立。
因为 \(x\) 是编号最小的,将 \(x\) 向后移不能获得更大的反向字典序。而 \(S− \left \{ x \right \} ,T\) 的结论已证。于是对于图 \(G\) 结论成立。
所以对于任意的 DAG,由数学归纳法得结论均成立。
证毕。
\(Code\)
#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;
}
例 \(3\):PUS (Pustynia)
思路
题目似乎看起来像数学题,其实不然,这与图论最短路中的一个经典应用有关——差分约束。
我们利用最短路的性质,如果一个整数 \(a_u \ge a_v\) ,那么就可以将点 \(u\) 连向点 \(v\) 且边的权值为 \(-1\)。然后我们就可以开心地根据题意模拟对每个点进行建边即可。
显然直接按照题意模拟建边的复杂度就为 \(\mathcal{O}(n ^ 3)\) 级别的。不能接受。
考虑进行一个小优化,每一个区间创立一个虚拟节点,其中 \(k\) 个点向虚拟节点连边权为 \(-1\) 的单向边,这个虚拟节点再向剩余 \(r-l+1-k\) 个点连边权为 \(0\) 的单向边。但很可惜,这样做时间复杂度也会炸掉,因为单次区间操作复杂度为 \(\mathcal{O}(n)\) 的,一共有 \(m\) 次操作,所以复杂度就为 \(\mathcal{O}(n ^ 2)\) 级别的。
思考还可以怎么优化,看到一个点向一个区间连边,可以使用线段树高效解决此类建图问题,最终时间复杂度为 \(\mathcal{O}(n~log_2~n)\)。
\(Code\)
#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;
}
例 \(4\):Elaxia的路线
思路
先分别把关于两对起点和终点的最短路图求出来,再取交集,然后进行 \(topo\)。当然直接这样做会有问题,因为最短路图是无向图,无法 \(topo\) 求出最短路图的交集的最长路。
那么可以考虑对求出来的最短路图的交集搞个方向,这样就能进行 \(topo\) 了。具体地,我们发现一个性质最终答案的路径长度不可能同时包含并行和相遇的边,也不可能不存在一条连续的包含并行和相遇的边的路径。正确性显然。
那么我们就可以进行分类讨论,固定一对起点和终点的最短路图的方向,另外一对起点和终点再判断是并行还是相遇的类型。
\(Code\)
#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;
}
例 \(5\):数列恢复
思路
考虑可以把 \(a_i + a_2 + \dots + a_j(n\ge j \ge i)\) 拆成两个前缀和相减。令 \(s\) 数组为 \(a\) 数组的前缀和数组,那么如果 \(a_i + a_2 + \dots + a_j > 0\) 就代表 \(s_{i - 1} < s_j\) 就可以把 \(i - 1\) 连向 \(j\) 且边权为 \(-1\),反之如果 \(a_i + a_2 + \dots + a_j < 0\) 就代表 \(s_{i - 1} > s_j\) 就可以把 \(j\) 连向 \(i - 1\) 且边权为 \(-1\)。 而如果 \(a_i + a_2 + \dots + a_j = 0\) 就会有问题,需要一些方法进行特殊处理。
实际上本质就是差分约束。
当然无解情况之一就是图中有环存在,即 \(x < x\)。
方法一:构造+判定
先不管 \(a_i + a_2 + \dots + a_j = 0\) 这种情况,那么前面两种约束就可以用最短路算法构造出来,求出 \(s\) 数组然后进行差分得到 \(a\) 数组。
具体地,注意我们最后的 \(a_i\) 要满足 \(a_i \le |n|\)。所以要是 \(s_i\) 尽可能的小,以便更可能地满足条件。
又因为图中没有环,所以可以进行拓扑排序求出 \(a\) 数组具体的值。
当然到这一步还没有结束,我们的做法似乎是假的,因为我们没有考虑 \(a_i + a_2 + \dots + a_j = 0\) 的情况。可我们想到我们构造出的 \(a\) 数组一定是满足除这个条件之外的所有条件的,且是“最优的”,即 \(a\) 数组是最小的。所以直接进行判定即可。
\(Code\)
#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;
}
方法二:并查集
当 \(a_i + a_2 + \dots + a_j = 0\) 时,把 \(i - 1\) 和 \(j\) 所在的集合合并即可。所以建边时也要按照两个点所在的集合建边。最后拓扑排序也是要让 \(s\) 数组尽可能的小。无解判定和方法一的一样。
\(Code\)
#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;
}
标签:nxt,图论,return,int,拓扑,cnt,排序,head
From: https://www.cnblogs.com/wyb-blog/p/17979862