#22 CF913F & CF1060F & CF1062F

Strongly Connected Tournament

题目描述

点此看题

解法

\(g[i]\) 表示 \(i\) 个点的竞赛图,解决它的比赛总场数期望值。转移考虑一次定向后取出入度为 \(0\) 的那个强连通块,设这个强连通块大小为 \(j\),就可以得到子问题 \(g[j]\)\(g[i-j]\)

那么如何规划那个入度为 \(0\) 的强连通块?可以拆分成两部分,第一部分是块内和其他点的连边,我们要求块内的点单向连接其他点,但是注意概率是和编号大小关系有关的,所以我们要用 \(dp\) 计算;第二部分是块内自身的连边,需要能形成强连通块。显然这两部分连边带来的概率也是独立的,并且其他连边我们可以一概不考察。

第一部分:按照编号顺序从小到大规划,设 \(dp[i][j]\) 表示考虑前 \(i\) 个点,钦定 \(j\) 个点单向连接其他点的概率:

\[dp[i][j]=dp[i-1][j-1]\cdot (1-p)^{i-j}+dp[i-1][j]\cdot p^j \]

第二部分:设 \(f[i]\) 表示 \(i\) 个点形成强连通分量的概率,转移正难则反法,减去形成更小强联通块的情况:

\[f[i]=1-\sum_{j=1}^{i-1}dp[i][j]\cdot f[j] \]

现在我们知道形成大小为 \(j\) 的强连通块的概率是 \(dp[i][j]\cdot f[j]\),利用这个也可以写出 \(g\) 的转移:

\[g[i]=\sum_{j=1}^{i}dp[i][j]\cdot f[j]\cdot (\frac{j(j-1)}{2}+j(i-j)+g[j]+g[i-j]) \]

但是注意到两边都有 \(g[i]\),所以简单地移项就可以得到最终的转移:

\[g[i]=\frac{dp[i][i]\cdot f[i]\cdot\frac{i(i-1)}{2}+\sum_{j=1}^{i-1}dp[i][j]\cdot f[j]\cdot (\frac{j(j-1)}{2}+j(i-j)+g[j]+g[i-j])}{1-dp[i][i]\cdot f[i]} \]

时间复杂度 \(O(n^2)\)

#include <cstdio>
#include <iostream>
using namespace std;
const int M = 2005;
const int MOD = 998244353;
#define int long long
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,a,b,p,p1[M],p2[M],dp[M][M],f[M],g[M];
int qkpow(int a,int b)
{
	int r=1;
	while(b>0)
	{
		if(b&1) r=r*a%MOD;
		a=a*a%MOD;
		b>>=1;
	}
	return r;
}
signed main()
{
	n=read();a=read();b=read();
	p=a*qkpow(b,MOD-2)%MOD;p1[0]=p2[0]=1;
	for(int i=1;i<=n;i++)
	{
		p1[i]=p1[i-1]*p%MOD;
		p2[i]=p2[i-1]*(MOD+1-p)%MOD;
	}
	dp[0][0]=1;
	for(int i=1;i<=n;i++)
	{
		dp[i][0]=1;
		for(int j=1;j<=i;j++)
			dp[i][j]=(dp[i-1][j-1]*p2[i-j]+
			dp[i-1][j]*p1[j])%MOD;
	}
	for(int i=1;i<=n;i++)
	{
		f[i]=1;
		for(int j=1;j<i;j++)
			f[i]=(f[i]+MOD-dp[i][j]*f[j]%MOD)%MOD;
	}
	for(int i=1;i<=n;i++)
	{
		int s=dp[i][i]*f[i]%MOD,r=i*(i-1)/2*s%MOD;
		for(int j=1;j<i;j++)
		{
			int t=j*(i-j)+j*(j-1)/2+g[j]+g[i-j];
			r=(r+t%MOD*dp[i][j]%MOD*f[j])%MOD;
		}
		g[i]=r*qkpow(MOD+1-s,MOD-2)%MOD;
	}
	printf("%lld\n",g[n]);
}

Shrinking Tree

