[CQOI2021模拟]染色

题目

题目描述
有一个长度为 n n n 的序列,用 m m m 种颜色依次染色,每次染色的范围是一个非空区间,范围内的所有格子都会变成当前颜色(无论是否被某种颜色染色过)。

现在神仙 y y b \sf yyb yyb 想问你,如果要让序列的每个格子都被染过色,最终的局面有多少种不同的情况?——最终局面不同,当且仅当存在一个格子,它在两个局面中的颜色不同。

数据范围与提示
对于 80 % 80\% 80% 的数据, n , m ≤ 2 × 1 0 5 n,m\le 2\times 10^5 n,m2×105;对于 100 % 100\% 100% 的数据, n , m ≤ 1 0 6 n,m\le 10^6 n,m106

提示:本题比较有趣,需要火眼金睛。

思路

只要两个相等的值之间,没有比二者更小的数就行。比如 2 , … , 1 , … , 2 2,\dots,1,\dots,2 2,,1,,2 就是必然不可以的。

但是还有个问题:每种颜色必须染。其实只要 m m m 出现过就行,它可以盖住一切污秽。

考虑从小到大把数字塞进去?假如当前已经塞了 x x x 个数字,那么新塞进来的 y y y 个数字就不能夹住它们,只能在 x + 1 x+1 x+1 个空位里面选一个。

如果 d p \tt dp dp 就是
f ( i + 1 , j ) = f ( i , j ) + ∑ t = 0 j − 1 ( t + 1 ) f ( i , t ) f(i+1,j)=f(i,j)+\sum_{t=0}^{j-1}(t+1)f(i,t) f(i+1,j)=f(i,j)+t=0j1(t+1)f(i,t)

最终答案就是 f ( m , n ) − f ( m − 1 , n ) f(m,n)-f(m-1,n) f(m,n)f(m1,n)(要确保 m m m 出现过),初值 f ( 0 , 0 ) = 1 f(0,0)=1 f(0,0)=1 。可是这个 d p \tt dp dp 怎么优化呢?

把它想象成坐标系内行走。可以从 ( i , j ) (i,j) (i,j) 走到 ( i + 1 , j ) (i+1,j) (i+1,j),也可以从 ( i , j ) (i,j) (i,j) 走到 ( i + 1 , t )    ( t > j ) (i+1,t)\;(t>j) (i+1,t)(t>j) 并且乘一个 j + 1 j+1 j+1 的系数。你会惊讶地发现,乘过一次 j + 1 j+1 j+1 就不会再乘 了,因为 t t t 在变大。那么容易想象到,你最终会乘 k k k [ 1 , n ] [1,n] [1,n] 内的互不相同的数。可是有多少种情况呢?

算情况数,其实就是确定 ( i , j ) → ( i + 1 , t ) (i,j)\rightarrow(i+1,t) (i,j)(i+1,t) 中的 i i i 罢了。因为有 i + 1 i+1 i+1 的兜底操作,任何一个递增的行序列都是可以的。转化为差值之后是经典隔板法,即 ( m − 1 k − 1 ) {m-1\choose k-1} (k1m1) 嘛。

你发现这只跟 k k k 有关, k k k 个数字具体是什么无关。那么我们只需要求出,任选 k k k 个数求乘积的和,最后乘这个组合数就行了。

这东西显然可以生成函数。即 [ x k ] ∏ i = 1 n ( 1 + i x ) [x^k]\prod_{i=1}^{n}(1+ix) [xk]i=1n(1+ix) 嘛。为了方便,可以取 x = x − 1 x=x^{-1} x=x1 [ x n − k ] ∏ i = 1 n ( i + x ) [x^{n-k}]\prod_{i=1}^{n}(i+x) [xnk]i=1n(i+x) 。反正对所有 k k k 都要算,直接把多项式计算出来就行。

我们有一个显然的 n log ⁡ 2 n n \log^2 n nlog2n 分治 N T T \tt NTT NTT 做法。但是这个式子很特殊,能不能更快呢?

