动态规划题单3

从这个题单开始将用 \(a_{i,j}\) 而不是 \(a[i][j]\) 来表示数组。

61.[CEOI2020] 星际迷航

来自 动态规划题单

\(D=0\) 时,我们思考怎么求出舰长是否必胜:
因为我们只能从 \(1\) 出发,所以如果我们以 \(1\) 为根,我们肯定只能一直往下走。
所以可以树形 DP,设 \(p_i\) 表示 \(i\) 点是否为先手必胜点 (下面简称为必胜点,必败点同理)。
根据 SG 函数的相关知识 \(p_i=mex_{j \in son(i)}\)
最后就是看 \(p_1\) 是否为 true

\(D=1\) 时:
我们需要把第二棵树的一个点 \(y\) 接到第一棵树的一个点 \(x\) 下,所以如果我们选择走 \((x,y)\) 这条星门,那么走到第二棵树后就是以 \(y\) 为根了,所以我们还需要求出以每一个点为根时,\(p\) 数组的情况,我们先假设我们做 \(n\) 次树形 DP \(O(n^2)\) 地来求。
下面将以 \(p_{rt,i}\) 表示以 \(rt\) 为根时,\(i\) 是否为必胜点 (只有一棵树,即 \(D=0\))。
还是考虑树形 DP,设 \(g_i\) 表示在 \(D=1\) 时,以 \(1\) 为根,将第二棵树的一个点 \(y\) 拉过来当做 \(i\) 子树内一个点的儿子,使 \(i\) 成为必胜点(\(i\) 可以原来就是必胜点)的方案数。
首先如果 \(y\) 是一个必胜点,即 \(p_{y,y}=true\),那么把它接过来没有任何影响,所以我们在 \(dp\) 状态中认为 \(y\) 是一个必败点。(这里 \(y\) 假设给定,即计算方案数时不需要考虑 \(y\) 到底是第二棵树的哪个点)。
下面除了 \(y\) 节点,节点均指在原树中的 (即第一棵树)。

  1. 如果 \(i\) 原来有 \(\ge 2\) 个子节点 \(j\) 是必败点:
    那么把 \(y\) 接过来不会有任何影响,因为 \(y\) 至多改变一个儿子的状态,\(g_i=size_i\)
  2. 如果 \(i\) 原来有 \(1\) 个儿子 \(j\) 是必败点:
    (1) 那么我要么不把 \(y\) 接在 \(j\) 的子树里,让他还是必败点,方案数 \(=size_i-size_j\)
    (2) 要么把 \(y\) 接在 \(j\) 的子树里,但是要使 \(j\) 还是必败点,方案数 \(=size_j-g_j\)
    综上 \(g_i=size_i-g_j\)
  3. 如果 \(i\) 原来一个儿子都不是必败点:
    那么我要使得让他的一个儿子变成必败点,\(g_i=1 + \sum (size_j - g_j) = size_i - \sum g_j\)(最开始 \(+1\) 是因为可以直接接在 \(i\) 下面)。

如果设 \(s_1\) 表示 \(p_{y,y}=true\)\(y\) 的数量,\(s_0\) 表示 \(p_{y,y}=false\)\(y\) 的数量,那么有:
\(ans = n\times s_1 \times p_{1,1} + g_1\times s_0\)
意思是,如果以 \(1\) 为根时,\(1\) 点是必胜点(即 \(p_{1,1}=true\)),那么第二棵树里 \(s_1\) 中的点可以随便连,否则不能连。而 \(s_0\) 中的点只能连在可以使 \(1\) 点成为必胜点的那 \(g_1\) 个点中。

再次提醒\(p_{rt,i},s_0,s_1\) 这些量都只是针对原树而言,并没有其他树连进来;\(g\) 只考虑的是 \(D=1\) 的情况。
时间复杂度 \(O(n^2)\)

在下面的讨论中由于我们只用到了每一个 \(p_{rt,rt}\),所以将以 \(p_{rt}\) 代替 \(p_{rt,rt}\)

\(D>1\) 时:
直觉告诉我们肯定是从后往前 DP,设 \(f_{i,0}\) 表示遍历到第 \(i\) 棵树,从第 \(i\) 棵树中选出一个必败点连向第 \(i-1\) 棵树(具体连第 \(i-1\) 棵树的哪个点未定)的方案数。
\(f_{i,1}\) 则是选出一个必胜点连向第 \(i-1\) 棵树。
由于我们每一次仅仅只需要挑出一个必胜点或必败点,这意味着我们需要求出以每一个 \(x\) 为根时,\(g_x\) 的值,因此下面的讨论将以 \(g_x\) 表示以 \(x\) 为根时,\(g_x\) 的值,而不是以 \(1\) 为根。
我们还是先暴力地做 \(n\) 次树形 DP 来得到这个东西。

  1. 如果第 \(i+1\) 棵树向第 \(i\) 棵树连进来的是必胜点,那么相当于没有连,所以从第 \(i\) 棵树选出一个必败点的方案为 \(s_0\),选出一个必胜点的方案为 \(s_1\),所以:
    \(f_{i+1,1}\times n\times s_0 \to f_{i,0}\)
    \(f_{i+1,1}\times n\times s_1 \to f_{i,1}\)
    因为我们的 dp 状态里规定了:具体连第 \(i-1\) 棵树的哪个点未定,所以第 \(i+1\) 棵树先随便连一个点,再从那 \(s_0/s_1\) 个点中选出一个点连向 \(i-1\)
  2. 如果第 \(i+1\) 棵树向第 \(i\) 棵树连进来的是必败点,那让一个点 \(x\) 成为必胜点的方案有 \(g_x\) 种,成为必败点的方案有 \(n-g_x\) 种,所以:
    \(f_{i+1,0}\times \sum g_x \to f_{i,1}\)
    \(f_{i+1,0}\times \sum (n-g_{x}) \to f_{i,0}\)

综上,得到转移方程:
\(f_{i,0}=f_{i+1,1}\times n\times s_0 + f_{i+1,0}\times \sum(n-g_x)\)
\(f_{i,1}=f_{i+1,1}\times n\times s_1 + f_{i+1,0}\times \sum g_x\)
容易预处理出,\(sum_1=\sum g_x,sum_0=\sum (n-g_x) = n^2 - sum_1\),所以转移是 \(O(1)\) 的。
边界:\(f_{D,0}=s_0,f_{D,1}=s_1\)
答案:\(ans = n \times f_{1,1}\times p_1 + f_{1,0}\times g_1\)
时间复杂度 \(O(n^2+D)\)

设计完状态就是一些关于优化的套路了,我们来看要优化什么:

  1. \(p_x\)
  2. \(g_x\)
  3. \(f_{i,0/1}\)

\(1,2\) 直接换根 DP:
\(up_i\) 表示以 \(fa_i\) 为根时,去掉 \(i\) 这棵子树,\(p_{fa_i}\) 的值。
\(h_i\) 表示以 \(fa_i\) 为根时,去掉 \(i\) 这棵子树,\(g_{fa_i}\) 的值。
那么就可以用 \(i\) 号节点儿子的 \(p\)\(up_i\) 更新 \(p_i\),儿子的 \(g_i\)\(h_i\) 更新 \(g_i\),更新方法类似。
那怎么求 \(up\)\(h\) 呢?
会发现 \(up_i\) 只需要用 \(up_{fa_i}\)\(p_u\) ( \(u\) 是除了 \(i\) 以外 \(fa_i\) 的儿子) 类似更新。
\(h_i\) 只需要用 \(h_{fa_i}\)\(g_u\) ( \(u\) 是除了 \(i\) 以外 \(fa_i\) 的儿子) 类似更新即可。

再来看 \(3\) 的转移:
\(f_{i,0}=f_{i+1,1}\times n\times s_0 + f_{i+1,0}\times sum_0\)
\(f_{i,1}=f_{i+1,1}\times n\times s_1 + f_{i+1,0}\times sum_1\)
这就非常的矩阵快速幂,因为只跟上一层的状态和 \(n,s_0,sum_0,s_1,sum_1\) 有关。
转移矩阵就不写了,手推即可。

DP 好题!点赞。

时间复杂度:\(O(n + \log D)\)

code

#include<bits/stdc++.h>
#define int long long 
using namespace std;
const int N=1e5+5,mod=1e9+7;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,D;
int tot,head[N],to[N<<1],Next[N<<1]; 
void add(int u,int v){
	to[++tot]=v,Next[tot]=head[u],head[u]=tot;
}

int Size[N],fa[N],p[N],g[N]; 
vector<int> son[N];
vector<int> V[N];   //V[u] 保存满足 v 是 u 的儿子,且 p[v]=0 的 v
int Sumg[N];  //保存儿子的 g 的和 
void dfs1(int u,int Fa){
	fa[u]=Fa;
	Size[u]=1;
	p[u]=0;
	int cnt=0,failson=0,sumg=0;
	for(int i=head[u];i;i=Next[i]){
		int v=to[i];
		if(v==fa[u]) continue;
		son[u].push_back(v);
		dfs1(v,u);
		Size[u]+=Size[v];
		if(p[v]==0) p[u]=1,cnt++,failson=v,V[u].push_back(v);
		(sumg+=g[v])%=mod;
	}
	Sumg[u]=sumg;
	if(cnt>=2) g[u]=Size[u];
	else if(cnt==1) g[u]=(Size[u]-g[failson]+mod)%mod;
	else g[u]=(Size[u]-sumg+mod)%mod;
}

int up[N],h[N];
void dfs2(int u){//在计算 up 和 h 时不能每一次都遍历兄弟,不然碰到菊花就假了 
	if(fa[u]){
		up[u]=0;
		int cnt=0,failson_g=0,sumg=0;
		cnt=V[fa[u]].size();
		for(int v:V[fa[u]])   //这里遍历兄弟至多只遍历2个 
			if(v!=u){
				failson_g=g[v];
				break;
			} 
		sumg=(Sumg[fa[u]]-g[u]+mod)%mod;
		if(p[u]==0) cnt--;
		
		if(fa[fa[u]]){
			if(up[fa[u]]==0) cnt++,failson_g=h[fa[u]];
			(sumg+=h[fa[u]])%=mod;
		}
		
		if(cnt) up[u]=1;
		if(cnt>=2) h[u]=n-Size[u];  //注意此时整棵树的大小要减去 Size[u] 
		else if(cnt==1) h[u]=(n-Size[u]-failson_g+mod+mod)%mod;
		else h[u]=(n-Size[u]-sumg+mod+mod)%mod;		
	}
	
	for(int i=head[u];i;i=Next[i]){
		int v=to[i];
		if(v==fa[u]) continue;
		dfs2(v);
	}
} 

int s0,s1,sum0,sum1;
void dfs3(int u){   //更新 p 和 g 
	p[u]=0;
	int cnt=0,failson_g=0,sumg=0;
	for(int v:son[u]){
		if(p[v]==0) p[u]=1,cnt++,failson_g=g[v];
		(sumg+=g[v])%=mod;
	}
	if(fa[u]){
		if(up[u]==0) p[u]=1,cnt++,failson_g=h[u];
		(sumg+=h[u])%=mod;
	}
	if(cnt>=2) g[u]=n;
	else if(cnt==1) g[u]=(n-failson_g+mod)%mod;
	else g[u]=(n-sumg+mod)%mod;
	
	if(p[u]==0) s0++;
	else s1++;
	(sum1+=g[u])%=mod,(sum0+=(n-g[u]+mod)%mod)%=mod;

	for(int i=head[u];i;i=Next[i]){
		int v=to[i];
		if(v==fa[u]) continue;
		dfs3(v);
	}
}

struct Matrix{
	int a[2][2];
	int n,m;
	void Init(){
		memset(a,0,sizeof a);
	}
}F,A;
Matrix operator * (const Matrix &A,const Matrix &B){
	Matrix C;
	C.Init();
	C.n=A.n,C.m=B.m;
	for(int k=0;k<=A.m;k++){
		for(int i=0;i<=C.n;i++){
			for(int j=0;j<=C.m;j++){
				(C.a[i][j]+=A.a[i][k]*B.a[k][j]%mod)%=mod;
			}
		}
	}
	return C;
}
Matrix Quick_power(Matrix A,int b){
	Matrix Ans;
	Ans.n=1,Ans.m=1;
	for(int i=0;i<=Ans.n;i++){    //单位矩阵 
		for(int j=0;j<=Ans.m;j++){
			if(i==j) Ans.a[i][j]=1;
			else Ans.a[i][j]=0;
		}
	}
	while(b){
		if(b&1) Ans=Ans*A;
		b>>=1,A=A*A;
	} 
	return Ans;
}
signed main(){
//	freopen("ball.in","r",stdin);
//	freopen("ball.out","w",stdout);
	n=read(),D=read();
	for(int i=1;i<n;i++){
		int u=read(),v=read();
		add(u,v),add(v,u);
	}
    
    dfs1(1,0);
    dfs2(1);
    dfs3(1);
    
    F.n=0,F.m=1;
    F.a[0][0]=s0,F.a[0][1]=s1;
    A.n=1,A.m=1;
    A.a[0][0]=sum0;
    A.a[0][1]=sum1;
    A.a[1][0]=s0*n%mod;
    A.a[1][1]=s1*n%mod;
    F=F*Quick_power(A,D-1);
    
	printf("%lld\n", (F.a[0][1]*n%mod*p[1]+F.a[0][0]*g[1]%mod)%mod );
	return 0;
}

