图论——拓扑排序

图论——拓扑排序

拓扑排序概述

对 DAG 进行拓扑排序,得到结点序列 \(\{p_i\}\),满足图上每条边的起始点比终点在序列上更靠前。形式化地,设 \(q_i\) 表示结点 \(i\)\(p\) 中的位置,那么对于图上每条有向边 \(u\to v\),均有 \(q_u < q_v\)

一个朴素的想法是,每次取入度为 \(0\) 的点加入 \(p\),然后将从 \(p\) 出发的所有边删去。因为一条边被删去时,其起始点已经加入序列,而终点没有加入序列(因为它们的入度大于 \(0\)),所以起始点相较于终点在序列上更靠前。

因为原图无环,所以在拓扑排序没有结束时,必然存在入度为 \(0\) 的点。否则,从任意一点出发走每个点的第一条出边得到无限长的结点序列。因为结点数有限,所以序列一定经过了重复的点,与原图无环矛盾。

通过 BFS 或 DFS 实现拓扑排序的过程:

  • 对于 BFS,用队列维护所有入度为 \(0\) 的点。取出队首 \(u\),删去从 \(u\) 出发的所有边 \(u\to v\)。如果删边导致某个点 \(v\) 从入度大于 \(0\) 变成入度等于 \(0\),那么将 \(v\) 入队。初始将所有入度为 \(0\) 的点入队。

  • 对于 DFS,对于当前点 \(u\),若 \(v\) 在删去 后从入度大于 \(0\) 变成入度等于 \(0\),那么向 \(vv\) DFS。初始从每个入度为 \(0\) 的点开始搜索。

时间复杂度为 \(\mathcal{O}(n + m)\)

合理性证明

考虑一个图,删掉某个入度为 \(0\) 的节点之后,如果新图可以拓扑排序,那么原图一定也可以。反过来,如果原图可以拓扑排序,那么删掉后也可以。

应用

拓扑排序可以判断图中是否有环,还可以用来判断图是否是一条链。拓扑排序可以用来求 AOE 网中的关键路径,估算工程完成的最短时间。

求字典序最大/最小的拓扑排序

将 BFS 算法中的队列替换成最大堆/最小堆实现的优先队列即可,此时总的时间复杂度为 \(O(N+M \log{M})\)

\(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;
}

\(6\)[HNOI2015] 落忆枫音

思路

如果没加入这条边的话,那么形成一棵树的方案数应为 \(\prod_{i = 2}^{n} ~~ ru_i\) ,意思就是每个点(除根结点外)都选择自己的父亲,所以方案数就为上述式子。

而如果加入这条边,就会形成多个环,但是直接求出所有环是不可能的,要采用 \(dp\) 的方式解决。我们发现只要用总方案数再除以环上的入度之积就行了。所以要先进行拓扑排序再计数即可。

#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;
}
posted @ 2024-01-21 23:47  CQWYB  阅读(29)  评论(0编辑  收藏  举报