仍然考虑分治,我们已经求出了 F n ( x ) = ∏ i = 1 n ( i + x ) F_n(x)=\prod_{i=1}^{n}(i+x) Fn(x)=i=1n(i+x),欲求 F 2 n ( x ) F_{2n}(x) F2n(x),其实可以
F 2 n ( x ) = F n ( x ) F n ( x + n ) F 2 n ( x ) F n ( x ) = ∑ i = 0 n b i ( x + n ) i = ∑ i = 0 n x i i ! ∑ j = i n ( b j ⋅ j ! ) ⋅ n j − i ( j − i ) ! \begin{aligned} F_{2n}(x) &= F_n(x)F_n(x+n)\\ { F_{2n}(x) \over F_n(x) } &= \sum_{i=0}^{n}b_i(x+n)^i\\ &= \sum_{i=0}^{n}\frac{x^i}{i!}\sum_{j=i}^{n} \left(b_j\cdot j!\right)\cdot \frac{n^{j-i}}{(j-i)!} \end{aligned} F2n(x)Fn(x)F2n(x)=Fn(x)Fn(x+n)=i=0nbi(x+n)i=i=0ni!xij=in(bjj!)(ji)!nji

上面只用了二项式展开,没有任何奇技淫巧。然后这就得到了卷积的形式,就是
T ( n ) = T ( n 2 ) + O ( n log ⁡ n ) = O ( n log ⁡ n ) T(n)=T\left(\frac{n}{2}\right)+\mathcal O(n\log n)=\mathcal O(n\log n) T(n)=T(2n)+O(nlogn)=O(nlogn)

代码

反而是超级好打的一道题。

#include <cstdio>
#include <iostream>
#include <cstring>
using namespace std;
typedef long long int_;
inline int readint(){
	int a = 0; char c = getchar(), f = 1;
	for(; c<'0'||c>'9'; c=getchar())
		if(c == '-') f = -f;
	for(; '0'<=c&&c<='9'; c=getchar())
		a = (a<<3)+(a<<1)+(c^48);
	return a*f;
}

const int Mod = 998244353;
inline int qkpow(int_ b,int_ q){
	int_ a = 1; // avoid type conversion
	for(; q; q>>=1,b=b*b%Mod)
		if(q&1) a = a*b%Mod;
	return a;
}

const int LogMod = 30;
int g[2][LogMod], inv2[LogMod];
void prepare(){
	int p = Mod-1, x = 0;
	inv2[0] = 1, inv2[1] = (Mod+1)>>1;
	for(; !(p&1); p>>=1,++x)
		inv2[x+1] = 1ll*inv2[x]*inv2[1]%Mod;
	g[1][x] = qkpow(3,p);
	g[0][x] = qkpow(g[1][x],Mod-2);
	for(; x>1; --x){
		g[1][x-1] = 1ll*g[1][x]*g[1][x]%Mod;
		g[0][x-1] = 1ll*g[0][x]*g[0][x]%Mod;
	}
}

const int MaxN = 2000005;
int went[MaxN];
void NTT(int a[],int n,int opt){
	for(int i=1; i<(1<<n); ++i)
		if(i < went[i]) // solved outside
			swap(a[i],a[went[i]]);
	for(int w=1,x=1; x<=n; w<<=1,++x)
	for(int *p=a; p!=a+(1<<n); p+=(w<<1))
	for(int i=0,v=1; i<w; ++i){
		int t = 1ll*v*p[i+w]%Mod;
		p[i+w] = (p[i]+Mod-t)%Mod;
		p[i] = (p[i]+t)%Mod;
		v = 1ll*v*g[opt][x]%Mod;
	}
	if(!opt) for(int i=0; i<(1<<n); ++i)
		a[i] = 1ll*a[i]*inv2[n]%Mod;
}
void multiply(int a[],int b[],int n){
	for(int i=1; i<(1<<n); ++i)
		went[i] = ((i&1)<<n>>1)
			| (went[i>>1]>>1);
	NTT(a,n,1), NTT(b,n,1);
	for(int i=0; i<(1<<n); ++i)
		a[i] = 1ll*a[i]*b[i]%Mod;
	NTT(a,n,0); // won't save B
}

int inv[MaxN], logtwo[MaxN];
inline int prepare(int n){
	inv[1] = 1; logtwo[1] = 0;
	for(int i=2; i<=n; ++i){
		inv[i] = (0ll+Mod-Mod/i)*inv[Mod%i]%Mod;
		logtwo[i] = logtwo[i>>1]+1;
	}
}