62.[NOI Online #1 入门组] 跑步

首先题目等价于正整数拆分。
\(f_{i,j}\):用前 \(i\) 个数构造的的不下降序列,和为 \(j\) 的方案数。
那么 \(f_{i,j}=f_{i-1,j}+f_{i,j-i}\),这就是完全背包
答案为。
$f_{n,n} $。

考虑根号分治,令 \(B=\sqrt n\)

  1. 对于 \(i<B\) 的部分我们仍然用上面的 dp
  2. \(g_{i,j}\) 表示构造长度为 \(i\) 的不上升序列,每个数都 \(\ge B\),和为 \(j\) 的方案数。
    那么 \(g_{i,j}=g_{i-1,j-B}+g_{i,j-i}\)。意思是每一次我要么在序列末尾加一个 \(B\),要么整体 \(+1\)
    由于 \(j\le n\),所以 \(i\le \sqrt n\)

最后答案就枚举第一部分的和,\(ans=\sum_{j=0}^n (f_{B-1,j}\times \sum_{k=0}^{\frac{n}{B}}g_{k,n-j})\)

code

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,m,mod,f[N],g[320][N];
signed main(){
	n=read(),mod=read();
	m=sqrt(n);
	f[0]=1;
	for(int i=1;i<m;i++){
		for(int j=i;j<=n;j++){
			(f[j]+=f[j-i])%=mod;
		}
	}
	g[0][0]=1;
	for(int i=1;i<=n/m+1;i++){
		for(int j=m*i;j<=n;j++){
			g[i][j]=(long long)(g[i][j-i]+g[i-1][j-m])%mod;
		}
	}
	
	long long ans=0;
	for(int j=0;j<=n;j++){
		long long sum=0;
		for(int k=0;k<=n/m;k++) (sum+=(long long)g[k][n-j])%=mod;
		(ans+=(long long)(f[j]*sum)%mod)%=mod;
	}
	printf("%lld\n",ans);
	return 0;
}

63.CF1220E Tourism

这题流行随机化解法,又因为我以前没写过随机化算法,所以就作为第一道随机化题吧。

没有奇环,让我们想到黑白染色,所以我们随机给每个点随一个颜色。
每一次走的时候不能走相同颜色的边,求解时可以简单dp:
\(f_{i,j}\) 表示当前走了 \(i\) 条边,走到 \(j\) 的最短距离。
转移 \(O(k\times n^2)\) 显然。

根据时间限制,我们可以随机 \(4500\) 次。

这个做法错的可能只有一种,就是正确答案对应的路径上出现了颜色相同的相邻点,否则一定可以 dp 出正确的答案。

所以这 \(k\) 个点一共有 \(2^k\) 总染色情况,有且仅有 0101010...1010101... 是对的,即一次正确的概率是:\(\frac{1}{(2^{k-1})}=\frac{1}{512}\),错误的概率是:\(\frac{511}{512}\)
\(4500\) 次都不对的概率是 \((\frac{511}{512})^{4500} \approx 0.00015\),事实上可以随到 \(5000\) 次,那概率更低。

code

#include<bits/stdc++.h>
#define int long long 
using namespace std;
const int N=1e5+5;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,k,a[85][85];
int col[85],f[15][85],ans=LLONG_MAX;
void work(){
	for(int i=1;i<=n;i++) col[i]=rand()%2;
	for(int i=0;i<=k;i++){
		for(int j=1;j<=n;j++){
			f[i][j]=0x3f3f3f3f3f3f3f3f;
		}
	}
	f[0][1]=0;
	for(int i=1;i<=k;i++){
		for(int j=1;j<=n;j++){
			for(int u=1;u<=n;u++){
				if(col[u]!=col[j])
					f[i][j]=min(f[i][j],f[i-1][u]+a[u][j]);
			}
		}
	}
	ans=min(ans,f[k][1]);
}
signed main(){
	srand(time(0));
	n=read(),k=read();
	for(int i=1;i<=n;i++){
		for(int j=1;j<=n;j++){
			a[i][j]=read();
		}
	}
	int T=4500;
	while(T--) work();
	printf("%lld\n",ans);
	return 0;
}

64.采集

题目描述

有一棵 \(n\) 个点(\(n \le 10^4\))的树,每个点有一个 \([1,n]\) 的颜色,请找出点数最少的连通块,满足这个连通块有至少 \(k\) 种不同的颜色(\(k\le 5\))。

题解

因为 \(k\) 很小,考虑随机化乱搞。

首先给每个颜色随机映射到 \([1,k]\)
如果最终的那 \(k\) 种颜色刚好被映射成了不同的 \(k\)
那么就正确,正确的概率是 \(\frac{k!}{k^k}\) ,随机 \(50\) 次即可。

随机完之后,考虑树形 dp 计算答案,设:
\(f_{i,s}\) 表示 \(i\) 这棵子树内选出集合 \(s\) 的颜色的最小连通块大小(一定要选 \(i\))。
转移每一次加进来一个子树更新即可。

code

#include<bits/stdc++.h>
using namespace std;
const int N=1e4+5;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,k;
int a[N];
int tot,head[N],to[N<<1],Next[N<<1];
void add(int u,int v){
	to[++tot]=v,Next[tot]=head[u],head[u]=tot;
}

int ans=INT_MAX,col[N];
int f[N][40];
void dfs(int u,int fa){
	f[u][1<<col[ a[u] ]]=1;
	for(int i=head[u];i;i=Next[i]){
		int v=to[i];
		if(v==fa) continue;
		dfs(v,u);
		for(int j=(1<<k)-1;j>=0;j--){
			for(int s=0;s<(1<<k);s++){
				f[u][j|s]=min(f[u][j|s],f[u][j]+f[v][s]);
			}
		}
	}
	ans=min(ans,f[u][(1<<k)-1]);
}
void work(){
	memset(f,0x3f,sizeof f);
	dfs(1,0);
}
signed main(){
	freopen("insect.in","r",stdin);
	freopen("insect.out","w",stdout);
	n=read(),k=read();
	bool flag=true;
	for(int i=1;i<=n;i++){
		a[i]=read();
		if(a[i]>k) flag=false;
	} 
	for(int i=1;i<n;i++){
		int u=read(),v=read();
		add(u,v),add(v,u);
	}
	
	if(flag){
		for(int i=1;i<=k;i++) col[i]=i-1;
		work();
		printf("%d\n",ans);
		return 0;	
	}
	
	mt19937 mtrand(time(0));
	int T=50;
	while(T--){
		for(int i=1;i<=n;i++){
			col[i]=(unsigned int)mtrand()%k;
		}
		work();
	}
	printf("%d\n",ans);
	return 0;
}


65.[ABC214G] Three Permutations

来自题解区的妙妙非传统 DP 做法。

首先容斥是显然的,设 \(dp_i\) 表示有 \(i\) 个位置不符合条件的数量,那么:
\(ans = \sum_{i=0}^n (-1)^i \times dp[i] \times (n-i)!\)

考虑进行一个经典转化:
\(a_i\)\(b_i\) 连边,那相当于我们要选出 \(i\) 条边,并给每条边定向,并且不能有两条边指向同一个点。
如果这 \(i\) 条边给定了,那就是一个有 \(n\) 个点, \(i\) 条边,每个点度数至多为 \(2\) 的图。
那一定是若干环加上若干链:

  1. 对于一个环,只有 \(2\) 中定向方案;
  2. 对于一条链,最终一定是选出一个点,它左边的边全朝左,右边的边全朝右。所以一共有 \(len\) 种方案,其中 \(len\) 为链的大小。

但是我们不能暴力枚举这 \(i\) 条边,所以考虑看到原图,即:\(n\) 个点,\(n\) 条边,每个点度数为 \(2\) 的图。这肯定是若干个环(有自环)组成的图。
\(f_{i,j}\) 表示前 \(i\) 个环,选出 \(j\) 条边定向的总方案数。
设第 \(i\) 个环的大小为 \(sz\)
(目前为止都是主流做法,接下来就要开始神奇妙妙思路了)。

  1. 如果 \(sz=1\),即是自环,那么要么选,要么不选:\(f_{i,j}=f_{i-1,j}+f_{i-1,j-1}\)
  2. 如果 \(s\ne 1\),即正常的环,枚举这个环选了 \(k\) 条边,则:\(f_{i,j}=\sum_{k=0}^j f_{i-1,j-k}\times g_{sz,k}\)。其中 \(g_{sz,k}\) 表示在大小为 \(sz\) 的环中,选 \(k\) 条边定向的方案数。

这个做法妙的地方就是求 \(g\) 时不用 \(dp\) 用组合意义:可以把一条 \((u,v)\) 的边拆成 \((u,w)\)\((w,v)\) ,定向朝 \(u\) 就是选 \((u,w)\) 这条边,否则选 \((w,v)\) 这条边。
那么问题变成,给你 \(2\times sz\) 个点的环,选出 \(k\) 条不相邻的边的方案。
随便考虑其中一条边 \((a,b)\)

  1. 如果不选他,问题就变成在 \(2\times sz\) 个点,\(2\times sz - 1\) 条边的链中选出 \(k\) 条不相邻边的方案。可以认为是先给你 \(2\times sz-k\) 个点,从中选出 \(k\) 个点作为右端点,再在这些点左边加上一个左端点。这样刚好一共 \(2\times sz\) 个点,且没有选出的边相邻,并且这样的结果也是可以唯一对应到原链的即方案为 \(C_{2\times sz-k}^k\)
  2. 如果选他,那它旁边的边不能选了,问题就变成在 \(2\times sz-2\) 个点,\(2\times sz-3\) 条边的链中选出 \(k-1\) 条不相邻边的方案,方案为 \(C_{2\times sz-2-(k-1)}^{k-1}\)

所以对于不是自环的 \(f\) 的转移为:\(f_{i,j} = f_{i-1,j} + \sum_{k=1}^j f_{i-1,j-k} \times ( C_{2\times sz-k}^k + C_{2\times sz-2-(k-1)}^{k-1})\)

这个复杂度看似是 \(O(n^3)\) 但是其实是:
\(O(\sum_{i=1}^{cnt} s_i\times sz_i)\)
其中 \(s\)\(sz\) 的前缀和,\(cnt\) 是环的总数。
而:

\[\begin{aligned} \sum_{i=1}^{cnt} s_i\times sz_i &= \sum_{i=1}^{cnt}(sz_1+sz_2+...+sz_i)\times sz_i \\ &= sz_1^2+sz_2^2+...+sz_{cnt}^2 + \sum_{1\le i<j\le cnt} sz_i\times sz_j \\ &< (sz_1+sz_2+...+sz_{cnt})^2 (展开即可) \\ &= n^2 \end{aligned} \]

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

code

#include<bits/stdc++.h>
#define int long long 
using namespace std;
const int N=3e3+5,mod=1e9+7;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,a[N],b[N];
int tot,head[N],to[N<<1],Next[N<<1];
void add(int u,int v){
	to[++tot]=v,Next[tot]=head[u],head[u]=tot;
} 

int fact[N<<1],inv[N<<1],q[N<<1];
void Init(){
	fact[0]=1;
	for(int i=1;i<N*2;i++) fact[i]=fact[i-1]*i%mod;
	inv[1]=1;
	for(int i=2;i<N*2;i++) inv[i]=(mod-mod/i)*inv[mod%i]%mod;
	q[0]=1;
	for(int i=1;i<N*2;i++) q[i]=q[i-1]*inv[i]%mod;
}
int C(int n,int m){
	return fact[n]*q[m]%mod*q[n-m]%mod;
}

int cnt,sz[N],s[N];
bool vis[N];
void dfs(int u,int len){
	sz[cnt]=max(sz[cnt],len);
	vis[u]=true;
	for(int i=head[u];i;i=Next[i]){
		int v=to[i];
		if(vis[v]) continue;
		dfs(v,len+1);
	}
}

int f[N][N];

signed main(){
//	freopen("game.in","r",stdin);
//	freopen("game.out","w",stdout);
	n=read();
	for(int i=1;i<=n;i++) a[i]=read();
	for(int i=1;i<=n;i++) b[i]=read(),add(a[i],b[i]),add(b[i],a[i]);
	
	Init();
	for(int i=1;i<=n;i++){
		if(!vis[i]){
			++cnt;
			dfs(i,1);
			s[cnt]=s[cnt-1]+sz[cnt];
		}
	}
	
	for(int i=0;i<=cnt;i++) f[i][0]=1;
	for(int i=1;i<=cnt;i++){
		for(int j=1;j<=s[i];j++){
			f[i][j]=f[i-1][j];  //下面就不要算不选的情况了 
			if(sz[i]==1) (f[i][j]+=f[i-1][j-1])%=mod;
			else{
				for(int k=1;k<=min(j,sz[i]);k++){
					(f[i][j]+=f[i-1][j-k] * ( C(2*sz[i]-k,k) + C(2*sz[i]-1-k,k-1) )%mod)%=mod;
				} 
			}
		}
	} 
	
	int ans=0;
	for(int i=0;i<=n;i++){
		if(i&1) (ans=ans-f[cnt][i]*fact[n-i]%mod+mod)%=mod;
		else (ans+=f[cnt][i]*fact[n-i]%mod)%=mod;
	}
	printf("%lld\n",ans);
	return 0;
}

66.[NOI Online #2 提高组] 游戏

二项式反演板子。

因为恰好 \(k\) 个不是很好算,所以考虑计算钦定 \(k\) 个的情况。

考虑树形 DP,设 \(f_{i,j}\) 表示 \(i\) 子树内,选出 \(j\) 对点,他们是非平局情况的方案数。
转移就是经典的树形背包,先转移不考虑根节点的情况,每一次合并进来一个子树:\(f_{u,x}\times f_{son,y} \to f_{u,x+y}\)
再考虑选根节点的情况,\(f_{u,x}\times \max(cnt_{1-a_u}-x,0) \to f_{u,x+1}\) 其中 \(cnt_{0/1}\) 表示 \(u\) 子树内 \(0/1\) 类型的点的个数。

所以钦定 \(k\) 对点的方案数为 \(f_{1,k}\times (m-k)!\),如果设他是 \(g_k\),那么有:
\(g_k = \sum_{i=k}^m C_i^k \times ans_i\)
二项式反演可得:
\(ans_k = \sum_{i=k}^m (-1)^{i-k} C_i^k \times g_i\)

code

#include<bits/stdc++.h>
#define int long long 
using namespace std;
const int N=5e3+5,mod=998244353;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,m,a[N];
vector<int> G[N]; 
int f[N][N],g[N],tmp[N],Size[N],cnt[N][2];
void dfs(int u,int fa){
	cnt[u][a[u]]++;
	f[u][0]=1;
	for(int v:G[u]){
		if(v==fa) continue;
		dfs(v,u);
		cnt[u][0]+=cnt[v][0];
		cnt[u][1]+=cnt[v][1];
		for(int i=0;i<=Size[u]+Size[v];i++) tmp[i]=0;
		for(int x=0;x<=Size[u];x++){
			for(int y=0;y<=Size[v];y++){
				(tmp[x+y]+=f[u][x]*f[v][y]%mod)%=mod;
			}
		}
		for(int i=0;i<=Size[u]+Size[v];i++) f[u][i]=tmp[i];
		Size[u]+=Size[v];
	}
	for(int i=Size[u];i>=0;i--) (f[u][i+1]+=f[u][i]*max(0ll,cnt[u][1-a[u]]-i)%mod)%=mod;
	Size[u]++;
}

int fact[N],inv[N],q[N];
int C(int n,int m){
	return fact[n]*q[m]%mod*q[n-m]%mod;
}
signed main(){
//	freopen(".in","r",stdin);
//	freopen(".out","w",stdout);
	n=read(); m=n/2;
	string s; cin>>s;
	for(int i=1;i<=n;i++) a[i]=s[i-1]-'0';
	for(int i=1;i<n;i++){
		int u=read(),v=read();
		G[u].push_back(v);
		G[v].push_back(u);
	}
	dfs(1,0);
	
	fact[0]=1;
	for(int i=1;i<N;i++) fact[i]=fact[i-1]*i%mod;
	inv[1]=1;
	for(int i=2;i<N;i++) inv[i]=(mod-mod/i)*inv[mod%i]%mod;
	q[0]=1;
	for(int i=1;i<N;i++) q[i]=q[i-1]*inv[i]%mod;
	
	for(int i=0;i<=m;i++) g[i]=f[1][i]*fact[m-i]%mod;	
	for(int i=0;i<=m;i++){
		int ans=0;
		for(int j=i;j<=m;j++){
			ans=(ans+( ((j-i)%2==0)?1:-1 )*C(j,i)%mod*g[j]%mod+mod)%mod;
		}
		printf("%lld\n",ans);
	}
	return 0;
}

67.CF1895F Fancy Arrays

题目的存在这个要求很不友好,考虑计算不合法情况,他的否命题是:\(\forall i,ai<x 或 ai\ge x+k\)
因为有任意相邻两数的差不超过 \(k\),所以上面这个条件等价于:每一个 \(a_i\)\(<x\) 或者 每一个 \(a_i\)\(\ge x+k\)

还是不好算?
再一次看他的否命题,就得到了原命题的等价命题:$ \max(a_i)\ge x,\min(a_i)\le x+k-1$。当然还要满足 \(\forall i \in [2,n],|a_i-a_{i-1}|\le k\),下面就统一省略后面这个条件了。
因为 \(\max(a_i)<x\)\(\min(a_i)\le x+k-1\) 的充分条件,所以可以转化为求:
\(\min(a_i)\le x+k-1 的数列数 - \max(a_i) < x 的数列数\)

先看前者:
如果知道了一个序列的差分数组就可以知道序列的每一个数与 \(a_1\) 的差。
此时就可以知道哪个数是最小值,也就是说差分数组和最小值可以唯一确定一个序列。
差分数组的方案数是 \((2\times k+1)^{n-1}\),最小值的方案数是 \(x+k\)\(0\) 也算),所以总方案数就是 \((x+k) \times (2*k+1)^{n-1}\)

再看后者,因为 \(x\) 很小,考虑 DP:
\(f_{i,j}\) 表示前 \(i\) 个数,第 \(i\) 个数是 \(j\) 的方案数,那么:

\[f_{i,j}=\sum_{\max(0,j-k)\le y\le min(j+k,x-1)} f_{i-1,y} \]

矩阵快速幂优化 DP 即可。

code

#include<bits/stdc++.h>
#define int long long 
using namespace std;
const int N=1e5+5,mod=1e9+7;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int T;
int n,x,k;
int quick_power(int a,int b){
	int ans=1;
	while(b){
		if(b&1) (ans*=a)%=mod;
		(a*=a)%=mod,b>>=1;
	}
	return ans;
}
struct Matrix{
	int a[45][45];
	int n,m;
	void Init(){
		memset(a,0,sizeof a);
	}
}A,F;
Matrix operator * (const Matrix &A,const Matrix &B){
	Matrix C;
	C.Init();
	C.n=A.n,C.m=B.m;
	for(int k=0;k<A.m;k++){
		for(int i=0;i<C.n;i++){
			for(int j=0;j<C.m;j++){
				(C.a[i][j]+=A.a[i][k]*B.a[k][j]%mod)%=mod;
			}
		}
	}
	return C;
}
Matrix Quick_power(Matrix A,int b){
	Matrix Ans;
	Ans.n=x,Ans.m=x;
	for(int i=0;i<Ans.n;i++){    //单位矩阵 
		for(int j=0;j<Ans.m;j++){
			if(i==j) Ans.a[i][j]=1;
			else Ans.a[i][j]=0;
		}
	}
	while(b){
		if(b&1) Ans=Ans*A;
		b>>=1,A=A*A;
	} 
	return Ans;
}
signed main(){
//	freopen(".in","r",stdin);
//	freopen(".out","w",stdout);
	T=read();
	while(T--){
		n=read(),x=read(),k=read();
		int ans=(x+k)*quick_power(2*k+1,n-1)%mod;
		
		A.Init(),F.Init();
		A.n=x,A.m=x;
		for(int i=0;i<x;i++){
			for(int j=0;j<x;j++){
				if(abs(j-i)<=k) A.a[i][j]=1;
			}
		}
		F.n=1,F.m=x;
		for(int i=0;i<x;i++) F.a[0][i]=1;
		A=Quick_power(A,n-1);
		F=F*A;
		for(int i=0;i<x;i++)
			ans=(ans-F.a[0][i]+mod)%mod;
		printf("%lld\n",ans);
	}
	return 0;
}

68.[ARC162E] Strange Constraints

关于这种填数的题有一个经典套路是按照值域从小到大或从大到小填数。
但是这里不好做,因为虽然第二个限制条件很好满足,但是第三个限制条件由于我们不知道目前有哪些位置是可以填的所以不好转移。

所以设计的状态需要满足对于第二个限制条件,前面的状态要能限制到后面的状态并且可以比较容易得出哪些位置可以填。
所以我们按照每个数在 \(b\) 中出现的次数从大到小枚举。
\(f_{i,j,k}\) 表示当前枚举到的次数为 \(i\),填完之后有 \(j\) 个数已经被填过了,有 \(k\) 个位置已经被填过了。
转移时枚举有 \(x\) 个数被填了 \(i\) 次,所以是从 \(f_{i+1,j-x,k-i\times x}\) 转移过来,转移系数有下面这几个,我们分别做解释,设 \(cnt_{y}\) 表示 \(A\) 中大于等于 \(y\) 的数的个数:

  1. \(C_{cnt_i-(j-x)}^x\)
    这是在从满足条件二的数中选 \(x\) 个数出来,因为 \(i\) 是从大到小枚举所以 \(cnt_i\) 也包含了$ cnt_{i+1},cnt_{i+2},...$ 那些位置,即前面的 \(j-x\) 个数也是从这 \(cnt_i\) 个数里选出来的,不能再选了。
  2. \(C_{cnt_i-(k-i\times x)}^{i\times x}\)
    这是在给这 \(x\) 个数选位置,同理前面的 \(k-i\times x\) 个位置也是从 \(cnt[i]\) 个位置里选出来的,不能再选了。这也是这么设计状态的原因,这就满足了条件三。
  3. \(\frac{(i\times x)!} {(i!)^x}\)
    这是可重集排列数。

这个 DP 貌似是 \(O(n^4)\) 但是容易发现 \(j\)\(x\) 的枚举范围分别是 \(\frac{n}{i}\) (因为 \(i\) 从大到小枚举) 和 \(\frac{k}{i}\)
所以时间复杂度为:

\[\begin{aligned} O(\sum_{i:n \to 1}(\frac{n}{i} \times \sum_{1\le k \le n}\frac{k}{i})) &= O(n \times (∑\frac{1}{i} \times (\frac{1}{i} \times ∑k))) \\ &= O(\frac{n^2 (n+1)}{2} \times ∑\frac{1}{i^2} ) \end{aligned} \]

\(∑\frac{1}{i^2}\) 可以省略,即时间复杂度是:\(O(n^3)\)

code

#include<bits/stdc++.h>
#define int long long 
using namespace std;
const int N=500+5,mod=998244353;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,a[505],cnt[505],f[505][505][505];
int fact[N],inv[N],pre[N];
void Init(){
	fact[0]=1;
	for(int i=1;i<N;i++) fact[i]=fact[i-1]*i%mod;
	inv[1]=1;
	for(int i=2;i<N;i++) inv[i]=(mod-mod/i)*inv[mod%i]%mod;
	pre[0]=1;
	for(int i=1;i<N;i++) pre[i]=inv[i]*pre[i-1]%mod;
}
int C(int n,int m){
	if(m>n) return 0;
	return fact[n]*pre[m]%mod*pre[n-m]%mod;
}
int quick_power(int a,int b){
	int ans=1;
	while(b){
		if(b&1) (ans*=a)%=mod;
		(a*=a)%=mod,b>>=1; 
	}
	return ans;
}
signed main(){
	n=read();
	for(int i=1;i<=n;i++) a[i]=read(),cnt[a[i]]++;
	for(int i=n;i>=1;i--) cnt[i]+=cnt[i+1];
	
	Init();
	
	f[n+1][0][0]=1;
	for(int i=n;i>=1;i--){
		for(int j=0;j<=n/i;j++){
			for(int k=0;k<=n;k++){
				for(int x=0;x<=min(j,k/i);x++){
					(f[i][j][k] += f[i+1][j-x][k-i*x]
								   * C(cnt[i]-(j-x) , x) % mod
								   * C(cnt[i]-(k-i*x) , i*x) % mod
								   * fact[i*x]%mod * quick_power(pre[i],x)%mod) %= mod;
				}
			}
		}
	}
	
	int ans=0;
	for(int i=0;i<=n;i++) (ans+=f[1][i][n])%=mod;
	printf("%lld\n",ans);
	return 0;
}

69.[ARC163D] Sum of SCC

这题主要是要知道一个结论来把 SCC 这个一看就不好求得东西转换一下。

结论:竞赛图的 SCC 的个数等于把这张图的点集 \(V\) 划分成两个集合(可以为空) \(A\)\(B\)\(A\)
\(B\) 之间的边方向都为 \(A\to B\) 的划分方案数 \(-1\)

证明:
考虑缩点,缩点之后的竞赛图也是一个形似链的竞赛图,他的拓扑序是唯一的。
如果拓扑序为 \(p_1,p_2,...,p_k\)\(k\) 为 SCC 的个数),那么对于任意的一个 \(i(0\le i\le k)\) 我们以 \(i\) 为断点,\(p_1,p_2,...,p_i\) 放进 \(A\)\(p_{i+1},...,p_k\) 放进 \(B\) 就是一个合法的划分方案。
又因为不能把一个 SCC 中的点分到两个集合中(如果可以的话这必然不是个 SCC,因为划分到 \(B\) 中的点没有到 \(A\) 中的点的路径),\(B\) 中的 SCC 的拓扑序也不能比 \(A\) 中的 SCC 的拓扑序小,所以这种划分方案也是唯一的。
一共有 \(k+1\) 种划分方案。