题目描述

点此看题

解法

这题好神啊,完完全全理解都花了一个晚上(话说真的只值 *2900 么?)

约定:为了方便描述与理解,下面把编号称为颜色。

使用枚举法,考虑枚举树根,并且钦定树根就是最后保留下来的点。然后考虑树形 \(dp\),状态设计为 \(u\) 的子树中,最后节点 \(u\) 就显 \(u\) 色的概率,我们要研究 \(u,v\) 之间如何转移。

我们在 \(dp\) 的过程中计算所有断边顺序的概率总和,最后再除以 \((n-1)!\) 即可,这说明我们在 \(dp\) 过程中要规划断边顺序。进一步思考,我们 \(dp\) 的过程中还需要记录什么?一个关键的 \(\tt observation\) 是:当断开边 \(u,v\) 时,可以看成把 \(v\) 的边都接到 \(u\) 上来,此时将会产生限制:这些边都不能使得 \(u\) 变色,并且这些问题可以化归到子问题,也就是等效成:单独考虑 \(v\) 子树时,这些边都不能使 \(v\) 变色

注意这些限制是断开边 \((u,v)\) 时刻后产生的,所以我们的状态中需要记录时刻。设 \(f_{u,i}\) 表示后 \(i\) 个时刻(也就是删除顺序最靠后的 \(i\) 条边)不能使 \(u\) 变色的概率,拿一个辅助数组 \(g_i\) 表示只考虑子树 \(v\) 和边 \((u,v)\),后 \(i\) 个时刻不能使 \(u\) 变色的概率。根据我们上面的观察,可以枚举边 \((u,v)\) 断开的时刻 \(j\)

  • 如果 \(j\leq i\),说明边 \((u,v)\) 的断开在我们的考虑范围内,由于断开后需要显 \(u\) 色,要乘上概率 \(\frac{1}{2}\),并且后 \(j-1\) 条边会被改接到 \(u\) 上,它们不能使 \(u\) 变色,其他边都是无限制的,所以 \(g_i\leftarrow \frac{1}{2}\cdot f_{v,j-1}\)
  • 如果 \(j>i\),说明边 \((u,v)\) 的断开不在我们的考虑范围内,这条边会提前断开,但是仍然需要考虑那些改接到 \(u\) 上的边,所以 \(g_i\leftarrow f_{v,i}\)

求出辅助数组之后,直接做背包合并就可以得到新的 \(f_u\),因为我们还需要规划断边的顺序,而子树间的断边是互不影响的,所以可以直接用组合数计算方案数,保证考虑到的边和没考虑到的边之间的顺序即可:

\[f'_{u,i+j}\leftarrow f_{u,i}\cdot g_j\cdot{i+j\choose i}\cdot{siz_u-i-1+siz_v-j\choose siz_v-j} \]

结合定义,最后得到颜色 \(i\) 的概率就是点 \(i\) 为根做 \(dp\)\(\frac{f_{i,n-1}}{(n-1)!}\)

暴力实现时间复杂度 \(O(n^4)\),简单前缀和优化可以做到 \(O(n^3)\),不过这些都不是重点了。

总结

我还是不会熟练应用枚举法,当问题看上去无法下手时可以尝试一下;此外无根树考虑定根是重要的技巧(本题的应用体现在之规划保留根的颜色的概率)

规划染色类问题时,限制可能源于钦定不变色,依照这个限制进行规划是有道理的。

#include <cstdio>
#include <vector>
#include <cstring>
#include <iostream>
using namespace std;
const int M = 55;
#define db double
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,siz[M];vector<int> G[M];
db f[M][M],C[M][M],g[M],h[M],fac[M];
void dfs(int u,int fa)
{
	f[u][0]=1;siz[u]=1;
	for(int v:G[u]) if(v^fa)
	{
		dfs(v,u);
		memset(g,0,sizeof g);
		for(int i=0;i<=siz[v];i++)
			for(int j=1;j<=siz[v];j++)
			{
				if(j<=i) g[i]+=f[v][j-1]/2;
				else g[i]+=f[v][i];
			}
		for(int i=0;i<siz[u];i++)
			for(int j=0;j<=siz[v];j++)
				h[i+j]+=f[u][i]*g[j]*C[i+j][i]
				*C[siz[u]-i-1+siz[v]-j][siz[v]-j];
		siz[u]+=siz[v];
		for(int i=0;i<siz[u];i++)
			f[u][i]=h[i],h[i]=0;
	}
}
signed main()
{
	n=read();fac[0]=1;
	for(int i=1;i<=n;i++) fac[i]=fac[i-1]*i;
	for(int i=0;i<=n;i++)
	{
		C[i][0]=1;
		for(int j=1;j<=i;j++)
			C[i][j]=C[i-1][j-1]+C[i-1][j];
	}
	for(int i=1;i<n;i++)
	{
		int u=read(),v=read();
		G[u].push_back(v);
		G[v].push_back(u);
	}
	for(int i=1;i<=n;i++)
	{
		memset(f,0,sizeof f);
		dfs(i,0);
		printf("%.10lf\n",f[i][n-1]/fac[n-1]);
	}
}

Upgrading Cities

题目描述

点此看题

解法

考虑求出 \(s[u]\) 表示 \(u\) 可以到达的节点数和可以到达 \(u\) 的节点数,那么 \(s[u]\geq n-2\) 的点 \(u\) 就是重要的或者次重要的。但是具体地求出这个东西至少都要 \(O(\frac{n^2}{w})\),我们可能要在计算的时候舍弃一些东西。

还是考虑拓扑排序,排序过程中有一个关键性质:同在队列中的点没有任何到达关系。所以如果队列中的元素 \(\geq 3\) 个,说明其中的元素不可能成为答案了,这时候我们就不需要计算出它们的 \(s\),直接忽略它们即可。剩下的情况可以讨论:

  • 如果队列中只有一个元素 \(u\),设已经访问到的元素有 \(t\) 个,那么 \(s[u]\leftarrow s[u]+n-t\)
  • 如果队列中有两个元素 \(u,v\),我们要判断 \(u\) 是不是能到达除 \(v\) 以外的所有被访问点(否则 \(u\) 被忽略),那么如果存在和 \(v\) 直接相连的点 \(x\),使得 \(deg[x]=1\),那么 \(x\) 一定不能被 \(u\) 到达,可以直接忽略 \(u\);如果不存在这样的 \(x\),说明后面的所有点都是可以从 \(u\) 到达的,那么 \(s[u]\leftarrow s[u]+n-t-1\)

正反两边拓扑排序就可以求出答案,时间复杂度 \(O(n+m)\)

#include <cstdio>
#include <iostream>
#include <queue>
using namespace std;
const int M = 300005;
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,m,ans,a[M],b[M],d[M],s[M];vector<int> g[M];
void tpsort()
{
	queue<int> q;int t=0;
	for(int i=1;i<=n;i++)
		if(!d[i]) q.push(i);
	while(!q.empty())
	{
		int u=q.front();q.pop();t++;
		if(q.size()==0) s[u]+=n-t;
		else if(q.size()==1)
		{
			int v=q.front(),f=1;
			for(int x:g[v]) if(d[x]==1)
				{f=0;break;}
			if(f) s[u]+=n-t-1;
		}
		for(int v:g[u])
			if(--d[v]==0) q.push(v);
	}
}
signed main()
{
	n=read();m=read();
	for(int i=1;i<=m;i++)
	{
		a[i]=read();b[i]=read();
		g[a[i]].push_back(b[i]);d[b[i]]++;
	}
	tpsort();
	for(int i=1;i<=n;i++) g[i].clear();
	for(int i=1;i<=m;i++)
		g[b[i]].push_back(a[i]),d[a[i]]++;
	tpsort();
	for(int i=1;i<=n;i++)
		if(s[i]>=n-2) ans++;
	printf("%d\n",ans);
}
posted @ 2022-05-30 15:24  C202044zxy  阅读(346)  评论(5编辑  收藏  举报