nflsoj360【六校联合训练 省选 #2】染色问题(color)

nflsoj360【六校联合训练 省选 #2】染色问题(color)

题目大意

题面下载

nflsoj题目链接

一个长度为\(n\)的序列,进行\(m\)次染色操作,每次操作可以将一段区间染成一种新颜色(区间长度大于等于\(1\)),要求最后得到的序列每个位置上必须有颜色。 一个格子可以被多次涂色,颜色为最后涂上的颜色。

求最后不同的序列数\(\bmod 998244353\)。两个序列\(\{a_i\},\{b_i\}\)被认为不同,当且仅当\(\exist k\in[1,n]:a_k\neq b_k\)

数据范围:\(1\leq n,m\leq 10^6\)

备注:题面里有一档部分分\(n,m\leq 100\),实际数据中是\(n,m\leq 300\)

本题题解

我们先不考虑“先染的颜色会被后染的颜色覆盖”这件事。如果某种颜色在最终的序列中出现了\(x\)次,我们就直接认为在染这种颜色的时候,我们只染了\(x\)个格子。也就是说,现在每次染的颜色,在序列里不一定是连续的一段。


备注,“思路1”与正解关系不大,想看正解可以直接跳到思路2。

思路1:有一种思路是,把条件转化为,有一个长度为\(n\)的数列\(\{a_i\}\),使得对所有\(i\in [1,n]\),不存在某种个大于\(a_i\)的数,在\(i\)左右两边都出现过。

然后设\(dp[i][j]\)表示一段长度为\(i\)的序列,用到了\(j\)个颜色(注意,要求这\(j\)种颜色都真的在最终的\(i\)个位置上出现过。我们在初步转化后,已经不谈“覆盖”这件事了)。

在这种初步转化的思路下,按照每种颜色第一次和最后一次的出现位置,两种颜色要么是“并列”的关系,要么是“包含”的关系。所以转移时,枚举最左边的一整段非并列关系的区间长度,再枚举里面的颜色数量,转移要乘上组合数。

时间复杂度\(O(n^4)\)。但是这种初步转化的思路,似乎没什么优化前途。


思路2:另一种思路是,不要直接转化成对序列计数。我们还是依次进行这\(m\)个操作。那么现在每次操作,相当于在原来的操作序列上,插入了一段新颜色。也就是说,我们不考虑元素的真实位置了,只考虑目前已有的元素的相对位置关系,然后整个序列——通过在中间插入一段新颜色——在不断变长。

按此思路,也可以设计出一个DP。设\(dp[i][j]\)表示进行了前\(i\)次操作(也就是插入了前\(i\)种新颜色)后,当前序列长度为\(j\)时的方案数。转移时,枚举插入新颜色前,序列的长度:

\[dp[i][j]=[i<m]\times dp[i-1][j]+\sum_{k=0}^{i-1}dp[i-1][k]\times(k+1) \]

其中,最前面的\([i<m]\),表示除了最后一次操作外,其他操作插入的段长度都可以为\(0\);最后一次操作插入的段长度至少是\(1\),因为最后一种颜色不会被其他颜色覆盖,所以它一定要在最终的序列里出现。

时间复杂度\(O(n^3)\),可以用前缀和优化到\(O(n^2)\)


对比思路1和思路2

思路1上来就把问题转化成对序列计数。但是这样,相当于所有位置必须在一开始就固定。状态缺乏灵活性,复杂度较差。

而思路2则把问题看成是在动态地插入元素。这样相当于所有位置都是不固定的,随时可能在中间插一段进来。这正好与题目的性质相契合(两种颜色,要么并列,要么包含。所谓包含,就是在中间插入。所谓并列,就是在两边插入),所以这种状态得到了较好的复杂度。

于是接下来,我们将抛弃思路1。顺着思路2,得到正解。


观察思路2里DP的转移,主要就是\(\times(k+1)\)

发现,如果最终实际使用\(j\)种颜色,则方案数就是\(\prod_{i=1}^{n-1}(x+i+1)\)\(x^{n-j}\)项前的系数。这可以结合DP转移来理解,也可以根据具体含义理解:如果实际使用了\(j\)种颜色,每次染色前已有的序列长度为\(a_1,a_2,\dots ,a_j\),那么一定有:\(0=a_1<a_2<\dots<a_j<n\),方案数就是\(\prod_{i=2}^{n}(a_i+1)\)。总方案数,是所有这样的\(a\)序列的贡献之和,就相当于在\([2,n]\)里选出\(j-1\)个数,它们的乘积之和。这也就是\(\prod_{i=1}^{n-1}(x+i+1)\)\(x^{n-j}\)项前的系数。