接下来就是简单 dp 了,设 \(f_{i,j,k}\) 表示已经放了前 \(i+j\) 个点,其中 \(|A|=i\)\(|B|=j\),并且一共有 \(k\) 条边满足 \(u<v\) 的方案数。
转移时考虑新加进来编号为 \(i+j+1\) 的点放到哪个集合:

  1. 放到 \(A\) 集合,那么因为他是目前编号最大的点所以他连向 \(B\) 中的那 \(j\) 条边均不符合条件,而他连向 \(A\) 中的那 \(i\) 条边是随便钦定的,枚举其中有 \(x\) 条边满足条件,转移为:\(f_{i,j,k}\times C_i^x \to f_{i+1,j,k+x}\)
  2. 放到 \(B\) 集合,所以 \(A\) 中连向他的那 \(i\) 条边均符合条件,而他连向 \(B\) 中的那 \(j\) 条边是随便钦定的,枚举其中有 \(x\) 条边满足条件,转移为:\(f_{i,j,k}\times C_j^x \to f_{i,j+1,k+i+x}\)

统计答案时,任意的一个方案都会给答案带来 \(1\) 的贡献,不用去管哪些方案是一张图的。
记得还要减去若干个 \(1\)
时间复杂度 \(O(n^3 \times m)\)

code

#include<bits/stdc++.h>
#define int long long 
using namespace std;
const int N=1e5+5,mod=998244353;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,m,f[35][35][905],ans;
int fact[N],inv[N],pre[N];
void Init(){
	fact[0]=1;
	for(int i=1;i<N;i++) fact[i]=fact[i-1]*i%mod;
	inv[1]=1;
	for(int i=2;i<N;i++) inv[i]=(mod-mod/i)*inv[mod%i]%mod;
	pre[0]=1;
	for(int i=1;i<N;i++) pre[i]=inv[i]*pre[i-1]%mod;
}
int C(int n,int m){
	return fact[n]*pre[m]%mod*pre[n-m]%mod;
}
int quick_power(int a,int b){
	int ans=1;
	while(b){
		if(b&1) (ans*=a)%=mod;
		(a*=a)%=mod,b>>=1; 
	}
	return ans;
}
signed main(){
	Init();
	n=read(),m=read();
	f[0][0][0]=1;
	for(int i=0;i<=n;i++){
		for(int j=0;j+i<=n;j++){
			for(int k=0;k<=m;k++){
				for(int x=0;x<=i;x++)
					(f[i+1][j][k+x]+=f[i][j][k]*C(i,x)%mod)%=mod;
				for(int x=0;x<=j;x++)
					(f[i][j+1][k+i+x]+=f[i][j][k]*C(j,x)%mod)%=mod; 
			}
		}
	}
	for(int i=0;i<=n;i++){
		(ans+=f[i][n-i][m])%=mod;
	}
	(ans=ans-C(n*(n-1ll)/2ll,m)+mod)%=mod;
	printf("%lld\n",ans);
	return 0;
}

70.CF1728G Illumination

考虑容斥,枚举集合 \(S\) 表示 \(S\) 中的这些点不被照亮,其他点随意,则 \(ans=\sum(-1)^{|S|} \times f_S\)
其中 \(f_S\) 表示对应的方案数。

首先每个不被照亮的点会对他周围的灯的照亮范围产生限制,并且两个钦定点之间的路灯仅仅只受到这两个点的限制。
这启发我们预处理 \(g_{l,r}\) 表示使得点 \(p_l\)\(p_r\) 之间的路灯照不到 \(p_l\)\(p_r\) 的方案数。
\(O(nm^2)\) 预处理即可。

那么 \(f_S= \prod g_{S_i,S_i+1}\)
注意到直接这么算是不对的,因为会漏了两侧的点,解决方法是加上 \(-\infty\)\(+\infty\) 两个点,并且钦定 \(S\) 中一定要包含这两个点(容易发现此时不影响 \(|S|\) 的奇偶性)。

由此我们可以 \(O(m2^m)\) 处理单个问题。

这里开始会出现两种做法,一种是算出每个 \(g_{l,r}\) 的容斥系数,动态加入一盏灯时算出他所影响的 \(g\) 的贡献。
但是比较麻烦且复杂度不优,这里讲第二种。

经典套路之——容斥转 DP。
\(dp_i\) 表示只考虑前 \(i\) 个点的答案,并且钦定第 \(i\) 个点一定在容斥的集合 \(S\) 里。
因为多加进来了一个点,所以原先容斥系数正的会变成负的,负的会变成正的,即转移要带上容斥系数 \(-1\)
\(dp_i=\sum_{\substack{j<i}} (-1)\times dp_j\times g_{j,i}\)
初始状态时 \(dp[0]=-1\)
由这个转移式子可以看出 \(0\) 号点(无穷小) 是一定会被选的。
答案就是 \(dp_{m+1}\) 因为 \(m+1\) 号点也一定要被选。

单次 dp 复杂度 \(O(m^2)\),每一次都做一遍就是 \(O(Tn^2)\)

code

