AtCoder Regular Contest 124


这场题质量真的高,我愿称之为 \(\tt Atcoder\ Regular\ Counting\ 124\)

E.Pass to Next

题目描述

点此看题

人和人是不能一概而论的,因为 \(\tt zxy\) 不知道想了多久的问题被小减一语道破天机。

\(n\) 个人排成一个环玩传球游戏,第 \(i\) 个人初始有的球数是 \(a_i\),每个人同时把球传给下一个人(第 \(n\) 个人传给第 \(1\) 个人),对所有最后可能形成的不同局面,设第 \(i\) 个人最终的球数是 \(b_i\),我们想求出:

\[\sum_{b}\prod_{i=1}^nb_i \]

\(3\leq n\leq 10^5,a_i\leq 10^9\)

解法

首先考虑如何不算重,因为每个人乱传球最后形成的局面可能相同。设 \(x_i\) 表示第 \(i\) 个人传出去的球数,那么如果所有 \(x\) 都为正,我们把所有 \(x\)\(1\) 得到的局面相同,那么说明只要保证至少有一个人不传球就可以不算重。

发现这个限制是很宽松的,先不管这个限制,我们来思考 \(\prod b_i\) 这东西怎么算。考虑它的组合意义是在每个局面中,从每个人最终的球中拿走一个的方案数,注意这个计数问题有两个要素:确定局面和选球,我们考虑用 \(dp\) 来解决这个问题。

球数肯定不能直接塞状态中,那么我们记录什么呢?考虑对于第 \(i\) 个人所选的球只有两种来源:自己原来就有的,别人传过来的。那么我们用拆分的方法把这两种情况分开来讨论,因为原来有的只和下一个人有关,传过来的只和上一个人有关,那么就达到了简化问题的目的。

\(dp[i][0]\) 表示第 \(i\) 个人选原有的球,考虑前 \(i-1\) 个人选球的方案数,\(dp[i][1]\) 表示第 \(i\) 个人选传过来的球,考虑前 \(i\) 个人选球的方案数,这样设计状态有利于 \(dp[i][0]\)\(dp[i+1][1]\) 的转移,\(i\) 原有的球和传给 \(i+1\) 的球是紧密相关的,所以 \(dp[i][0]\) 就暂时不能算 \(i\) 选球的方案数,然后我们考虑状态之间的 \(4\) 种转移:

  • \(s(n)=\sum_{i=1}^ni\ ,\ g(n)=\sum_{i=1}^ni^2\)

  • \(dp[i+1][0]\leftarrow dp[i][0]\),因为第 \(i\) 个人的选择还没有算进去:\(dp[i+1][0]\leftarrow dp[i][0]\cdot s(a_i)\)

  • \(dp[i+1][0]\leftarrow dp[i][1]\),不用考虑决策,直接算传球的方案即可:\(dp[i+1][0]\leftarrow dp[i][1]\cdot(a_i+1)\)

  • \(dp[i+1][1]\leftarrow dp[i][0]\),考虑第 \(i\) 个人如果递送 \(x\) 个,那么第 \(i+1\) 个人有 \(x\) 种选择方案,那么方案是 \(\sum (a_i-x)x\)\(dp[i+1][1]\leftarrow dp[i][0]\cdot(a_i\cdot s(a_i)-g(a_i))\)

  • \(dp[i+1][1]\leftarrow dp[i][1]\),考虑递送过去的数量:\(dp[i+1][1]\leftarrow dp[i][1]\cdot s(a_i)\)

那么我们先算出没有限制的答案,然后减去每个位置都递送球的答案,对于第一二种转移将 \(a_i\)\(1\) 计算即可。

考虑有环怎么初始化,我们先让 \(dp[1][0]=1\),跑环之后得到 \(dp[1][0]-1\) 就是第一个人选原有球的答案。再让 \(dp[1][1]=1\),得到 \(dp[1][1]-1\) 就是第一个人选传过来球的答案,两种情况要分开讨论避免互相转移(第一个人又选原有球又选递送球)

总结

寻找答案式的组合意义可以用于计数。

学会用拆分的方法来解决计数问题,可能的适用情况:情况数有限,拆分之后易于考虑;拆分之后的状态不会互相影响,满足乘法原理;记录某种状态另一状态就没用了,可以拆分来设计状态。