\(\text{res}_j=[x^{n-j}](\prod_{i=1}^{n-1}(x+i+1))\),则最终答案就是\(\sum_{j=1}^{\min(n,m)}\text{res}_j\times{m-1\choose j-1}\)。之所以要乘\({m-1\choose j-1}\),是因为要确定选出的是哪\(j\)种颜色,并且颜色\(m\)是必选的,所以只需要确定其他\(j-1\)种颜色即可。

于是我们要求一堆小多项式的乘积。它们的乘积一定是一个\(n-1\)次的多项式。可以用分治FFT求解。(这是比较套路的用法,如果不会建议学习一下分治FFT)。

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


继续优化。考虑用倍增代替分治。

假设已经求出了\(F_{t}(x)=\prod_{i=1}^{t}(x+i+1)\)这个\(t\)次多项式。如果我们用倍增解决本题,相当于要处理两个问题:

  1. 如何从\(F_t(x)\)推出\(F_{t+1}(x)\)
  2. 如何从\(F_{t}(x)\)推出\(F_{2t}(x)\)

第1个问题比较简单:\(F_{t+1}(x)=F_{t}(x)\times(x+t+2)\)。可以线性推出来。

第2个问题,可以写成:\(F_{2t}(x)=F_{t}(x)\times F_{t}(x+t)\)

\(F_{t}(x)=\sum_{i=0}^{t}f_ix^i\),那么\(F_{t}(x+t)\)就等于\(\sum_{i=0}^{t}f_i\times(x+t)^i\)

也就是说,\(f_{0\dots t}\)这个数组是我们已经知道的。我们要通过它,求出\(F_{2t}(x)=F_{t}(x)\times F_{t}(x+t)\)

考虑先用一个数组\(a_{0\dots t}\)表示出\(F_{t}(x+t)\),再对\(a,f\)做多项式乘法。问题转化为求这个数组\(a\)。观察\(F_{t}(x+t)\)的式子,考虑用二项式定理展开:

\[\begin{align} F_{t}(x+t)&=\sum_{i=0}^{t}f_i\times(x+t)^i\\ &=\sum_{i=0}^{t}f_i\sum_{j=0}^{i}x^jt^{i-j}{i\choose j}\\ &=\sum_{j=0}^{t}x^j\sum_{i=j}^{t}f_it^{i-j}\frac{i!}{j!(i-j)!} \end{align} \]

\(b_i=f_i\times i!\)\(c_i=t^{i}\times\frac{1}{i!}\)。则:

\[a_i=\frac{1}{i!}\sum_{j=i}^{t}b_jc_{j-i} \]

这其实也是一个卷积。简单讲就是同时反转\(a,b\),就能变成通常意义上的卷积了。

用一次卷积求出\(a\),再对\(a,f\)做一次卷积求出结果。时间复杂度\(O(n\log n)\)

那么倍增的总时间复杂度就是\(T(n)=T(\frac{n}{2})+O(n\log n)=O(n\log n)\)。可以通过\(n\leq 10^6\)的数据。

参考代码:

//problem:nflsoj360
#include <bits/stdc++.h>
using namespace std;

#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fi first
#define se second
#define SZ(x) ((int)(x).size())

typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;

template<typename T>inline void ckmax(T& x,T y){x=(y>x?y:x);}
template<typename T>inline void ckmin(T& x,T y){x=(y<x?y:x);}

const int MAXN=1e6;
const int MOD=998244353;
inline int mod1(int x){return x<MOD?x:x-MOD;}
inline int mod2(int x){return x<0?x+MOD:x;}
inline void add(int& x,int y){x=mod1(x+y);}
inline void sub(int& x,int y){x=mod2(x-y);}
inline int pow_mod(int x,int i){int y=1;while(i){if(i&1)y=(ll)y*x%MOD;x=(ll)x*x%MOD;i>>=1;}return y;}

int fac[MAXN+5],ifac[MAXN+5];
inline int comb(int n,int k){
	if(n<k)return 0;
	return (ll)fac[n]*ifac[k]%MOD*ifac[n-k]%MOD;
}
void facinit(int lim=MAXN){
	fac[0]=1;
	for(int i=1;i<=lim;++i)fac[i]=(ll)fac[i-1]*i%MOD;
	ifac[lim]=pow_mod(fac[lim],MOD-2);
	for(int i=lim-1;i>=0;--i)ifac[i]=(ll)ifac[i+1]*(i+1)%MOD;
}