#include<bits/stdc++.h>
#define int long long 
using namespace std;
const int N=2e5+5,mod=998244353,inf=INT_MAX;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int d,n,m,a[N],p[N],T;
int g[20][20],tmp[20][20],f[20];
signed main(){
	d=read(),n=read(),m=read();
	for(int i=1;i<=n;i++) a[i]=read();
	for(int i=1;i<=m;i++) p[i]=read();
	sort(p+1,p+m+1);
	p[0]=-inf,p[m+1]=inf;
	
	for(int l=0;l<=m+1;l++)
		for(int r=l+1;r<=m+1;r++)
			g[l][r]=1;
	for(int i=1;i<=n;i++)
		for(int l=0;l<=m+1;l++)
			for(int r=l+1;r<=m+1;r++)
				if(p[l]<=a[i]&&a[i]<=p[r])
					(g[l][r]*=min({d+1ll,a[i]-p[l],p[r]-a[i]}))%=mod; 
					
	for(int l=0;l<=m+1;l++)
		for(int r=l+1;r<=m+1;r++){
			tmp[l][r]=g[l][r];
		}
	
	T=read();
	while(T--){
		int x=read();
		for(int l=0;l<=m+1;l++)
			for(int r=l+1;r<=m+1;r++)
				if(p[l]<=x&&x<=p[r])
					(g[l][r]*=min({d+1,x-p[l],p[r]-x}))%=mod; 
		
		for(int i=0;i<=m+1;i++) f[i]=0;
		f[0]=-1;
		for(int i=1;i<=m+1;i++){
			for(int j=0;j<i;j++)
				(f[i]=f[i]-f[j]*g[j][i]%mod+mod)%=mod;
		}
		printf("%lld\n",f[m+1]);
		
		for(int l=0;l<=m+1;l++)
			for(int r=l+1;r<=m+1;r++)
				g[l][r]=tmp[l][r];		
	}
	return 0;
}

71.[ARC162D] Smallest Vertices

注意 \(d_i\) 的定义是儿子的个数。

前置知识:Prufer序列,具体看 OI.wiki。
涉及结论:
对于给定每个点度数的所有本质不同的 \(n\) 个点的有标号无根树一共有( \(deg_i\)表示 \(i\) 的度数):
\(\frac{(n-2)!}{\prod(deg_i-1)!}\)

证明:
根据 Prufer 序列的性质,每个点在序列中出现的次数是 \(deg_i-1\),所以我们可以根据度数还原出 Prufer 序列里面的元素种类,总共有 \((n-2)!\) 种顺序,去掉重复的即为上述结果。

对于每一个点考虑计算他为好点的有根树的数量,即拆分贡献。
对于一个点 \(u\) 如果他是好点意味着它子树内的点在 \([u,n]\) 范围内。
枚举它的子树大小 \(sz\),且根据 Prufer 序列他们的 \(\sum (deg_i-1)=sz-2\)
注意到此时我们认为 \(u\) 是根,所以他的 \(deg_u=d_u\),其余点的 \(deg_v=d_v+1\)
所以上面式子的要求即为 \(\sum_{v∈subtree(u)} d_v = sz-1\)

容易想到背包,设 \(f_{i,j,k}\) 表示从 \([i,n]\) 中选出 \(j\) 个点,他们的 \(d\) 的和为 \(k\) 的方案数。
转移略。

那么对于 \(u\) 子树内的贡献就是:

\[ \frac{ (sz-2)! } { (d_u-1)! \times \prod _{v\ne u}d_v! } = \frac{ d_u \times (sz-2)! } { \prod d_v! } \]

对于 \(u\) 子树外的贡献,此时我们把 \(u\) 当做一个叶子结点,并且他子树内的那 \(sz-1\) 个点删掉再计算,
注意 \(1\) 的度数也是 \(d_1\),而不是 \(d_1+1\),贡献为:\(\frac{ (n-sz+1-2)! \times d_1}{ \prod d_v! }\) ,其中 \(v\) 不属于 \(u\) 的子树(\(u\) 此时为叶子的话他的度数 \(-1=0\),可以不考虑进去)。
把上面这两个东西乘起来会发现分母刚好等于 \(\prod_{i=1}^n d_i!\) ,可以直接预处理。

于是最后的贡献就是:

\[f_{u+1,sz-1,sz-1-d_u} \times \frac{d_u \times d_1 \times (sz-2)! \times (n-sz+1-2)!}{\prod_{i=1}^n d_i!} \]

这里写 $ f_{u+1,sz-1,sz-1-d_u}$ 而不是 \(f_{u,sz,sz-1}\) 是因为不能出现不选 \(u\) 的情况。

枚举 \(u\)\(sz\) 计算答案即可。

注意这里如果 \(u\) 是根节点或者是叶子结点要特判一下 (因为 \(sz\ge 2\),且 \(u\) 子树外不能为空) ,此时贡献就是所有符合题意的无根树的数量。

总时间复杂度 \(O(n^3)-O(n^2)\)(这个的意思是预处理 \(O(n^3)\),求答案 \(O(n^2)\))。

code

#include<bits/stdc++.h>
#define int long long 
using namespace std;
const int N=1e5+5,mod=998244353;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,d[N],f[505][505][505],ans;
int fact[N],inv[N],pre[N];
void Init(){
	fact[0]=1;
	for(int i=1;i<N;i++) fact[i]=fact[i-1]*i%mod;
	inv[1]=1;
	for(int i=2;i<N;i++) inv[i]=(mod-mod/i)*inv[mod%i]%mod;
	pre[0]=1;
	for(int i=1;i<N;i++) pre[i]=inv[i]*pre[i-1]%mod;
}
signed main(){
	Init(); 
	
	n=read();
	for(int i=1;i<=n;i++) d[i]=read();
	
	f[n+1][0][0]=1;
	for(int i=n;i>=1;i--){
		for(int j=0;j<=n;j++){
			for(int k=0;k<=n-1;k++){
				f[i][j][k]=f[i+1][j][k];
				if(j>0&&k>=d[i]) f[i][j][k]=(f[i][j][k]+f[i+1][j-1][k-d[i]])%mod;
			}
		}
	} 
	
	int tmp=1;
	for(int i=1;i<=n;i++) (tmp*=pre[d[i]])%=mod; //预处理分母
	 
	for(int u=1;u<=n;u++){
		if(u==1||d[u]==0)
			(ans += d[1] * fact[n-2] % mod * tmp % mod) %= mod;
		else
			for(int sz=2;sz<n;sz++){
				(ans += d[1] * fact[n-sz-1] % mod * d[u] % mod * fact[sz-2] % mod * f[u+1][sz-1][sz-1-d[u]] % mod * tmp % mod) %= mod;
			}
	}
	
	printf("%lld\n",ans);
	return 0;
}


72.[ABC306Ex] Balance Scale

根据题目意思可以连边,如果没有 = 相当于给每条边定向。
因为不能出现环,所以相当于一个有标号 DAG 计数。

经典思路设 \(f_S\) 表示 \(S\) 中的点的导出子图一共有多少种可能的 DAG。
因为一个 DAG 必然有一些入度为 \(0\) 的点,考虑容斥这些点,去掉这些点仍然是个 DAG 那么:
\(\sum_{T \in S} (-1)^{|T|} \times f_{S-T}\)\(S-T\) 表示的是补集
这个东西算出来的结果表示的意义是 \(S\) 中的点形成的 DAG 中有 \(0\) 个入度为 \(0\) 的点的方案数。
所以神奇的地方来了,因为这种 DAG 不存在,所以他等于 \(0\)
那么移项即可得到:
\(f_S=\sum_{T \in S,T\ne \emptyset}(-1)^{|T|+1} \times f_{S-T}\)
注意 \(T\) 中的点之间不能有连边。

加上 = 的情况也很简单,其实相当于可以合并这条边的两个端点所以 \(T\) 中的点就可以出现有连边的情况,容斥系数从 \((-1)^{|T|+1}\) 变成 \((-1)^{cnt_T + 1}\) 即可,\(cnt_T\)\(T\) 中的点的导出子图的连通块数量。
时间复杂度 \(O(3^n)\)

code

#include<bits/stdc++.h>
#define int long long 
using namespace std;
const int N=1e5+5,mod=998244353;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,m;
bool G[20][20];
int fa[20];
int get(int x){
	return (x==fa[x])?(x):(fa[x]=get(fa[x]));
}
void merge(int x,int y){
	fa[get(x)]=get(y);
}
int cnt[(1<<17)+5];  //预处理连通块个数 
int f[(1<<17)+5];
signed main(){
	n=read(),m=read();
	for(int i=1;i<=m;i++){
		int a=read(),b=read();
		G[a][b]=G[b][a]=true;
	}
	
	for(int s=0;s<(1<<n);s++){
		for(int i=1;i<=n;i++) fa[i]=i;
		for(int i=1;i<=n;i++){
			for(int j=1;j<=n;j++){
				if(G[i][j]&&(s>>(i-1)&1)&&(s>>(j-1)&1))
					if(get(i)!=get(j)) merge(i,j);
			}
		}
		for(int i=1;i<=n;i++)
			if((s>>(i-1)&1)&&get(i)==i) cnt[s]++;
	}
	
	f[0]=1;
	for(int s=1;s<(1<<n);s++){
		for(int t=s;t;t=(t-1)&s){
			if((cnt[t]+1)&1) f[s]=(f[s]+mod-f[s^t])%mod;
			else f[s]=(f[s]+f[s^t])%mod;
		}	
	}
	
	printf("%lld\n",f[(1<<n)-1]);
	return 0;
}

73. [TJOI2018] 游园会

这应该算是 dp 套 dp 板子。

下面称输入给出的奖章串为 \(s\),要求的兑奖串为 \(t\)

会发现题目中的 \(t\) 有三个限制:

  1. 长度为 \(N\),且只有 N,O,I 三个字符。
  2. \(s\) 的 LIS 为 \(len\)
  3. 不能出现子串 NOI

会发现限制 \(1\) 可以直接在 dp 转移的时候满足,限制 \(3\) 直接多加一维状态 \(0/1/2\) 表示现在的后缀和 NOI 的匹配位数即可。
即我们的 dp 状态应该是 \(dp_{i,S,0/1/2}\) 表示填到 \(t\) 的第 \(i\) 位,当前状态是 \(S\),后缀和 NOI 的匹配位数是 \(0/1/2\) 的方案数。
但是限制 \(2\) 极其不好记录状态,因为你需要知道当前匹配到 \(s\) 的第几位,而直接记录匹配到 \(s\) 的第几位的话又容易算重。

考虑我们是怎么判定一个给定的 \(t\)\(s\) 的 LIS 是否为 \(i\) 的。
显然就是先求出他们的 LIS 再判断。
这是一个经典 dp,设 \(f_{i,j}\) 表示 \(t[1,i]\)\(s[1,j]\) 匹配的 LIS,转移:
\(f_{i,j}=\max(f_{i-1,j},f_{i,j-1},f_{i-1,j-1}+(t_i==s_j))\)