int res[MaxN], tmp1[MaxN], tmp2[MaxN];
void solve(int n){
	if(n == 1){
		res[0] = res[1] = 1;
		return ; // F_1(x) = x+1
	}
	if(n&1){
		solve(n-1); // F_{n-1}(x)
		for(int i=n; i>=1; --i)
			res[i] = (res[i-1]+
				1ll*n*res[i])%Mod;
		res[0] = 1ll*res[0]*n%Mod;
		return ; // *= (x + n)
	}
	solve(n >>= 1); tmp2[n] = 1;
	for(int i=n-1; i>=0; --i)
		tmp2[i] = 1ll*tmp2[i+1]*n
			%Mod*inv[n-i]%Mod;
	for(int i=0,jc=1; i<=n; ++i){
		tmp1[i] = 1ll*res[i]*jc%Mod;
		jc = 1ll*jc*(i+1)%Mod;
	}
	int N = logtwo[n<<1]+1;
	multiply(tmp1,tmp2,N);
	for(int i=0,jc=1; i<=n; ++i){
		tmp1[i] = 1ll*tmp1[i+n]*jc%Mod;
		jc = 1ll*jc*inv[i+1]%Mod;
	}
	for(int i=n+1; i<(1<<N); ++i)
		tmp1[i] = 0; // useless part
	multiply(res,tmp1,N);
	for(int i=(n<<1|1); i<(1<<N); ++i)
		res[i] = 0; // too much
	memset(tmp1,0,(1<<N)<<2); // clear
	memset(tmp2,0,(1<<N)<<2);
}

int getAns(int n,int m){
	memset(res,0,MaxN<<2); // avoid UKE
	solve(n); int ans = 0;
	for(int i=0,c=1; i<=n&&i<m; ++i){
		ans = (ans+1ll*c*res[n-i])%Mod;
		c = 1ll*c*(m-1-i)%Mod*inv[i+1]%Mod;
	}
	return ans;
}

int main(){
//	freopen("color.in","r",stdin);
//	freopen("color.out","w",stdout);
	int n = readint(), m = readint();
	prepare(); prepare(n+1);
	printf("%d\n",(getAns(n,m)-
		getAns(n,m-1)+Mod)%Mod);
	return 0;
}

后记

宋队有个生成函数的做法。请看集训队论文一般的存在。一模一样的 d p \tt dp dp 式,双倍经验可以直接暴力……

染色问题是指给定一个无向图,为每个节点分配一种颜色,使得相邻节点的颜色不相同。这是一个NP完全问题,因此没有确定性的多项式时间算法可以解决它。因此,我们可以考虑使用启发式算法来解决这个问题。其中一种经典的算法是模拟退火算法。 模拟退火算法是一种元启发式优化算法,它可以在大规模、复杂的问题中找到较好的解,它利用了自然界中的物理过程来搜索解空间。模拟退火算法是一种基于马尔科夫链蒙特卡罗方法的随机优化算法。它从一个随机的解开始,在搜索过程中接受更劣的解,以便能够跳出局部最优解并最终找到全局最优解。 模拟退火算法的基本步骤如下: 1.初始化当前状态为一个随机解 2.设置初始温度和结束温度 3.在每个温度下,对当前解进行局部搜索,找到一个新的解 4.计算新解的代价和当前解的代价之差 5.如果新解的代价小于当前解的代价,则接受新解作为当前解 6.如果新解的代价大于当前解的代价,则以一定的概率接受新解作为当前解 7.降低温度 8.重复步骤3-7,直到达到结束温度 在图染色问题中,我们可以将每个节点的颜色作为状态。我们可以将初始温度设置为一个较高的值,以便接受更劣的解。在每个温度下,我们可以随机选择一个节点,将其颜色更改为另一个随机的颜色,并计算新解的代价。这里代价可以定义为相邻节点颜色相同的数量。如果新解的代价小于当前解的代价,则接受新解。如果新解的代价大于当前解的代价,则以一定的概率接受新解,以便跳出局部最优解。降低温度的过程可以使用线性或指数下降的方式。 最后,模拟退火算法的输出是一个解,它可能是全局最优解,也可能是局部最优解。因此,我们可以多次运行模拟退火算法,以便找到更好的解。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值