namespace PolyNTT{
int rev[MAXN*4+5];
int f[MAXN*4+5],g[MAXN*4+5];
void NTT(int *a,int n,int flag){
	for(int i=0;i<n;++i)if(i<rev[i])swap(a[i],a[rev[i]]);
	for(int i=1;i<n;i<<=1){
		int T=pow_mod(3,(MOD-1)/(i<<1));
		if(flag==-1) T=pow_mod(T,MOD-2);
		for(int j=0;j<n;j+=(i<<1)){
			for(int k=0,t=1;k<i;++k,t=(ll)t*T%MOD){
				int Nx=a[j+k],Ny=(ll)a[i+j+k]*t%MOD;
				a[j+k]=mod1(Nx+Ny);
				a[i+j+k]=mod2(Nx-Ny);
			}
		}
	}
	if(flag==-1){
		int invn=pow_mod(n,MOD-2);
		for(int i=0;i<n;++i)a[i]=(ll)a[i]*invn%MOD;
	}
}
void mul(int n,int m){
	int lim=1,ct=0;
	while(lim<=n+m)lim<<=1,ct++;
	for(int i=n;i<=lim;++i)f[i]=0;
	for(int i=m;i<=lim;++i)g[i]=0;//clear
	for(int i=0;i<lim;++i)rev[i]=(rev[i>>1]>>1)|((i&1)<<(ct-1));
	NTT(f,lim,1);
	NTT(g,lim,1);
	for(int i=0;i<lim;++i)f[i]=(ll)f[i]*g[i]%MOD;
	NTT(f,lim,-1);
}
}//namespace PolyNTT

typedef vector<int> Poly;

Poly operator*(const Poly& a,const Poly& b){
	if(!SZ(a) && !SZ(b))return Poly();
	Poly res;
	res.resize(SZ(a)+SZ(b)-1);
	if(SZ(a)<=50 && SZ(b)<=50){
		for(int i=0;i<SZ(a);++i)for(int j=0;j<SZ(b);++j)add(res[i+j],(ll)a[i]*b[j]%MOD);
		return res;
	}
	for(int i=0;i<SZ(a);++i)PolyNTT::f[i]=a[i];
	for(int i=0;i<SZ(b);++i)PolyNTT::g[i]=b[i];
	PolyNTT::mul(SZ(a),SZ(b));
	for(int i=0;i<SZ(res);++i)res[i]=PolyNTT::f[i];
	return res;
}
Poly& operator*=(Poly& lhs,const Poly& rhs){
	lhs=lhs*rhs;return lhs;
}
Poly operator+(const Poly& a,const Poly& b){
	Poly res;
	res.resize(max(SZ(a),SZ(b)));
	for(int i=0;i<SZ(res);++i){
		res[i]=mod1((i>=SZ(a)?0:a[i])+(i>=SZ(b)?0:b[i]));
	}
	return res;
}
Poly& operator+=(Poly& lhs,const Poly& rhs){
	lhs=lhs+rhs;return lhs;
}

int n,m;

Poly solve(int n){
	// prod_{i=1->n} (x+i+1)
	if(n==1){
		Poly res(2);
		res[0]=2;
		res[1]=1;
		return res;
	}
	if(n&1){
		// 奇数
		Poly res=solve(n-1);
		assert(SZ(res)==n);
		res.push_back(0);
		for(int i=n;i>=1;--i){
			res[i]=((ll)res[i]*(n+1) + res[i-1])%MOD;
		}
		res[0]=(ll)res[0]*(n+1)%MOD;
		return res;
	}
	// 偶数
	int len=n/2;
	Poly f=solve(len);
	assert(SZ(f)==len+1);
	Poly a,b,c;
	b.resize(len+1);
	c.resize(len+1);
	int pow_of_len=1;
	for(int i=0;i<=len;++i){
		b[i]=(ll)f[i]*fac[i]%MOD;
		c[i]=(ll)pow_of_len*ifac[i]%MOD;
		pow_of_len=(ll)pow_of_len*len%MOD;
	}
	reverse(b.begin(),b.end());
	a=b*c;
	while(SZ(a)>len+1) a.pop_back();
	reverse(a.begin(),a.end());
	for(int i=0;i<=len;++i){
		a[i]=(ll)ifac[i]*a[i]%MOD;
	}
	return f*a;
}
int main() {
	facinit();
	cin>>n>>m;
	Poly res=solve(n-1);
	int ans=0;
	for(int i=1;i<=n && i<=m;++i){
		// 实际使用了i种颜色
		ans=((ll)ans + (ll)res[n-i]*comb(m-1,i-1))%MOD;
	}
	cout<<ans<<endl;
	return 0;
}
posted @ 2020-09-02 20:24  duyiblue  阅读(512)  评论(1编辑  收藏  举报