发现我们要计算出 \(f\)\(i\) 行的所有值,需要的只是 \(t_i\)\(f\)\(i-1\) 行的所有值。
所以最外层 dp 的状态的 \(S\) 我们直接让他表示内层 dp 的第 \(i\) 行的所有值。
外层 dp 的转移就枚举下一个位置 \(t_{i+1}\) 是什么,并对第二维 \(S\) 再做一遍 LIS 的那个 dp,得到新的状态 \(S'\)(即内层 dp \(f\) 数组的第 \(i+1\) 行)。
这样就转移成功了。

但是很明显 \(S\) 的数量太多了,时间空间双爆炸。
但显然并不是所有的 \(S\) 都是合法的。
因为 \(0\le f_{i,j}-f_{i,j-1}\le 1\),所以 \(S\) 表示的序列的差分数组每一位都一定是 \(0/1\)
因此状态 \(S\) 可以改为记录内层 dp \(f\) 数组的第 \(i\) 行的差分数组。
进一步地,可以直接状压成一个二进制数。
这样 \(S\) 的总数就是 \(2^K\) 了。

状态数是 \(O(N2^K)\),转移在 \(O(1)\) 枚举完 \(t_{i+1}\) 之后需要对内层 dp 进行 \(O(K)\) 的转移。
复杂度 \(O(NK2^K)\)

转移好函数的封装并不难写,实现参考了第一篇题解。
注意滚动数组并稍微剪枝。

因为在进行外层 dp 转移的时候还要对内层 dp 进行一次转移,所以叫 dp 套 dp

code

#include<bits/stdc++.h>
using namespace std;
const int N=1e3+5,mod=1e9+7;

inline int read(){
	int w=1,s=0;
	char c=getchar();
	for(;c<'0'||c>'9';w*=(c=='-')?-1:1,c=getchar());
	for(;c>='0'&&c<='9';s=s*10+c-'0',c=getchar());
	return w*s;
}
int n,k,dp[2][(1<<15)+5][3],f1[20],f2[20],ans[20];
char s[20];
int Hash(int f[20]){   //把一个数组的差分数组状压
	int S=0;
	for(int i=0;i<k;i++) S+=(f[i+1]-f[i])*(1<<i);
	return S;
}
void decrypt(int f[20],int S){  //解压缩
	for(int i=0;i<k;i++) f[i+1]=S>>i&1;
	for(int i=1;i<=k;i++) f[i]+=f[i-1];
}
void DP(int S,int newi,int newj,char c,int val){
	decrypt(f1,S);
	for(int i=1;i<=k;i++) f2[i]=max({f2[i-1],f1[i],f1[i-1]+(c==s[i])});
	int S2=Hash(f2);
	(dp[newi&1][S2][newj]+=val)%=mod;
}
signed main(){
	n=read(),k=read();
	scanf("%s",s+1);
	
	dp[0][0][0]=1;
	for(int i=0;i<n;i++){
		for(int S=0;S<(1<<k);S++) dp[i&1^1][S][0]=dp[i&1^1][S][1]=dp[i&1^1][S][2]=0;
		for(int S=0;S<(1<<k);S++){
			if(dp[i&1][S][0]){   //这个剪枝很重要
				DP(S,i+1,1,'N',dp[i&1][S][0]);
				DP(S,i+1,0,'O',dp[i&1][S][0]);
				DP(S,i+1,0,'I',dp[i&1][S][0]);
			}
			
			if(dp[i&1][S][1]){
				DP(S,i+1,1,'N',dp[i&1][S][1]);
				DP(S,i+1,2,'O',dp[i&1][S][1]);
				DP(S,i+1,0,'I',dp[i&1][S][1]);
			}
			
			if(dp[i&1][S][2]){
				DP(S,i+1,1,'N',dp[i&1][S][2]);
				DP(S,i+1,0,'O',dp[i&1][S][2]);
			}
		}
	}
	
	for(int S=0;S<(1<<k);S++){   //显然 S 的 1 的个数就是这个内层 dp DP 出来的 LIS 的长度
		(ans[__builtin_popcount(S)]+=((dp[n&1][S][0]+dp[n&1][S][1])%mod+dp[n&1][S][2])%mod)%=mod;
	}
	for(int i=0;i<=k;i++) printf("%d\n",ans[i]);
	return 0;
}

74.开心消消乐

题面

数据范围: \(N\le 1e5,T\le 10,N 是奇数\)

题解

考虑如何判断一个没有 ? 的序列合不合法。

题目中的操作不好顺序处理,转换一下:
相当于把原序列分成 \(\lfloor \frac{n}{2} \rfloor\) 个块 \([1,2],[3,4],[5,6],...,[n-2,n-1]\) 和最后一个数。
并维护一个栈,栈里面存储的是块。
我们一次考虑每个块,如果当前考虑的块是 \((x,y)\),那相当于有两种操作:

  1. \(x\) 和栈中所有块依次合并,直到栈中只剩一个数 \(z\),将 \((z,y)\) 放入栈。
  2. 直接把 \((x,y)\) 丢入栈。

考虑完所有块后让最后一个数和栈中的块依次合并,最后得到的数就是结果。
判断就是要判断是否存在一种操作方式使得最后可以得到 \(1\)
你会发现,我们其实不在乎这个栈具体长什么样,只在乎当我们把一个数 \(x\) 和栈中的所有块合并完之后会得到什么数。
即我们只需要维护栈所对应的函数 \(f:x\to (a,b)\) 表示当 \(x=0\) 时,会得到 \(a\),当 \(x=1\) 时会得到 \(b\)
并且对于这两种操作都可以快速更新出新的 \(f\)

于是我们考虑 dp 设 \(dp_{i,a,b}\) 表示是否可以在考虑完第 \(i\) 个块之后得到函数 \(f:x\to (a,b)\)
转移显然,于是我们完成了判定。

对于计数,因为这个判定的 dp 的后两维只有 \(4\) 种情况,且 dp 值天然地只有 \(0/1\) 两种情况。
所以直接上 dp of dp 即可。
状态数是 \(O(2^4 \times n)\)

code

#include<bits/stdc++.h>
#define PII pair<int,int>
#define fi first
#define se second 
using namespace std;
const int N=1e5+5,mod=998244353;

inline int read(){
	int w=1,s=0;
	char c=getchar();
	for(;c<'0'||c>'9';w*=(c=='-')?-1:1,c=getchar());
	for(;c>='0'&&c<='9';s=s*10+c-'0',c=getchar());
	return w*s;
}
int T,n;
char c[10],s[N];
int calc(char x,char y,char z){
	int u=x-'0',v=y-'0',w=z-'0';
	return c[(w<<2)+(v<<1)+u]-'0';
}
int dp[N][(1<<4)+5];
bool f1[2][2],f2[2][2];
int Hash(bool f[2][2]){
	return (f[0][0]<<3)+(f[0][1]<<2)+(f[1][0]<<1)+f[1][1];
}
void decrypt(bool f[2][2],int S){
	f[0][0]=S>>3&1;
	f[0][1]=S>>2&1;
	f[1][0]=S>>1&1;
	f[1][1]=S&1;
}
void DP(int newi,int S,char x,char y,int val){
	decrypt(f1,S);
	f2[0][0]=f2[0][1]=f2[1][0]=f2[1][1]=false;
	for(int a=0;a<=1;a++){
		for(int b=0;b<=1;b++){
			//转移 1:先放 x,再放 y
			if(x=='0') f2[calc(a+'0',y,'0')][calc(a+'0',y,'1')]|=f1[a][b];
			else f2[calc(b+'0',y,'0')][calc(b+'0',y,'1')]|=f1[a][b];
			
			//转移 2:把 (x,y) 直接丢进栈里
			int a1=calc(x,y,'0'),b1=calc(x,y,'1');
			if(a1==0&&b1==0) f2[a][a]|=f1[a][b];
			else if(a1==0&&b1==1) f2[a][b]|=f1[a][b];
			else if(a1==1&&b1==0) f2[b][a]|=f1[a][b];
			else f2[b][b]|=f1[a][b];
		}
	}
	int S2=Hash(f2);
	(dp[newi][S2]+=val)%=mod;
}
void work(){
	memset(dp,0,sizeof dp);
	dp[0][4]=1;
	for(int i=1;i<n;i+=2){
		int id=(i+1)/2;
		for(int S=0;S<(1<<4);S++){
			if(s[i]=='?'&&s[i+1]=='?'){
				DP(id,S,'0','0',dp[id-1][S]);			
				DP(id,S,'0','1',dp[id-1][S]);			
				DP(id,S,'1','0',dp[id-1][S]);			
				DP(id,S,'1','1',dp[id-1][S]);			
			}
			else if(s[i]=='?'){
				DP(id,S,'0',s[i+1],dp[id-1][S]);
				DP(id,S,'1',s[i+1],dp[id-1][S]);
			}
			else if(s[i+1]=='?'){
				DP(id,S,s[i],'0',dp[id-1][S]);
				DP(id,S,s[i],'1',dp[id-1][S]);
			}
			else DP(id,S,s[i],s[i+1],dp[id-1][S]);
		}
	}
	int id=(n-1)/2,ans=0;
	for(int S=0;S<(1<<4);S++){
		decrypt(f1,S);
		if(s[n]!='1') if(f1[1][0]||f1[1][1]) (ans+=dp[id][S])%=mod;
		if(s[n]!='0') if(f1[0][1]||f1[1][1]) (ans+=dp[id][S])%=mod;
	}
	printf("%d\n",ans);
}
signed main(){
	scanf("%d",&T);
	while(T--){
		scanf("%s%s",c,s+1);
		n=strlen(s+1);
		work();
	}
	return 0;
}

75. P1654 OSU!

根据初中数学:\((x+1)^3 = x^3 + 3\times x^2 + 3\times x + 1\)
所以每次增加一个一,答案的增加量为 \(3\times x^2 + 3\times x + 1\)
所以我们只需要在每个位置维护增加量的期望即可。

我们分别维护 \(f_i\) 表示以 \(i\) 结尾的最长 \(1\) 的个数的期望,即 \(x\) 的期望;\(g_i\) 表示以 \(i\) 结尾的最长 \(1\) 的个数的平方的期望,即 \(x^2\) 的期望。
那么 \(f\) 的转移显然: \(f_i = p_i\times (f_{i-1}+1) + (1-p[i])\times 0 = p_i\times (f_{i-1}+1)\)
\(g\) 的转移由 \((x+1)^2=x^2+2x+1\) 可得: \(g_i = p_i\times (g_{i-1}+2\times f_{i-1}+1) + (1-p_i)\times 0 = p_i\times (g_{i-1}+2\times f_{i-1}+1)\)
那么如果 \(ans_i\) 表示前 \(i\) 次操作的期望得分,就有:
\(ans_i = ans_{i-1}+期望增加量 = ans_{i-1}+(3\times g_{i-1}+3\times f_{i-1}+1)\times p_i+(1-p_i)\times 0 = ans_{i-1}+(3\times g_{i-1}+3\times f_{i-1}+1)\times p_i\)
注意: 在这个式子里 \(ans_{i-1}\) 并没有放在 \(p_i \times (...)\) 的括号里面,所以 \(ans\) 表示的就是期望得分,而不是末尾 \(x^3\) 的期望。

\(n\le 100000\) 直接递推即可,但显然可以用矩阵快速幂进行加强。

code

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n;
double p[N],f[N],g[N],ans[N];
signed main(){
	n=read();
	for(int i=1;i<=n;i++){
		scanf("%lf",&p[i]);
		f[i]=p[i]*(f[i-1]+1.0);
		g[i]=p[i]*(g[i-1]+f[i-1]*2.0+1.0);
		ans[i]=ans[i-1]+p[i]*(3.0*g[i-1]+3.0*f[i-1]+1.0);
	}
	printf("%.1lf\n",ans[n]);
	return 0;
}

76. Fluorescent

题意

一共有 \(n\) 盏灯,\(m\) 个开关,每个开关有对应的操控灯的集合,按下开关可以使这些灯的转台翻转。
\(2^m\) 中开关状态下,所有开着的灯的数量的立方的和。
数据范围: \(n\le 50,m\le 50\)

题解

主要考的是一个转换,dp 倒不是很难。

\(x_i\) 表示第 \(i\) 盏灯的状态,\(X\) 表示开着的灯的数量,那么:\(X^3=(x_1+x_2+...+x_n)\times (x_1+x_2+...+x_n)\times (x_1+x_2+...+x_n) = \sum_{1\le i,j,k \le n} [x_i \land x_j \land x_k]\)
即满足 \(x_i=x_j=x_k=1\) 的有序三元组 \((i,j,k)\) 个数。

枚举 \(i,j,k\) 然后状压 dp 求解使他满足 \(x_i=x_j=x_k=1\) 的开关状态的方案数。
\(f_{i,s}\) 表示前 \(i\) 个开关,这三盏灯的状态为 \(s\) 的方案数,转移显然。

code

#include<bits/stdc++.h>
#define int long long 
using namespace std;
const int N=50+5,mod=1e9+7;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int T,n,m;
bool flag[N][N];
int f[N][10];
signed main(){
	T=read();
	for(int _=1;_<=T;_++){
		memset(flag,0,sizeof flag);
		n=read(),m=read();
		for(int i=1;i<=m;i++){
			int k=read();
			for(int j=1;j<=k;j++){
				int x=read();
				flag[i][x]=true;
			}
		}
		int ans=0;
		for(int x=1;x<=n;x++){
			for(int y=1;y<=n;y++){
				for(int z=1;z<=n;z++){
					memset(f,0,sizeof f);
					f[0][0]=1;
					for(int i=1;i<=m;i++){
						for(int s=0;s<(1<<3);s++){
							f[i][s]=f[i-1][s];
							int t=s;
							if(flag[i][x]) t^=1ll;
							if(flag[i][y]) t^=(1ll<<1);
							if(flag[i][z]) t^=(1ll<<2);
							(f[i][s]+=f[i-1][t])%=mod;
						}
					}
					(ans+=f[m][(1<<3)-1])%=mod;
				}
			}
		}
		printf("Case #%lld: %lld\n",_,ans);
	}
	return 0;
}

77. P3239 [HNOI2015] 亚瑟王

把每张牌的期望拆开计算,即设: \(P_i\) 表示第 \(i\) 张牌被出的概率,那么,\(E=\sum_{i=1}^n P_i\times d_i\)

直接计算 \(P\) 并不好求,因为有每一轮只能出一张的限制,考虑计算一些辅助信息。
\(f_{i,j}\) 表示在 \(r\) 轮中,前 \(i\) 张牌总共出了 \(j\) 张的概率。
那么对于 \(P_i\),可以从 \(f_{i-1,j}\) 转移过来,转移时,那 \(j\) 轮因为在前 \(i-1\) 张牌就被出掉了,所以不会考虑到 \(i\),剩下的 \(r-j\) 轮都会考虑到 \(i\),那么这 \(r-j\) 轮都不出的概率是 \((1-p_i)^{r-j}\),有一次出的概率是 \(1-(1-p_i)^{r-j}\)
所以: \(P_i=\sum_{j=0}^{i-1} f_{i-1,j} \times (1-(1-p_i)^{r-j})\)

接下来看 \(f\) 的转移。

  1. \((1-p_i)^{r-j} \times f_{i-1,j} \to f_{i,j}\),表示 \(i\) 这张牌一次都没有出过,转移系数的意义和上面一样。
  2. \(( 1-(1-p_i)^{r-j+1} ) \times f_{i-1,j-1} \to f_{i,j}\),表示第 \(i\) 张牌出过了,转移系数意义同上。

复杂度:\(O(Tn^2)\)

code

#include<bits/stdc++.h>
using namespace std;
const int N=220+5;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int T,n,r,d[N];
double p[N],P[N],f[N][N];
double quick_power(double a,int b){
	double ans=1.0;
	while(b){
		if(b&1) ans*=a;
		b>>=1,a*=a;
	}
	return ans;
}
signed main(){
	T=read();
	while(T--){
		n=read(),r=read();
		for(int i=1;i<=n;i++) cin>>p[i]>>d[i];
		memset(f,0,sizeof f);
		f[0][0]=1.0;
		for(int i=1;i<=n;i++){
			for(int j=0;j<=min(r,i);j++){
				f[i][j]=quick_power(1.0-p[i],r-j) * f[i-1][j] + ((j > 0) ? ((1.0 - quick_power(1.0-p[i],r-j+1)) * f[i-1][j-1]) : 0); 
			}
		}
		double ans=0;
		for(int i=1;i<=n;i++){
			P[i]=0;
			for(int j=0;j<=min(r,i-1);j++){
				P[i]+=f[i-1][j]*(1.0 - quick_power(1.0-p[i],r-j));
			}
			ans+=P[i]*d[i]*1.0;
		}
		printf("%.10lf\n",ans);
	}
	return 0;
}

78.P3600 随机数生成器

首先根据期望的定义写出:\(E=\sum_{i=1}^x p_i \times i\)
其中 \(p_i\) 表示最后的最大值为 \(i\) 的概率。
但是这玩意非常不好算,所以变成前缀和形式,即:\(p_i\) 表示最后的最大值 \(\le i\) 的概率。
然后求得时候差分一下:\(E=\sum_{i=1}^x (p_i-p_{i-1})\times i\)

怎么算 \(p_i\)?
最大值 \(\le i\),意味着每一个区间的最小值都 \(\le i\),也等价于每一个区间都有 \(\le i\) 的数。
我们考虑一个经典套路: 当所有区间互不包含时,将区间左端点升序排序,则右端点也升序。
而在这道题里面,当一个区间包含另一个区间,那这个区间的 \(min\) 一定 \(\le\) 被包含的那个区间的 \(min\),也就一点用也没有了,可以删掉。
\(g_j\) 表示选出 \(j\) 个点,并使得每个区间都包含至少一个点的方案数,那么:\(p_i = \frac {\sum g_j \times i^j \times (x-i)^{n-j}}{x^n}\)

下面假设已经减去包含关系的区间,并将区间升序排序。

\(g\) 就可以愉快的 dp 了。
\(f_{i,j}\) 表示在前 \(i\) 个点中放 \(j\) 个点,第 \(i\) 个点必须放,使得覆盖所有左端点小于等于 \(i\) 的区间的方案数。
\(l_i\) 表示最靠左的包含 \(i\) 的区间,\(r_i\) 表示最靠右的,容易知道那么 \(l_i\)\(r_i\) 的区间都包含 \(i\)
特殊的,如果没有包含 \(i\) 的区间那么 \(r_i\) 表示最靠右的在 \(i\) 左边的区间,\(l_i\) 此时等于 \(r_i+1\)
转移枚举上一个放的点的位置: \(f_{i,j}=\sum_{0\le k<i,r_k\ge l_i -1} f_{k,j-1}\)
前缀和优化即可,\(O(n^2)\)
那么 \(g_j=\sum_{r_i=q} f_{i,j}\) ( \(q\) 是去掉包含关系之后的区间数量)。

code

#include<bits/stdc++.h>
#define int long long 
using namespace std;
const int N=2e3+5,mod=666623333;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,x,q;
struct P{
	int l,r;
}a[N]; 

int l[N],r[N],f[N][N],pre[N][N];  //pre[j][i]表示∑f[k][j](0<=k<=i) 
int g[N],p[N];
int quick_power(int a,int b){
	int ans=1;
	while(b){
		if(b&1) (ans*=a)%=mod; 
		b>>=1,(a*=a)%=mod;
	}
	return ans;
}
signed main(){
	n=read(),x=read(),q=read();
	for(int i=1;i<=q;i++){
		a[i].l=read(),a[i].r=read();
	}
	
	for(int i=1;i<=q;i++){
		for(int j=1;j<=q;j++){
			if(a[j].l>n||i==j) continue;
			if(a[i].l<=a[j].l&&a[i].r>=a[j].r){
				a[i]={n+1,n+1};
				break;
			}
		}
	}
	sort(a+1,a+q+1,[](P x,P y){return x.l<y.l;});
	while(a[q].l>n) q--;
	
	for(int i=1;i<=n;i++){
		for(int j=1;j<=q;j++){
			if(a[j].l<=i) r[i]=j;
			if(a[j].l<=i&&a[j].r>=i){
				if(!l[i]) l[i]=j;
			}
		}
		if(!l[i]) l[i]=r[i]+1;
	}
	
	f[0][0]=1;
	pre[0][0]=1; 
	for(int i=1;i<=n;i++) pre[0][i]=pre[0][i-1];
	for(int i=1;i<=n;i++){
		int k;
		for(int j=0;j<i;j++) 
			if(r[j]+1>=l[i]){
				k=j;
				break;
			}
		for(int j=1;j<=i;j++){
			if(k==0) f[i][j]=pre[j-1][i-1];
			else f[i][j]=(pre[j-1][i-1]-pre[j-1][k-1]+mod)%mod;
			pre[j][i]=(pre[j][i-1]+f[i][j])%mod;
		} 
	}
	
	for(int j=0;j<=n;j++){
		for(int i=0;i<=n;i++)
			if(r[i]==q) (g[j]+=f[i][j])%=mod; 
	}
	
	for(int i=1;i<=n;i++){
		for(int j=0;j<=n;j++){
			( p[i] += g[j] * quick_power(i,j) % mod * quick_power(x-i,n-j) % mod * quick_power( quick_power(x,n) , mod-2 ) )%=mod; 
		}
	}
	
	int ans=0;
	for(int i=1;i<=n;i++){
		(ans+=(p[i]-p[i-1]+mod)%mod*i%mod)%=mod;
	}
	printf("%lld\n",ans);
	return 0;
}

79.CF1151F Sonya and Informatics

显然的是最后一定是 000...1111,并且 0 的个数不变。
下面记 \(m\)0 的个数。

会发现这个 \(k\) 很大,\(n\) 很小,经验告诉我们最后的方法一定是用 dp 然后第一维表示操作次数,第二维大小为 \(n\),然后矩阵加速。
在联系第一句这个跟个废话一样的结论,可以设计出: \(f_{i,j}\) 表示前 \(i\) 次操作,使得前 \(m\) 个数里有 \(j\)0 的方案数。
概率就是 \(\frac {f_{k,m}}{(C^2_n)^k}\)
转移还是比较显然的:

  1. 把后面的 \(m-j\)0 和前面的 \(m-j\)1 中的一个换一下:\(f_{i,j}\times (m-j)^2 \to f_{i+1,j+1}\)
  2. 把后面的 \(n-m-m+j\)1 和前面的 \(j\)0 中的一个换一下:\(f_{i,j}\times (n-2\times m+j)\times j \to f_{i+1,j-1}\)
  3. 最后就是 \(0\) 的个数不变的情况,容斥一下即可: \((\frac{n\times (n-1)}{2} - (m-j)^2 - (n-2\times m+j)\times j)\times f_{i,j} \to f_{i+1,j}\)

最后用矩阵加速即可。

code

int n,k;
int a[N],m;
struct Matrix{
	int n,m,a[105][105];
	void Init(){memset(a,0,sizeof a);}
}F,A;
Matrix operator * (const Matrix &A,const Matrix &B){
	Matrix C; C.Init();
	C.n=A.n,C.m=B.m;
	for(int k=0;k<=A.m;k++){
		for(int i=0;i<=C.n;i++){
			for(int j=0;j<=C.m;j++){
				(C.a[i][j]+=A.a[i][k]*B.a[k][j]%mod)%=mod;
			}
		}
	}
	return C;
}
Matrix Quick_power(Matrix A,int b){
	Matrix Ans;
	Ans.n=n,Ans.m=n;
	for(int i=0;i<=Ans.n;i++){  
		for(int j=0;j<=Ans.m;j++){
			if(i==j) Ans.a[i][j]=1;
			else Ans.a[i][j]=0;
		}
	}
	while(b){
		if(b&1) Ans=Ans*A;
		b>>=1,A=A*A;
	} 
	return Ans;
}
int quick_power(int a,int b){
	int ans=1;
	while(b){
		if(b&1) ans=ans*a%mod;
		b>>=1,a=a*a%mod;
	}
	return ans;
}
signed main(){
	n=read(),k=read();
	for(int i=1;i<=n;i++) a[i]=read(),m+=(a[i]==0);
	int cnt=0;
	for(int i=1;i<=m;i++) cnt+=(a[i]==0);
	
	F.n=1,F.m=m; F.Init();
	F.a[1][cnt]=1;
	A.n=m,A.m=m;
	A.Init();
	for(int i=0;i<=m;i++){
		A.a[i][i+1]=(m-i)*(m-i);
		if(i>0) A.a[i][i-1]=(n-2*m+i)*i;
		A.a[i][i]=(n*(n-1)/2ll - (m-i)*(m-i) - (n-2*m+i)*i);
	}
	A=Quick_power(A,k);
	F=F*A;
	
	printf("%lld\n",F.a[1][m]*quick_power( quick_power(n*(n-1)/2ll,k) , mod-2)%mod); 
	return 0;
}

80. CF1737E Ela Goes Hiking

思维题,手玩可以发现:
一开始那些往右的蚂蚁一定会被他右边第一只往左的蚂蚁吃掉。
我们可以钦定第 \(n\) 只蚂蚁就是往左的,这是显然正确的,如果他一开始是往右的他在碰到挡板之后就变成往左了。
于是所有往右的蚂蚁一开始都会被吃掉,这可以看做第一个阶段。

对于那些往左的若干个蚂蚁他们在接下来的遭遇一定是:

  1. 第一只蚂蚁掉头和第二只蚂蚁相遇,决出一个获胜者,而不管是谁胜利,获胜者都将与第三只蚂蚁再次相遇
    所以可以认为他们是合并了
  2. 由此不断进行合并,直到剩余 \(1\) 只蚂蚁。

这是游戏的第二个阶段。

在上述不断合并的过程中我们关心的无非只有大小和编号,如果要让第 \(i\) 只蚂蚁获的最终胜利的话。
设一开始第 \(i\) 只蚂蚁前面的第一只向左的蚂蚁是 \(j\)
首先他得往左,那么它的初始重量是 \(i-j\),因为他会吃掉 \((j,i)\) 中向右的蚂蚁。
其次在前 \(j\) 只蚂蚁合并完之后,胜出的那个大蚂蚁在和第 \(i\) 只蚂蚁决斗的时候第 \(i\) 只蚂蚁要胜出。
而大蚂蚁无论编号是什么,他的重量一定是 \(j\),所以 \(i-j\ge j\),即 \(j\le \lfloor \frac{i}{2} \rfloor\)
所以前 \(\lfloor \frac{i}{2} \rfloor\) 只蚂蚁随便,\([\lfloor \frac{i}{2} \rfloor+1,i-1]\) 的蚂蚁一定要向右,\(i\) 一定要向左,方案数为 \(2^{\lfloor \frac{i}{2} \rfloor}\)

接下来第 \(i\) 只蚂蚁变成 \(i\),他要接着向右去决斗,接下来的问题相当于要给 \([i+1,n]\) 分段,设每一段的长度为 \(len_1,len_2,...,len_m\)
那就要求:

  1. \(i>len_1\)
  2. \(i+len_1>len_2\)
  3. \(i+len_1+len_2>len_3\)
    ...

这种分段的一眼 dp, 设 \(f_i\) 表示第 \(i\) 只蚂蚁(重量为 \(i\))吃掉 \([i+1,n]\) 的蚂蚁,\([i+1,n]\) 蚂蚁的分配方案数。
转移枚举后面第一个往左的蚂蚁: \(f_i=\sum_{i<j<2\times i} f_j\)

最后第 \(i\) 只蚂蚁的答案是 \(\frac{2^{\lfloor \frac{i}{2} \rfloor} \times f_i}{2^{n-1}}\)\(n-1\) 是因为一开始我们钦定了第 \(n\) 只蚂蚁是往左的。

复杂度是线性的。

code

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e6+5,mod=1e9+7;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int T,n,f[N],suf[N];
int quick_power(int a,int b){
	int ans=1;
	while(b){
		if(b&1) (ans*=a)%=mod;
		b>>=1,(a*=a)%=mod;
	}
	return ans;
}
signed main(){
	T=read();
	while(T--){
		n=read();
		for(int i=1;i<=2*n;i++) f[i]=suf[i]=0;
		f[n]=1;
		suf[n]=1;
		for(int i=n-1;i>=1;i--)	f[i]=(suf[i+1]-suf[2*i]+mod)%mod,suf[i]=(suf[i+1]+f[i])%mod;
		for(int i=1;i<=n;i++){
			printf("%lld\n",quick_power(2,i/2) * f[i] % mod * quick_power( quick_power(2,n-1) , mod-2) % mod);
		}
	}
	return 0;
}

81.[IOI 2008] Island

陈年老题,当时竟然没过,所以这个份代码其实是调出来的,代码基本用以前写的框架,码风难评。

根据题意:
会给你一个基环树森林,并且由渡船的规则可知,你离开一棵基环树之后就不能回来了,所以等价于求每一棵基环树的"直径"(即最长简单路径)。

基环树经典思路就是以那个基环作为广义的根节点,然后对每棵子树分别求解,最后在环上合并。
基环树的直径有两张可能:

  1. 在每棵子树内部
  2. 经过基环上的若干条边,起终点在两棵不同的子树内。

所以我们先经典树形 DP,求出 \(dp_i\) 表示 \(i\) 子树内的直径以及 \(f_i\) 表示 \(i\) 到他子树内的最长路径。
那么第二种情况相当于求 \(max(f_i+f_j+dist(i,j))\)\(dist(i,j)\) 表示环上 \(i,j\) 之间距离的最大值,有两种可能。
断环成链,复制两遍。
对于断环成链之后长度为 \(2\times len\) 的链,两点 \(j<i\) 的答案为:\(S_i-S_j+f_i+f_j\)\(S\) 为前缀和数组。
即求在满足 \(i-len<j<i\)\(j\)\(f_j-S_j\) 的最大值,滑动窗口即可。

code

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e6+5;
inline int read(){
	int w = 1, s = 0;
	char c = getchar();
	for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
	for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
	return s * w;
}	
int n;
int head[N],to[N<<1],val[N<<1],Next[N<<1],deg[N],tot;
void add(int u,int v,int w){
	deg[v]++;
	to[++tot]=v,val[tot]=w,Next[tot]=head[u],head[u]=tot;
}

int id[N],num;   
void dfs(int u){
	id[u]=num;
	for(int i=head[u];i;i=Next[i]){
		int v=to[i];
		if(id[v]) continue;
		dfs(v);
	}
}
queue<int> q;    
bool flag[N];   //记录拓扑排序时进过队列的数 
void topo(){
	for(int i=1;i<=n;i++) if(deg[i]==1) q.push(i); 
	while(q.size()){
		int u=q.front(); q.pop();
		flag[u]=1;	
		for(int i=head[u];i;i=Next[i]){
			int v=to[i];
			if(flag[v]) continue;
			deg[v]--;
			if(deg[v]==1) q.push(v);
		}
	}	
} 

int dp[N],f[N]; 
int F[N];  //记录每棵基环树里的最长链:有两种情况
void dfs1(int u,int fa,int rt){	
	for(int i=head[u];i;i=Next[i]){
		int v=to[i],w=val[i];
		if(v==fa||!flag[v]) continue;
		dfs1(v,u,rt);
		dp[rt]=max(dp[rt],f[u]+f[v]+w);
		f[u]=max(f[u],f[v]+w); 
	}
}

bool stater[N];
int cnt,hoop[N<<1],pre[N<<1];
deque<int> dq;
signed main(){
	n=read();
	for(int i=1;i<=n;i++){
		int v=read(),w=read();
		add(i,v,w),add(v,i,w);
	}
	
	for(int i=1;i<=n;i++){  //找出每一棵基环树,并给同一棵基环树上的点编号
		if(!id[i]){
			++num;
			dfs(i);
		}
	}
	
	topo();//拓扑排序 
	
	for(int u=1;u<=n;u++){
		if(!stater[u]&&!flag[u]){
			cnt=0;
			hoop[++cnt]=u,pre[cnt]=0;
			int x=u,tmp;
			while(!stater[x]){
				stater[x]=true;
				for(int i=head[x];i;i=Next[i]){
					int y=to[i],w=val[i];
					if(!flag[y]&&!stater[y]){
						x=y,hoop[++cnt]=y,pre[cnt]=w;
						break;
					}
					else if(y==u) tmp=w;
				}
			}
			hoop[++cnt]=u,pre[cnt]=tmp;
			int len=cnt-1;  //环长,注意到 hoop[cnt] 此时是 u, 
			for(int i=2;i<=len;i++) hoop[++cnt]=hoop[i],pre[cnt]=pre[i];
			for(int i=2;i<=2*len;i++) pre[i]+=pre[i-1];
			for(int i=1;i<=len;i++)
				dfs1(hoop[i],0,hoop[i]),F[id[u]]=max(F[id[u]],dp[hoop[i]]);
			
			
			while(dq.size()) dq.pop_back();
			dq.push_front(1);
			for(int i=2;i<=2*len;i++){
				while(dq.size()&&dq.front()<=i-len) dq.pop_front();
				int j=dq.front();
				F[id[u]]=max(F[id[u]],f[hoop[i]]+pre[i]+f[hoop[j]]-pre[j]);
				while(dq.size()&&f[hoop[dq.back()]]-pre[dq.back()] <= f[hoop[i]]-pre[i]) dq.pop_back();
				dq.push_back(i);
			}
		}
	}	
	
	int ans=0;
	for(int i=1;i<=num;i++) ans+=F[i];
	printf("%lld\n",ans);
	return 0;
}

82.不条理狂诗曲

考虑分治,思考如何计算跨越 \(mid\) 的区间的答案。

可以用 \(dp\) 求出:
\(fl_i\):表示区间 \([i,mid]\) , \(a_{mid}\) 可选可不选,选择若干不相邻的数的和的最大值。
\(gl_i\):表示区间 \([i,mid]\) , \(a_{mid}\) 不选,选择若干不相邻的数的和的最大值。
\(fr_i\):表示区间 \([mid+1,i]\) , \(a_{mid+1}\) 可选可不选,选择若干不相邻的数的和的最大值。
\(gr_i\):表示区间 \([mid+1,i]\) , \(a_{mid+1}\) 不选,选择若干不相邻的数的和的最大值。
那么一个区间 \([i,j],(i\le mid,j\ge mid+1)\) 的答案为 \(max(fl_i+gr_j,gl_i+fr_j)\)
如果这个东西的值为第二项那么: \(fl_i-gl_i<fr_j-gr_j\)

把左边按照 \(fl_i-gl_i\) 排序(不要取模),并求一下排序后,\(gl_i\) 的前缀和以及 \(fl_i\) 的后缀和。
对于右边的每一个 \(j\),二分找到最后一个 \(fl_i-gl_i<fr_j-gr_j\)\(i\),那么所有以 \(j\) 为右端点的区间的答案为:\(fr_j\times (i-l+1)+pre_i + gr_j\times (mid-i)+suf_{i+1}\)

复杂度为 \(O(n\log ^2 n)\)

code

#include<bits/stdc++.h>
#define int long long 
using namespace std;
const int N=1e5+5,mod=1e9+7;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,a[N],ans;
int fl[N],gl[N],fr[N],gr[N],pre[N],suf[N];
struct P{
	int x,y,c;
}b[N];
int find(int l,int r,int x){
	int res=l-1;
	while(l<=r){
		int mid=(l+r)>>1;
		if(b[mid].c<x) res=mid,l=mid+1;
		else r=mid-1;
	}
	return res;
}
void solve(int l,int r){
	if(l==r){
		(ans+=a[l])%=mod;
		return;
	}
	int mid=(l+r)>>1;
	solve(l,mid);
	solve(mid+1,r);
	
	for(int i=l;i<=mid+1;i++) fl[i]=gl[i]=0;
	for(int i=mid;i<=r;i++) fr[i]=gr[i]=0;
	
	gl[mid]=0,fl[mid]=a[mid];
	for(int i=mid-1;i>=l;i--){
		fl[i]=max(fl[i+1],fl[i+2]+a[i]);
		gl[i]=max(gl[i+1],gl[i+2]+a[i]);
	}
	fr[mid+1]=a[mid+1],gr[mid+1]=0;
	for(int i=mid+2;i<=r;i++){
		fr[i]=max(fr[i-1],fr[i-2]+a[i]);
		gr[i]=max(gr[i-1],gr[i-2]+a[i]);
	}
	
	for(int i=l;i<=mid;i++) b[i]={fl[i],gl[i],fl[i]-gl[i]};
	sort(b+l,b+mid+1,[](P x,P y){return x.c<y.c;});
	pre[l-1]=0;
	for(int i=l;i<=mid;i++) pre[i]=(pre[i-1]+b[i].y)%mod;
	suf[mid+1]=0;
	for(int i=mid;i>=l;i--) suf[i]=(suf[i+1]+b[i].x)%mod;
	
	for(int j=mid+1;j<=r;j++){
		int i=find(l,mid,fr[j]-gr[j]);
		(ans+=fr[j]*(i-l+1)%mod+pre[i]+gr[j]*(mid-i)%mod+suf[i+1])%=mod;
	}
}
signed main(){
//	freopen(".in","r",stdin);
//	freopen(".out","w",stdout);
	n=read();
	for(int i=1;i<=n;i++) a[i]=read();
	
	solve(1,n);
	
	printf("%lld\n",ans);
	return 0;
}

83.Sum

首先会发现一个性质:
一组里面的数要么全选,要么全不选,最多只有一组数是只选了一部分的。

证明:
如果有两组数,分别选了一部分,那么比较它们的队尾 \(x,y\) 如果 \(x>y\) 那么完全可以把 \(y\) 去掉,选 \(x\) 的下一个,因为他保证了每一组非严格单增。

线段树分治即可,递归到叶子的时候就把他当做那个没选满的组。

时间复杂度:\(O(nk\log n)\)

code

#include<bits/stdc++.h>
#define int long long 
using namespace std;
const int N=3e3+5;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,k,t[N],f[N],ans;  //背包数组f[i]要求恰好放满
vector<int> a[N];  
void solve(int l,int r){
	if(l==r){
		for(int i=0;i<=k;i++) ans=max(ans,f[i]+a[l][min(t[l],k-i)]);
		return; 
	}
	vector<int> tmp;
	for(int i=0;i<=k;i++) tmp.push_back(f[i]);
	int mid=(l+r)>>1;
	for(int i=l;i<=mid;i++){
		for(int j=k;j>=t[i];j--) f[j]=max(f[j],f[j-t[i]]+a[i][t[i]]);
	}
	solve(mid+1,r);
	for(int i=0;i<=k;i++) f[i]=tmp[i];
	for(int i=mid+1;i<=r;i++){
		for(int j=k;j>=t[i];j--) f[j]=max(f[j],f[j-t[i]]+a[i][t[i]]);
	}
	solve(l,mid);
	for(int i=0;i<=k;i++) f[i]=tmp[i];
	tmp.clear();   //及时释放 
}
signed main(){
//	freopen(".in","r",stdin);
//	freopen(".out","w",stdout);
	n=read(),k=read();
	for(int i=1;i<=n;i++){
		t[i]=read();
		a[i].push_back(0);
		for(int j=1;j<=t[i];j++) a[i].push_back(read()),a[i][j]+=a[i][j-1];
	}
	memset(f,-0x3f,sizeof f); 
	f[0]=0;
	solve(1,n);
	printf("%lld\n",ans);
	return 0;
}

84. [PA 2021] Od deski do deski

\(f_i\) 表示是否可以删完前 \(i\) 个数,转移很简单,\(a_j=a_i\)\(f_{j-1}\to f_i\)
我们定义一个颜色 \(c\) 是好颜色,当且仅当目前存在一个 \(j\) 满足:

  1. \(a_j=c\)
  2. \(f_{j-1}=true\)

所以如果有一个 \(i\) 和某个好颜色颜色一样,那么 \(f_i\) 一定 \(=true\)

于是设 \(g_{i,j,0/1}\) 表示前 \(i\) 个数,有 \(j\) 个好颜色,\(f_i=0/1\) 的序列个数。
转移时分类讨论:

  1. \(f_i=0\)\(a_{i+1}\) 和某个好颜色一样:\(g_{i,j,0} \times j \to g_{i+1,j,1}\)
  2. \(f_i=0\)\(a_{i+1}\) 和之前任意一个好颜色都不一样:\(g_{i,j,0} \times (m-j) \to g_{i+1,j,0}\)
  3. \(f_i=1\)\(a_{i+1}\) 和某个好颜色一样:\(g_{i,j,1} \times j \to g_{i+1,j,1}\)
  4. \(f_i=1\)\(a_{i+1}\) 和之前任意一个好颜色都不一样,注意这个时候 \(a_{i+1}\) 成为了一个从没出现过的好颜色:\(g_{i,j,1} \times (m-j) \to g_{i+1,j+1,0}\)

code

#include<bits/stdc++.h>
#define int long long 
using namespace std;
const int N=3e3+5,mod=1e9+7;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,m,g[N][N][2];
signed main(){
	freopen("del.in","r",stdin);
	freopen("del.out","w",stdout);
	n=read(),m=read();
	g[0][0][1]=1;
	for(int i=0;i<=n;i++){
		for(int j=0;j<=min(i,m);j++){
			(g[i+1][j][1]+=g[i][j][0]*j%mod)%=mod;
			(g[i+1][j][0]+=g[i][j][0]*(m-j)%mod)%=mod;
			(g[i+1][j][1]+=g[i][j][1]*j%mod)%=mod; //相同的好颜色算一个
			(g[i+1][j+1][0]+=g[i][j][1]*(m-j)%mod)%=mod; 
		}
	}	
	int ans=0;
	for(int j=0;j<=min(n,m);j++) (ans+=g[n][j][1])%=mod;
	printf("%lld\n",ans);
	return 0;
}

85.Divan and Kostomuksha (hard version)

如果设 \(g_i=\gcd(a_1,a_2,...,a_i)\),那么 \(g\) 是个单调递减的数列。
考虑 dp,设 \(f_i\) 表示最后答案的每个 \(g_j\) 都是 \(i\) 的倍数的权值的最大值(当然不一定可以凑出 \(n\)\(g_j\),具体可以看转移方程来理解)。
为了求这个,我们先预处理:\(c_i\) 表示 \(a\) 序列中 \(i\) 的倍数的个数。
那么 dp 时我们从大到小枚举 \(i\),因为每个 \(g_j\) 都要是 \(i\) 的倍数,所以我们只能从 \(i\) 的倍数 \(k\) 转移过来,方程为:\(f_i=f_k+(c_i-c_k)\times i\)
意思是,在 \(f_k\) 所对应的 \(a\) 序列后面,再加上 \(c_i-c_k\)\(i\) 的倍数,但不是 \(k\) 的倍数的数。
初始值:\(f_i=i\times c_i\)
答案就是所有 \(c_i=n\)\(f_i\) 的最大值,因为要满足能构成一个长度为 \(n\)\(g\) 序列。

转移时直接枚举倍数虽然也是调和级数,但会被卡,所以可以只枚举 \(k=i\times p\)\(p\) 是质数)的 \(k\),正确性也是显然的,如果一个 \(k'=i\times p \times x\),那么在 \(k\) 的转移时已经转移过 \(k'\),这里就没必要再转移了。