#include 
#include 
const int MOD = 998244353;
const int M = 100005;
#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,i2,i6,a[M],dp[M][2];
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;
}
int get(int C,int D)
{
	memset(dp,0,sizeof dp);
	dp[1][0]=C;dp[1][1]=C^1;
	for(int i=1;i<=n;i++)
	{
		int t=i%n+1,b=a[i]-D;
		int c=b*(b+1)%MOD*i2%MOD,d=0,e=0;
		dp[t][0]=(dp[t][0]+dp[i][0]*c)%MOD;
		dp[t][0]=(dp[t][0]+dp[i][1]*(b+1))%MOD;
		if(D) b++;
		c=b*(b+1)%MOD*i2%MOD;
		d=b*(b+1)%MOD*(2*b+1)%MOD*i6%MOD;
		e=(c*b-d)%MOD;
		dp[t][1]=(dp[t][1]+dp[i][0]*e)%MOD;
		dp[t][1]=(dp[t][1]+dp[i][1]*c)%MOD;
	}
	return (C?dp[1][0]:dp[1][1])-1;
}
signed main()
{
	n=read();
	i2=qkpow(2,MOD-2);
	i6=qkpow(6,MOD-2);
	for(int i=1;i<=n;i++)
		a[i]=read();
	int ans=get(1,0)+get(0,0)-get(1,1)-get(0,1);
	printf("%lld\n",(ans%MOD+MOD)%MOD);
}

F.Chance Meeting

题目描述

点此看题

人和人是不能一概而论的,因为 \(\tt zxy\) 不知道想了多久的问题被标杆一语道破天机。

给你一个 \(n\times m\) 的矩阵,\(\tt zxy\)\((1,1)\),标杆在 \((n,1)\),因为校长的要求,\(\tt zxy\) 需要通过下移和右移走到 \((n,m)\),而标杆需要通过上移和右移走到 \((1,m)\);但是 \(\tt zxy\) 因为害怕被报告给校长,所以 \(\tt zxy\) 只希望和标杆碰面恰好一次。

设四种移动操作互不相同,如果我们把它写成序列有多少种本质不同的方案?答案对 \(998244353\) 取模。

\(2\leq n,m\leq 2\times 10^5\)

解法

先搞一个 \(\tt observation\):因为两人总是会走到同一行,所以限制出现在是否能在此时走到同一列,发现行对列没有任何限制,所以我们只需要考虑 \(\tt zxy\) 走到 \(n\) 再和标杆恰好相遇一次的情况,然后使用乘法原理考虑走到其他行的情况。

为了方便表示我们把 \(n,m\) 都减 \(1\),设 \(f(x)\) 表示 \(\tt zxy\) 往下走 \(n\) 步之后和标杆在 \((n,x)\)第一次相遇的方案。我们把整个过程拆分两部分,首先需要从 \(2n\) 步纵向移动选出 \(n\) 步,第二部分本来是两人纵向分开到终点的过程,但是可以看成从终点走到汇合的的过程,而且这个反转过后的过程也满足需要第一次在 \((n,x)\) 处相遇,所以答案是:

\[{2n\choose n}\sum_{i=0}^m f(i)\cdot f(m-i) \]

现在的问题是计算 \(f(x)\),设 \(g(x)\) 表示 \(\tt zxy\) 向下走 \(n\) 步之后和标杆在 \((n,x)\) 相遇的方案,计算时需要插入纵向的移动:

\[g(x)={2x+n\choose n}{2x\choose x} \]

\(f(x)\) 时考虑容斥,也就是枚举 \(i\) 作为 \(x\) 前面第一个相遇的位置,那么 \((i,x)\) 这一段就不能再相遇了,需要考虑谁在这个过程中先走了一步,那么另一个人就会走最后一步,中间的过程不能越过 \(y=x\),所以就转化为了卡特兰数 \(C(x-i-1)\)

\[f(x)=g(x)-2\sum_{i=0}^{x-1} g(i)\cdot C(x-i-1) \]

然后用 \(\tt NTT\) 卷一下就做出来了,时间复杂度 \(O(n\log n)\)

总结

首先排除无关变量很重要,如果把行混在一起就很难搞。

然后就是考虑方案是什么样子的,比如这道题有多次相遇的情况,我们就只在第一次相遇的时候统计。

还有一点是设计函数来计数,比如此题觉得第一次相遇这个方案难以表示的时候就设计了函数 \(f(x)\),再考虑计算 \(f(x)\) 即可,如果不设计函数直接列式的话就会很麻烦。

#include 
#include 
using namespace std;
const int M = 1000005;
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,m,fac[M],inv[M],rev[M],g[M],c[M],f[M];
void init(int n)
{
	fac[0]=inv[0]=inv[1]=1;
	for(int i=2;i<=n;i++) inv[i]=inv[MOD%i]*(MOD-MOD/i)%MOD;
	for(int i=2;i<=n;i++) inv[i]=inv[i-1]*inv[i]%MOD;
	for(int i=1;i<=n;i++) fac[i]=fac[i-1]*i%MOD;
}
int C(int n,int m)
{
	if(n0)
	{
		if(b&1) r=r*a%MOD;
		a=a*a%MOD;
		b>>=1;
	}
	return r;
}
void NTT(int *a,int len,int op)
{
	for(int i=0;i>1]>>1)|((i&1)*(len/2));
		if(i