code

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5,M=20000000+5;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,a[N],maxn,cnt[M],c[M];
long long f[M],ans;
int m,pri[6000000];
bool stater[M];
void Eular(){
	stater[1]=true;
	for(int i=2;i<M;i++){
		if(!stater[i]) pri[++m]=i;
		for(int j=1;j<=m&&(long long)(pri[j]*i)<(long long)M;j++){
			stater[i*pri[j]]=true;
			if(i%pri[j]==0) break;
		}
	}
}
signed main(){
//	freopen(".in","r",stdin);
//	freopen(".out","w",stdout);
	Eular();
	n=read();
	for(int i=1;i<=n;i++) a[i]=read(),maxn=max(maxn,a[i]),cnt[a[i]]++;
	for(int i=1;i<=maxn;i++)
		for(int j=i;j<=maxn;j+=i)
			c[i]+=cnt[j];
	for(int i=maxn;i>=1;i--){
		f[i]=1ll*i*c[i];
		for(int j=1;j<=m&&pri[j]*i<=maxn;j++){
			f[i]=max( f[i],f[i*pri[j]]+1ll*i*(c[i]-c[i*pri[j]]) );
		}
		if(c[i]==n){
			ans=max(ans,f[i]);
		} 
	}
	printf("%lld\n",ans);
	return 0;
}

86.小 Y 和恐怖的奴隶主

首先设 \(f_{i,a,b,c}\) 表示第 \(i\) 次攻击后有 \(a\)\(1\) 血仆从,\(b\)\(2\) 血仆从,\(c\)\(3\) 血仆从的概率,\(g_i\) 表示 \(i\) 次攻击的期望扣血量。
\(g,f\) 的转移是显然的。

注意到要满足 \(a+b+c\le k\)
所以 \(f\) 的合法的状态数比较少,只有 \(165\),然后加上 \(g\) 的话一共 \(166\) 个状态。

所以考虑状压之后矩阵快速幂转移,然后直接跑是 \(O(T \times 166^3 \times \log n)\) 会 TLE。
但是可以预处理出所有 \(2\) 的幂次的矩阵,这样只需要用一个初始向量乘以 \(\log n\) 个矩阵即可。
复杂度变为:\(O(166^3 \times \log n + T \times 166^2 \times \log n)\)

code

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5,mod=998244353;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int T,m,k,inv[N];
int quick_power(int a,int b){
	int ans=1;
	while(b){
		if(b&1) (ans*=a)%=mod;
		(a*=a)%=mod,b>>=1; 
	}
	return ans;
}
void work1(){
	while(T--){
		int n=read(),ans=quick_power(inv[2],n)*n%mod;
		for(int i=1;i<=n;i++)
			(ans += quick_power(inv[2],i)*(n-1)%mod )%=mod;
		printf("%lld\n",ans);
	}
} 

struct Matrix{
	int n,m,a[167][167];
	void Init(){
		memset(a,0,sizeof a);
	}
}F,A[100];
Matrix operator * (Matrix &A,Matrix &B){
	Matrix C; C.Init();
	C.n=A.n,C.m=B.m;
	for(int i=1;i<=C.n;i++){
		for(int j=1;j<=C.m;j++){
			__int128 tmp=0;
			for(int k=1;k<=A.m;k++){
				tmp+=A.a[i][k]*B.a[k][j];
			}
			C.a[i][j]=tmp%mod;
		}
	}
	return C;
}

int cnt,id[10][10][10];
void Init2(){
	for(int a=0;a<=k;a++){
		for(int b=0;a+b<=k;b++){
			id[a][b][0]=++cnt;
		}
	} 
	++cnt;  //表示记录boss扣血期望 
	for(int a=0;a<=k;a++){
		for(int b=0;a+b<=k;b++){
			int ID=id[a][b][0],Inv=inv[a+b+1];  //Inv表示攻击场上任何一个人的概率 
			if(a) A[0].a[ID][ id[a-1][b][0] ]=Inv*a%mod;
			if(b){
				if(a+b<k) A[0].a[ID][ id[a+1][b][0] ]=Inv*b%mod;
				else A[0].a[ID][ id[a+1][b-1][0] ]=Inv*b%mod;
			}
			A[0].a[ID][ID]=Inv;  //表示攻击到boss
			A[0].a[ID][cnt]=Inv;  //转移到表示期望的那个状态 
		}
	} 	
	A[0].a[cnt][cnt]=1;  //注意每次操作要继承上一次操作的期望
	A[0].n=A[0].m=cnt; 
}
void Init3(){
	for(int a=0;a<=k;a++){
		for(int b=0;a+b<=k;b++){
			for(int c=0;a+b+c<=k;c++){
				id[a][b][c]=++cnt;
			}
		}
	} 
	++cnt; 
	for(int a=0;a<=k;a++){
		for(int b=0;a+b<=k;b++){
			for(int c=0;a+b+c<=k;c++){
				int ID=id[a][b][c],Inv=inv[a+b+c+1];  
				if(a) A[0].a[ID][ id[a-1][b][c] ]=Inv*a%mod;
				if(b){
					if(a+b+c<k) A[0].a[ID][ id[a+1][b-1][c+1] ]=Inv*b%mod;
					else A[0].a[ID][ id[a+1][b-1][c] ]=Inv*b%mod;
				}
				if(c){
					if(a+b+c<k) A[0].a[ID][ id[a][b+1][c] ]=Inv*c%mod;
					else A[0].a[ID][ id[a][b+1][c-1] ]=Inv*c%mod; 
				}
				A[0].a[ID][ID]=Inv; 
				A[0].a[ID][cnt]=Inv; 
			} 
		}
	} 	
	A[0].a[cnt][cnt]=1; 
	A[0].n=A[0].m=cnt; 
}

signed main(){
//	freopen("patron.in","r",stdin);
//	freopen("patron.out","w",stdout);
	T=read(),m=read(),k=read();
	inv[1]=1;
	for(int i=2;i<N;i++) inv[i]=(mod-mod/i)*inv[mod%i]%mod;
	
	if(m==1){
		work1();
		return 0;
	} 
	if(m==2) Init2();
	else Init3();
	for(int i=1;i<=60;i++) A[i]=A[i-1]*A[i-1];
	while(T--){
		int n=read();
		F.Init();
		F.n=1,F.m=cnt; 
		if(m==2) F.a[1][ id[0][1][0] ]=1;
		else F.a[1][ id[0][0][1] ]=1;
		for(int i=0;i<=60;i++)
			if(n>>i&1) F=F*A[i];
		printf("%lld\n",F.a[1][cnt]);
	}
	return 0;
}

87.阿儿克

题面

一个圆上有 \(3n\) 个点,一共有 \(n\) 种颜色,且每种颜色恰好 \(3\) 个点,问有多少种把这个圆分成 \(n\) 段圆弧的方式,使得:

  1. 任意两段圆弧不交。
  2. 每一个圆弧的两个端点颜色都为 \(c\),且不经过另一个颜色为 \(c\) 的端点。

数据范围: \(n\le 2e5\)

题解

每个颜色很明显只有三种连接方式,于是可以枚举颜色 1 用的是哪种连接方法,这样变成链上的问题了。

恰好 \(n\) 段圆弧不太好刻画,于是我们可以先求出 \(f_i\) 表示前 \(i\) 个数里最多可以连出多少圆弧,以及此时的方案数。
用一个 pair 记录。
转移是显然的。

最后只要合并三种情况即可,如果最多的圆弧个数不足 \(n-1\)(因为第 \(1\) 种颜色去掉了) 个答案就是 \(0\)

code

#include<bits/stdc++.h>
#define PII pair<int,int>
#define fi first
#define se second
#define int long long 
using namespace std;
const int N=2e5+5,mod=998244353;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,a[N*6],t[N];
PII operator +(PII x,PII y){   //重载 + 运算符 
	if(y.fi==x.fi) (x.se+=y.se)%=mod;
	else if(y.fi>x.fi) x=y;
	return x;
}
PII f[N*6];
int pre[N*6],lst[N*6];
PII solve(int l,int r){  //表示对 [l,r] 进行 DP 
	for(int i=1;i<=n/3;i++) lst[i]=0;
	for(int i=l;i<=r;i++){
		pre[i]=lst[a[i]];
		lst[a[i]]=i;
	}
	f[l-1]={0,1};
	for(int i=l;i<=r;i++){
		f[i]=f[i-1];
		if(pre[i]>=l){
			PII tmp=f[pre[i]-1];
			tmp.fi+=1;
			f[i]=f[i]+tmp;
		}
	}
	return f[r];
}
signed main(){
	freopen("arc.in","r",stdin);
	freopen("arc.out","w",stdout);
	n=read(); n*=3;
	for(int i=1;i<=n;i++){ 
		a[i]=read(),a[i+n]=a[i];
		if(a[i]==1) t[++t[0]]=i; 
	} 
	PII ans=solve(t[2]+1,t[1]+n-1) + solve(t[3]+1,t[2]+n-1) + solve(t[1]+1,t[3]-1);
	if(ans.fi==n/3-1)  printf("%lld\n",ans.se);
	else puts("0");
	return 0;
}

88.归并

题面

定义对 \(k\) 个长度为 \(n\) 的序列的归并为执行以下操作 \(kn\) 次后得到的序列 \(A\):每次取出某个非空序列的开头并放入 \(A\) 的末尾。
两个归并不同当且仅当某一次操作不同。
我们称一个归并是合法的,当且仅当其中不存在长度为 \(k\) 的子串满足这个子串中的数全部相同。
现在给定 \(m\) 个长度为 \(n\) 的排列,排列的编号从 \(1\)\(m\)
\(f(l,r)\) 表示只考虑编号在 \([l,r]\) 中的排列,他们的合法归并的个数。
你需要求出 \(\sum_{i=1}^m \sum_{j=i+1}^m f(i,j)\)

保证数据随机生成

数据范围: \(n,m\le 300\)

题解

先考虑对单个问题的求解,即如何求 \(f(1,m)\)
\(f_i\) 表示当前归并出的序列 \(A\) 末尾第一次出现 \(m\)\(a_{1,i}\) (即第一个排列的第 \(i\) 个数),且前面都合法的方案数(不去管后面的方案,也不去管那些 \(a_{1,i}\) 的相对顺序)。
转移时先求总方案数,即前面随便填(但要满足每个排列是一个一个填的),只要满足末尾有 \(m\)\(a_{1,i}\) 即可,计算这个的话就是:\(\frac {(\sum cnt_j)!}{\prod cnt_j!}\)
\(cnt_j\) 表示第 \(j\) 个排列 \(a_{1,i}\) 前面有多少个数。
假设有一个 \(j\) 可以转移到 \(i\),那么可以想到肯定是用 (总方案数) - $f_j \times $ (某些系数),即要去掉 \(a_{1,j}\)\(a_{1,i}\) 之前就出现了 \(m\) 次的情况。
这个系数的话就是 \(m! \times \frac{(\sum s_k)!}{\prod s_k!}\)
其中 \(m!\) 表示的是那 \(m\)\(a_{1,j}\) 的顺序要确定,然后 \(s_k\) 表示第 \(k\) 个排列的 \(a_{1,j}\)\(a_{1,i}\) (不包括两端) 之间的数的个数,\(m!\) 后面那个式子意义类似于总方案那个式子。
然后 \(j\) 能转移到 \(i\) 当且仅当在每个排列中 \(a_{1,j}\) 都在 \(a_{1,i}\) 前面。
我们可以在每个排列后面加一个 \(n+1\),这样 \(f_{n+1}\) 的意义其实就是答案了(因为我们不去考虑最后那 \(m\) 个相同数的顺序)。
这样求一次是 \(O(n^2m)\),因为我们需要枚举 \(i\),再枚举 \(j\),再对每个排列判断。

优化的话顺序枚举 \(l\),再从 \(l\) 开始枚举 \(r\),求出 \([l,r]\) 里的排列的答案。
考虑对每个 \(r\),用一个 vector 记录对于每一个 \(i\), 能转移到他的 \(j\),以及对应的系数,在最后统一转移即可。

注意到数据随机生成,所以一个排列里数字 \(x\)\(y\) 前的概率是 \(\frac{1}{2}\),即随着 \(r\) 的增大 \(j\) 能转移到 \(i\) 的概率越小。
那么在 \(r\) 右移时 \(j\) 能转移到 \(i\) 的总次数的期望就是 \(\frac{1}{2} + \frac{1}{4} + \frac{1}{8} +...= O(1)\)
也就是说对于每个 \(l\),总共的转移次数期望是 \(O(n^2)\) 的。

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

code

#include<bits/stdc++.h>
#define int long long 
#define PII pair<int,int>
#define fi first
#define se second 
using namespace std;
const int N=3e2+5,M=1e6+5,mod=998244353;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int m,n,a[N][N],inv[M],q[M],fact[M],f[N],ans;
int pos[N][N];  //pos[i][x] 表示排列 i 中 x 的位置 
PII g[N];  //表示算每个 i 的总方案的系数 
struct P{
	int j,t1,t2,t3; //分别表示 j,m!,Σs[k],Π(s[k]!) 
};
vector<P> v[N];  
signed main(){
	freopen("merge.in","r",stdin);
	freopen("merge.out","w",stdout);
	m=read(),n=read()+1;
	for(int i=1;i<=m;i++){
		for(int j=1;j<n;j++){
			a[i][j]=read();
			pos[i][a[i][j]]=j;
		}
		pos[i][n]=n;
		a[i][n]=n;  //在结尾加入 n+1 
	}
	
	inv[1]=1;
	for(int i=2;i<M;i++) inv[i]=(mod-mod/i)*inv[mod%i]%mod;
	fact[0]=1;
	for(int i=1;i<M;i++) fact[i]=fact[i-1]*i%mod;
	q[0]=1;
	for(int i=1;i<M;i++) q[i]=q[i-1]*inv[i]%mod;
	
	for(int l=1;l<=m;l++){
		for(int i=1;i<=n;i++) v[i].clear();
		for(int i=1;i<=n;i++){
			g[i].fi=i-1 , g[i].se=q[i-1]; 
			for(int j=1;j<i;j++){
				v[i].push_back({j , 1 , i-j-1 , q[i-j-1]});
			}
		} 
		for(int r=l;r<=m;r++){
			for(int i=1;i<=n;i++){
				f[i] = fact[ g[i].fi ] * g[i].se % mod;
				for(P x:v[i]){
					f[i] = (f[i] - f[x.j] * x.t1 % mod * fact[x.t2] % mod * x.t3 % mod + mod) % mod ;		
				}				
			}
			(ans += f[n]) %= mod;  //r=l 时答案肯定是 0
			for(int i=1;i<=n;i++){   //加入 r+1 排列,更新可以转移到 i 的 j。 
				int u=a[l][i];
				( g[i].fi += pos[r+1][u]-1 ) %= mod;
				( g[i].se *= q[pos[r+1][u]-1] ) %= mod;
				vector<P> tmp;
				for(P x:v[i]){
					int j=x.j,v=a[l][j];
					if(pos[r+1][v] < pos[r+1][u]){
						tmp.push_back({j , x.t1*(r+1-l+1)%mod , (x.t2+(pos[r+1][u]-pos[r+1][v]-1))%mod , x.t3*q[ pos[r+1][u]-pos[r+1][v]-1 ]%mod});
					}
				}
				swap(v[i],tmp);  //把 tmp 复制到 v[i] 
			}
		}
	}
	printf("%lld\n",ans);
	return 0;
}

89.[ARC114C] Sequence Scores

操作肯定是先搞点小的位置再搞定大的位置。

考虑在 \(A\) 的末尾 \(pos\) 位置加一个 \(x\) 会产生什么影响:

  1. 如果 \(x\) 是第一次出现,那显然 \(ans+1\)
  2. 如果 \(lst<i<pos\)\(a_i\)\(>x\)\(ans\) 是不变的。
  3. 如果 \(lst<i<pos\)\(a_i\) 有一个 \(<x\)\(ans+1\)

\(lst\) 表示上一个 \(x\) 出现的位置。

\(f_{i,x}\) 表示在第 \(i\) 个位置放 \(x\),在所有可能的情况中对答案产生的影响的和。
那么 \(f_{i,x} = m^{i-1} - \sum_{k<i} m^{k-1} \times (m-x)^{i-k-1}\)
预处理后面那个东西就可以 \(O(n^2)\) 了。

算答案时设 \(ans_i\) 表示前 \(i\) 个位置的答案,那么 \(ans_i=\sum_{x=1}^m ans_{i-1}+f_{i,x}\)

code

#include<bits/stdc++.h>
#define int long long 
using namespace std;
const int N=5e3+5,mod=998244353;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,m,g[N][N],f[N][N],ans[N];
int mi[N][N];
signed main(){
//	freopen(".in","r",stdin);
//	freopen(".out","w",stdout);
	n=read(),m=read();
	for(int i=0;i<=m;i++){
		mi[i][0]=1;
		for(int j=1;j<=n;j++) mi[i][j]=mi[i][j-1]*i%mod;
	}
	for(int i=2;i<=n;i++){
		for(int x=1;x<=m;x++){
			g[i][x] = (g[i-1][x]*(m-x)%mod + mi[m][i-2])%mod;
		}
	}
	for(int i=1;i<=n;i++){
		for(int x=1;x<=m;x++){
			f[i][x] = (mi[m][i-1] - g[i][x] + mod)%mod;
			(ans[i] += (ans[i-1]+f[i][x])%mod)%=mod;
		}
	}
	printf("%lld\n",ans[n]);
	return 0;
}

90.[ARC114D] Moving Pieces on Line

首先一个棋子不会走回头路,他选好终点后就直接到终点了。

首先区间取反,可以认为是区间异或,区间异或再转换为差分,改变判定条件:记 \(flag_i\) 表示 \(i\) 点左右两边的边颜色是否相同,那么一次匹配 \((a_i,b_i)\) (\(a_i\)为起点,\(b_i\)为终点),相当于把 \(flag_{a_i}\oplus 1,flag_{b_i}\oplus 1\)
最终要求:每个 \(flag_{t_i}=true\)

首先我们可以计算出哪些点要被操作奇数次,记它们为 \(c_1,c_2...,c_m\), 操作定义为选他为终点。
具体的计算方法就是,一开始先把 \(a_i\)\(t_i\)\(\oplus 1\),那么场上此时 \(flag=1\) 的点就是要被操作奇数次的。
也就是说并不是所有的 \(t_i\) 都要被操作奇数次,比如一个 \(t_i\) 上放了一个棋子时。
也不是所有不是 \(t_i\) 的都不能被操作奇数次,比如第一个样例。
再次注意,一次操作指的是以他为终点,而不是起点。

一次匹配的代价是 \(|b_i-a_i|\)
然后分类讨论。

  1. \(n=m\) 时,直接分别排序,两两匹配即可。
  2. \(n<m\) 时,显然无解。
  3. \(n>m\) 相当于分配多余的棋子去操作一些点偶数次,实际意义就是这两个棋子相遇,即代价是 \(|a_i-a_j|\),显然 \(|j-i|=1\) 即相邻时最优。
    所以考虑 DP,设 \(f_{i,j}\) 表示前 \(i\) 个棋子匹配前 \(j\)\(c\) ,且必须匹配完(即需要考虑多余棋子)的最小代价。
    转移你就按照第 \(i\) 个棋子是去匹配 \(c_j\) 还是 \(a_{i-1}\) 即可。

code

#include<bits/stdc++.h>
#define int long long 
using namespace std;
const int N=1e4+5;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,k,a[N],t[N],c[N],m,flag[N],f[5005][5005];
int dis[N],cnt;
int Dis(int x){
	return lower_bound(dis+1,dis+cnt+1,x)-dis;
}
signed main(){
	n=read(),k=read();
	for(int i=1;i<=n;i++) a[i]=read(),dis[++cnt]=a[i];
	for(int i=1;i<=k;i++) t[i]=read(),dis[++cnt]=t[i];
	sort(dis+1,dis+cnt+1);
	sort(a+1,a+n+1);
	cnt=unique(dis+1,dis+cnt+1)-dis-1;
	for(int i=1;i<=n;i++) a[i]=Dis(a[i]),flag[a[i]]^=1;
	for(int i=1;i<=k;i++) t[i]=Dis(t[i]),flag[t[i]]^=1;
	for(int i=1;i<=cnt;i++) if(flag[i]) c[++m]=i;
	if(n<m) puts("-1");
	else if(n==m){
		int ans=0;
		for(int i=1;i<=n;i++){
			ans+=abs(dis[c[i]]-dis[a[i]]);
		}
		printf("%lld\n",ans);
	} 
	else{
		memset(f,0x3f,sizeof f);
		f[0][0]=0; 
		for(int i=1;i<=n;i++){
			for(int j=0;j<=m;j++){
				if(i==1&&j>=1) f[i][j]=f[i-1][j-1]+abs(dis[c[j]] - dis[a[i]]);
				else if(i>=2&&j==0) f[i][j]= f[i-2][j]+abs(dis[a[i]] - dis[a[i-1]]);
				else if(i>=2&&j>=1) f[i][j]=min(f[i-1][j-1]+abs(dis[c[j]]-dis[a[i]]) , f[i-2][j]+abs(dis[a[i]] - dis[a[i-1]]));		
			}
		}
		printf("%lld\n",f[n][m]);
	}
	return 0;
}
posted @ 2024-12-31 08:01  Green&White  阅读(113)  评论(0)    收藏  举报