题解 DP专题汇总(15题)

DP专题

进度:15/15

完结撒花!!!

  • 早期作品,锅一大堆

  • 修改于:2022/5/12


洛谷博客传送门





Part 1 DP+树

T1 GCD Counting (CF1101D)

CF传送门

洛谷传送门

数据加强版(UOJ) 100w数据

数据加强版(洛谷) 200w数据

题意简述

给出一棵有n个节点的树,树有点权 \(a_i\) ,求树上最长的一条链满足:链上所有点的点权的 \(gcd>1\)

注意一个点也可以构成一条链.

数据范围

\(1 \leq n \leq 2 \times 10^5\)

\(1 \leq a_i \leq 2 \times 10^5\)

\(1 \leq u,v \leq 2 \times n\) , \(u \neq v\)


题解

思路

首先看清题意,会发现一条链上的点权 \(gcd>1\) 等价于 存在一个 \(k\) ,使对于任意链上的点i, \(k|a_i\)

于是乎就把难以处理的gcd转换成了简单的整除问题

从约数的角度考虑。假如我已经确定了一个 \(k\) ,那问题就变为了把树上所有权值能被k整除的点 点亮,再找最长的链

显然可以用树上dp轻松搞定

但约数个数明显也不少啊

所以不妨再退回来,一次解决所有k

再回到只观察一个点,发现它只能由与它不互质的儿子转移上来

那就只转移这些就行了

再回来考虑 \(k\)

如果 \(k\) 不是质数,即 \(k=x*y\)

显然 集合A{ \(i\) | \(k|a_i\) } \(\in\) 集合B{ \(i\) | \(x|a_i\) }

\(k\) 的答案当然没有 \(x\) 的答案大啦

这样就好办了,分解质因数

做法

先把每个节点的质因数处理出来

$ 2 \times 3 \times 5 \times 7 \times 11 \times 13 < 200000 < 2 \times 3 \times 5 \times 7 \times 11 \times 13 \times 17$

所以约数个数至多6个

\(f[N][7]\) 存储约数即可

\(dp[i][j]\) 表示以 \(i\) 为根,经过 \(i\) 的,都能被 \(j\) 整除的最长路径长度

转移时直接暴力枚举即可(因为至多也就6*6=36嘛)

注意这只是从下往上的一条路径,

所以还要边dp边更新答案

把约数个数看作常数就是 \(O(n)\)

注意事项

  • 建双边别忘了开两倍大小(也就只有我犯了这个错吧)

  • 预处理好像可以用筛加速

但是我懒所以直接打表(最大202ms)

不加速也可以吧4.5s耶

  • 别忘了初始化dp数组(全为1)

  • 别忘了特判 \(a_i=0\) 的情况,输出0

AC代码

#include<bits/stdc++.h>
using namespace std;
const int N=20000005;
int n,a[N],f[N][7],dp[N][7],ans=1;
int k[100]={0,2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97,101,103,107,
109,113,127,131,137,139,149,151,157,163,167,173,179,181,191,193,197,199,211,223,
227,229,233,239,241,251,257,263,269,271,277,281,283,293,307,311,313,317,331,337,
347,349,353,359,367,373,379,383,389,397,401,409,419,421,431,433,439,443};
//sqrt(200000)内的质数表,打表使我快乐yeah 
struct nod{
	int to,nxt;
}e[N*2];//双边! 
int head[N],cnt;
void add(int u,int v){
	e[++cnt].to=v;
	e[cnt].nxt=head[u];
	head[u]=cnt;
}
void dfs(int o,int fa){
	for(int i=head[o];i;i=e[i].nxt){
		int v=e[i].to;
		if(v==fa) continue;
		dfs(v,o);
		for(int j=1;j<=f[o][0];j++)
			for(int k=1;k<=f[v][0];k++)	
				if(f[o][j]==f[v][k])
					ans=max(ans,dp[o][j]+dp[v][k]),//先更新答案 
					dp[o][j]=max(dp[o][j],dp[v][k]+1);//再更新dp 
	}
}
int main(){
	scanf("%d",&n);
	bool flag=1;
	for(int i=1;i<=n;i++){
		scanf("%d",&a[i]);
		if(a[i]!=1) flag=0;//特判 
	}
	for(int i=1;i<=n-1;i++){
		int u,v; scanf("%d%d",&u,&v);
		add(u,v),add(v,u);
	}
	if(flag) {printf("0");return 0;}//特判 
	for(int i=1;i<=n;i++)
		for(int j=1;j<=86 && k[j]*k[j]<=a[i];j++)
			if(a[i]%k[j]==0){
				while(a[i]%k[j]==0) a[i]/=k[j];
				f[i][++f[i][0]]=k[j];
			}
	//判断每个数的能否被质数整除 
	for(int i=1;i<=n;i++)
		if(a[i]!=1)
			f[i][++f[i][0]]=a[i];
	//除完肯定只剩下sqrt(200000)外的质数了,直接搬下来 
	for(int i=1;i<=n;i++)
		for(int j=1;j<=f[i][0];j++)
			dp[i][j]=1;
	//初始化dp数组 
	dfs(1,0);
	printf("%d",ans);
}

时间测试

\(n=200000\) \(150ms\)

\(n=2000000\) \(1000ms\)


T2 You Are Given a Tree (CF1039D)

CF传送门

洛谷传送门

题意简述

有一棵 \(n\) 个节点的树

在树上选择一些长度为 \(k\) 的路径,使得这些路径覆盖的点互不重复

求对于 \(k\) (\(1 \leq k \leq n\)) ,最多能选择多少条路径

数据范围

\(1 \leq n \leq 10^5\)

\(1 \leq u \leq v \leq n\)


题解

思路

首先从特殊情况考虑

\(k=1 - n\) 想不到什么好方法一下处理,先来对单个的k处理

覆盖问题自然可以考虑贪心

一条链,必定可以找出一个点作为“根”(即 \(dep\) 最小的那个点)

对于一个节点 \(o\) ,想要覆盖它

如果有一根长度为 \(k\) 的链以 \(o\) 为根,那自然把它数进来最好

不然就会占用 \(o\) 上面的节点,显然不优

(那如果这样的链有很多条呢?

\(o\) 只能用一次啊!选哪种都是一样的)

所以对于一个o来说,记录转移它子树最长链与次长链的长度即可 \(O(n)\) 解决


一个 \(k\) 的情况搞定了,但 \(n\)\(k\) 就要 \(O(n^2)\)

7s都无济于事

所以不妨来观察一下答案

显然,\(ans \leq n/k\)

而且 \(ans\) 必然是单调不增

对任意一个 \(a\) 来说,前 \(a\)\(k\) ,答案种数不超过 \(a\) 种;后 \((n-a)\)\(k\) ,答案种数不会超过 \(n/a+1\)\((0 - n/a)\)

所以答案种数不会超过 \(a+n/a \geq 2 \times \sqrt n/a\) 种(当 \(a\)\(\sqrt n\) 时)

对前 \(\sqrt n\) 个来说,暴力枚举即可,反正也没多少个

对后 \(n-\sqrt n\) 个来说,就是枚举断点

\(\Rightarrow\) 二分查找

有多个断点?

\(\Rightarrow\) 分治+二分

\(O(nlogn \sqrt n)\)

做法

首先,dp部分

\(f[i]\) 表示包括 \(i\)节点在内的,子树下最长链长度

\(max1,max2\)表示不包括 \(i\) 节点在内的,子树下最长链长度

然后转移即可

如果找得到的话,注意 \(f[i]=0\) ,因为此节点不可再用了


分治+二分部分

\(l\)\(r\) 表示查找的区间

\(L\)\(R\)表示查找的值域

如果值域确定下来了表明这是个断点,更新答案

否则dp一遍 \(mid\) 的答案,再去分治

更新答案的方向应该不用管,是分治嘛,无论哪个方向都会更新到的

注意事项

  • 代码有点难以理解,慢慢想

  • max值的更新方式好像这样是最正确也是最易于理解的

我原来写的不知为何错了

AC代码

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n,k,sum,flag;
int f[N],ans[N];
struct abc{
	int to,nxt;
}e[2*N];
int head[N],cnt;
void add(int u,int v){
	e[++cnt].to=v;
	e[cnt].nxt=head[u];
	head[u]=cnt;
}
void dfs(int o,int fa){
	int max1=0,max2=0;
	for(int i=head[o];i;i=e[i].nxt){
		int v=e[i].to;
		if(v==fa) continue;
		dfs(v,o);
		if(f[v]>max1) max2=max1,max1=f[v];
		else if(f[v]>max2) max2=f[v];
		//这两行是更新max值 
	}
	if(max1+max2+1>=k) sum++,f[o]=0;//如果找得到 
	else f[o]=max1+1;//找不到就更新f[o] 
}
void solve(int l,int r,int L,int R){
	if(l>r || L>R) return;
	if(L==R){
		for(int i=l;i<=r;i++) ans[i]=L;
		return;//记得啊 
	}
	int mid=(l+r)>>1;
	k=mid,sum=0;
	dfs(1,0);
	//跑一遍mid的答案
	ans[mid]=sum;//别忘了更新ans[mid](因为下面不会枚举到这个点了) 
	solve(l,mid-1,sum,R);
	solve(mid+1,r,L,sum);
}
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n-1;i++){
		int u,v;
		scanf("%d%d",&u,&v);
		add(u,v),add(v,u);
	}
	for(int i=1;i<=sqrt(n);i++){
		k=i;
		sum=0;//sum为单次答案计数 
		dfs(1,0);
		ans[i]=sum;
	}
	solve(sqrt(n)+1,n,0,sqrt(n)); 
	for(int i=1;i<=n;i++) printf("%d\n",ans[i]);
}


T3 Vladislav and a Great Legend (CF1097G)

人生中第一道黑题

对我来说有点过于复杂与难以理解了,可能理解得不是很透彻

花了我一整天,我太弱了

CF传送门

洛谷传送门

题意简述

给你一棵有 \(n\) 个节点的树 \(T\)\(n\) 个节点编号为 \(1\)\(n\)

对于 \(T\) 中每个非空的顶点的集合 \(X\) ,令 \(f(X)\) 为包含 \(X\) 中每个节点的最小连通子树的最小边数,即虚树的大小。

再给你一个整数 \(k\) 。你需要计算对于每一个顶点的集合 \(X\)\((f(X))^{k}\) 之和,即:

\(\sum\limits_{X \subseteq\{1,2, \ldots, n\}, X \neq \varnothing}(f(X))^{k}\)

数据范围

\(2 \leq n \leq 10^5\)

\(1 \leq u \leq v \leq n\)

$ 1 \leq k \leq 200$


题解

前铺知识

树形依赖背包

复杂版

题意:在一棵有根树上,每个节点都挂着一个物件有 \(w_i\) 的价值和 \(c_i\) 的体积

选出一个包含根节点的连通块,使得体积之和不超过背包大小 \(k\) ,价值之和最大。

简化版

题意:给定一棵有 \(n\) 个节点的点权树,要求你从中选出 \(m\) 个节点,使得这些选出的节点的点权和最大。

即每个节点的体积均为1

--下面是解法--

我们可以仿照背包的思想

只不过在这里不是一个整体的背包,而是每个节点上都有一个背包

转移过程相当于把每个子节点当做一个物品,一个一个加进去

其他都与背包一样的

这道题用到的是树形依赖背包的思想


第二类斯特林数

第二类斯特林数

定义:第二类斯特林数 \(S(n,m)\) 表示的是把 \(n\)不同的小球放在 \(m\)相同的盒子里方案数。

求法:\(S(n, m)=S(n-1, m-1)+m S(n-1, m)\)

考虑有 \(n-1\) 个小球,现在再放一个小球

如果新开一个,那就是 \(S(n-1, m-1) \Rightarrow S(n, m)\)

如果放到已有的盒子里,有 \(m\) 种选择,即 \(m S(n-1, m) \Rightarrow S(n, m)\)

性质:\(n^{k}=\sum\limits_{i=0}^{k} S(k, i) \times i ! \times C(n, i)\)

\(n^k\) 相当于把 \(n\) 个不同的小球放到 \(k\) 个不同盒子里,每个小球都有 \(k\) 种选择

但这样放会有一些盒子是空的,于是用 \(i\) 枚举有多少个盒子是非空的

确定到底是哪 \(i\) 个盒子是非空的,要乘上 \(C(n, i)\)

把k个不同的小球放入 \(i\) 个相同的盒子里要 \(S(k, i)\)

但这里盒子是不同的,因此再乘上 \(i!\)


思路

首先,看到要算的东西里有个大大的 \(k\) 次方

而且 \(k\) 还很小

可以考虑用第二类斯特林数的那个性质转化

\(n^{k}=\sum\limits_{i=0}^{k} S(k, i) \times i ! \times C(n, i)\)

于是

\(\sum(f(X))^{k}=\sum \sum\limits_{i=0}^{k} S(k, i) \times i ! \times C(f(X), i)\)

交换和号

\(=\sum\limits_{i=0}^{k} S(k, i) \times i ! \times \sum C(f(X), i)\)

噢!

发现其实左边的东西都是可以很快算出来的

因此实际要算的是

\(\sum C(f(X), i)\)


于是我们把难以处理的幂换成了组合数

但这样直接去考虑好像和幂没有什么区别

不妨从整体来考虑

考虑它的实际意义,其实就是 对于每个生成树,从 \(f(X)\) 选出 \(i\) 条边涂色的方案数之和

选出 \(i\) 条边,像不像前面提到的树形依赖背包?

一个一个点来转移

解决了?

做法

NO

这题真正的难点不在于思路,在于接下来的实现

仿照树形依赖背包,设 \(dp[i][j]\) 表示以 \(i\) 为根节点的子树中,所有 点集形成的生成树 中涂j条边的方案数

请注意是 所有点集形成的生成树 (即要求的)而不是 所有生成树

请注意是所有而不是经过 \(i\) 节点的

我们是从边的角度来考虑的

也就是说,存在一种特殊情况

只连一棵子树(因为连了多棵子树就合法了),不选根节点而选它连向子节点的边

会被转移,但不会被计入答案

\(why\)

这种情况显然是不合法的,又不选 \(o\) ,却选了这条边

但是我们稍后在上面转移时需要它,因为是虚树,在上面就不需要选中这个 \(o\) 点了

(这大概是最难理解的部分)

讲个定义都已经把要点讲了一半了

来个状态转移方程:

\(dp[o][k]=dp[o][i] \times dp[v][k-i] (0 \leq i \leq k)\)

很好理解,就是在前面的点里先选 \(i\) 条涂色,再在这颗子树里选 \(k-i\) 条涂色

当然为了好写实际上是这样写的

\(dp[o][i+j]=dp[o][i] \times dp[v][j] (0 \leq i \leq k, 0 \leq j \leq k-i)\)

理解到这可能又会冒出一个问题

这是按边来转移的,所以转移到父节点时,又不 \(+1\) ,岂不是漏了 \(o\)\(fa(o)\) 之间的边了吗?

当然其实可以连,也可以不连

所以dp完后还要把 \(dp[o][i]\) 变为 \(dp[o][i]+dp[o][i-1]\)

最后一个问题

如何初始化?

\(dp[o][0]=2\)

因为选了 \(o\) ,还是不选 \(o\) ,都是 \(0\) 条边

所以有两种情况

于是有一个东西不能转移

就是初始化时 \(o\) 不选的情况,总不能转移个空集吧

所以还得减 \(1\)

由树形依赖背包经典结论可知是 \(O(nk)\) 的。

注意事项

· 很难理解,请结合题解与代码,仔细思考,不要着急

AC代码

#include<bits/stdc++.h>
using namespace std;
const long long N=1e5+5,mod=1e9+7;
struct nod{
	long long to,nxt;
}e[2*N];
long long head[N],cnt;
void add(long long u,long long v){
	e[++cnt].to=v;
	e[cnt].nxt=head[u];
	head[u]=cnt;
}
long long n,k;
long long dp[N][205],f[N],size[N],ans[N],S[205][205];
void dfs(long long o,long long fa){
	size[o]=1,dp[o][0]=2;//初始化,dp[o][0]:选o还是不选o都是0条边(后面会处理这种情况) 
	for(long long i=head[o];i;i=e[i].nxt){
		long long v=e[i].to;
		if(v==fa) continue;
		dfs(v,o);
		for(long long i=0;i<=k;i++) f[i]=0;
		for(long long p=0;p<=min(k,size[o]);p++)
			for(long long q=0;q<=min(k-p,size[v]);q++)
				f[p+q]=(f[p+q]+(dp[o][p]*dp[v][q])%mod)%mod;
		//类似背包,f为临时数组避免混用 
		for(long long j=0;j<=k;j++) dp[o][j]=f[j];
		//把答案从f转移到dp[o] 
		for(long long j=0;j<=k;j++) ans[j]=(ans[j]+mod-dp[v][j])%mod;
		//这一步其实可以放到后面,ans要减掉不经过o的情况,这里先减了(为了好写) 
		size[o]+=size[v];
	}
	for(long long i=0;i<=k;i++) ans[i]=(ans[i]+dp[o][i])%mod;
	//累加答案,前面已经减过多余的答案了 
	for(long long i=k;i>=1;i--) dp[o][i]=(dp[o][i]+dp[o][i-1])%mod;
	//考虑o的父亲,那么显然i条会变成i+1条,于是dp[o]整体往右移一格 
	dp[o][1]=(dp[o][1]+mod-1)%mod;
	//减掉最前面不选o的情况(无法转移至父节点) 
}
int main(){
	scanf("%lld%lld",&n,&k);
	for(long long i=1;i<=n-1;i++){
		long long u,v;
		scanf("%lld%lld",&u,&v);
		add(u,v),add(v,u); 
	}
	dfs(1,0);
	S[0][0]=1;
	for(long long i=1;i<=k;i++)
		for(long long j=1;j<=i;j++)
			S[i][j]=(S[i-1][j-1]+(S[i-1][j]*j)%mod)%mod;
	//计算第二类斯特林数 
	long long tmp=1,sum=0;
	for(long long i=1;i<=k;i++) tmp=(tmp*i)%mod,sum=(sum+(tmp*S[k][i])%mod*ans[i])%mod; 
	//计算答案 
	printf("%lld",sum);
}


T4 Uniformly Branched Trees (CF724F)

人生中第二道黑题

又一道计数题,但相比T3要好理解得多

细节还是一如既往的多

CF传送门

洛谷传送门

题意简述

求有多少种不同构(即交换节点不能使两棵树完全相同)的 \(n\) 个点构成的树,满足除了子节点(度数为1的结点)外,其余结点的度数均为 \(d\) 。答案对质数 \(mod\) 取模。

数据范围

\(1 \leq n \leq 10^3\)

\(2 \leq d \leq 10\)

\(10^8 \leq mod \leq 10^9\)


题解

思路

刚拿到这个题目,显然最大的障碍就是如何处理同构(或者说如何判重)

对于一颗无根树来说判重显然是不现实的

自然就找一个根咯

这个根可不能随便乱找,要有一些有用的性质才行

比如说重心

这样一来子树的大小就不会超过 \(\frac {n}{2}\)


那接下来怎么做呢?

当然不是树形dp因为连一颗固定的树都没有

但还是得dp

关联的状态是什么呢?

显然有 节点个数子树大小的上限

还有一个很妙的,根节点的子树个数

这样就方便转移了

于是用 \(f[i][j][k]\) 表示 有i个节点根节点有j个子节点子树大小均不超过k的方案总数

\(f[i][j][k]\) 中有两种方案

一种是所有子树 (指根节点的子树,下同) 大小连等于 \(k\) 都没有,全都比 \(k\)

直接转移就行了

\(f[i][j][k]+=f[i][j][k-1]\)

第二种则是有子树大小为 \(k\)

假设有 \(t\) 棵子树大小为 \(k\)

那去掉这 \(t\) 棵,其实就是 \(f[i-t*k][j-t][k-1]\)

但从 \(f[i-t*k][j-t][k-1]\) 加上这些子树的时候,其实又有很多种方案

每颗子树有 \(f[t][d-1][k-1]\)

其实就是t棵子树中,每颗子树都可不分顺序地可重复地\(f[t][d-1][k-1]\) 种方案,即

\(C^{t}_{f[t][d-1][k-1]+t-1}\)

所以状态转移方程就出来了(别忘了前面的)

\(f[i][j][k]+=f[i][j][k-1]\)

\(f[i][j][k]+=f[i-t*k][j-t][k-1] C^{t}_{f[t][d-1][k-1]+t-1}\)


最后,不要得意忘形o

还有双重心要考虑(即n为偶数)

可以想象一下,就是一座桥连接着两颗全等(对称)的树,桥的两头就是两个重心

大概是这样的

\    /
/\__/\
 /  \

怎么画得跟个人似的[doge]

两边的方案自然是完全一样的

\(f[n/2][d-1][n/2]\)

这时候就会发现一种重复了

比如有两个方案 \(a\)\(b\)

\(a\)\(b\) 和 左 \(b\)\(a\) ,是没有本质区别的(翻转一下就一样了)

当然 \(a!=b\) 不然是不会计算两次的

要是这样的话,直接减去 \(C^{2}_{f[n/2][d-1][n/2]}\)不就行啦~

做法

这个就很简单了

直接按 \(i\)\(j\)\(k\)\(t\) 枚举转移即可

注意初始化还有枚举的区间(写在下面了QwQ)

组合数运算要除法,要用到逆元

只是阶乘的逆元,看到质数用费马小定理暴力处理就好了(反正不超过 \(d\)

AC代码

#include<bits/stdc++.h>
using namespace std;
const long long N=1005;
long long n,d,mod;
long long f[N][11][N/2];
long long inv[11];
long long ksm(long long a,long long x){
	long long ans=1;
	while(x){
		if(x%2==1) ans=ans*a%mod;
		a=a*a%mod;
		x/=2;
	}
	return ans%mod;
}
void pre(){
	inv[0]=1;
	long long tmp=1;
	for(long long i=1;i<=d;i++){
		tmp=tmp*i;
		inv[i]=ksm(tmp,mod-2);
	}
}
long long C(long long m,long long n){
	long long ans=1;
	for(long long i=m;i>=m-n+1;i--) ans=(ans*i)%mod;
	ans=(ans*inv[n])%mod;
	return ans;
}
int main(){
	cin>>n>>d>>mod;
	if(n<=2) {cout<<1;return 0;}
	pre();
	for(long long i=0;i<=n/2;i++) f[1][0][i]=1;//初始化,只有一个节点就是1种情况 
	for(long long i=2;i<=n;i++){//枚举i 
		for(long long j=1;j<=min(i-1,d);j++){//枚举j
		//j<=i-1(每棵子树只有一个节点时最多也只有i-1棵子树)
		//j<=d(根节点至多有d棵子树) 
			for(long long k=1;k<=n/2;k++){
				f[i][j][k]=f[i][j][k-1];
				for(long long t=1;t<=min((i-1)/k,j);t++){
					//i-t*k>0 => t<=(i-1)/k
					//t<=j(放入的新子树数量t不会超过有的子树数量j) 
					long long tmp=(k==1)?0:d-1;
					//如果k=1,即只有一个根节点(无子树),需要特判 
					f[i][j][k]=(f[i][j][k]+f[i-t*k][j-t][k-1]*C(f[k][tmp][k-1]+t-1,t)%mod)%mod;
				}
			} 
		}
	}
	long long ans=f[n][d][n/2];
	if(n%2==0) ans=(ans+mod-C(f[n/2][d-1][n/2],2))%mod;//双重心特判 
	cout<<ans;
}

Part 2 DP+计数

T5 Multiplicity (CF1061C)

很基础的一个dp(为什么是紫题

CF传送门

洛谷传送门

题意简述

从序列 \({a_1,a_2,...,a_n} (1 \leq ai \leq 1e6)\) 中选出非空子序列 \({b_1,b_2,...,b_k}\) ,一个子序列合法需要满足,\(∀ i∈[1, k], i∣b_i\)
。求有多少互不相等的合法子序列,答案对
\(10^9+7\)取模。

注意:序列 \({1,1}\)\(2\)种选法得到子序列 \(1\) ,但 \(1\) 的来源不同,认为这两个子序列不相等。

数据范围

\(1 \leq n \leq 10^5\)


题解

思路

显然是dp

发现条件是跟选出来的数列有关的

所以状态就要设计一个 选出来的数列长度

所以设 \(f[i][j]\) 表示从 \(a\) 数列前 \(i\) 个数中选出长度恰好为 \(j\)\(b\) 数列个数

考虑从 \(f[i-1][]\) 转移到 \(f[i][]\)

可以不选第 \(i\) 个,所以 \(f[i][j]+=f[i-1][j]\)

也可以选,这时候就要求 \(j|a[j]\)

所以状态转移方程

\(f[i][j]+=f[i-1][j]\)

\(f[i][j]+=f[i-1][j-1](j|a[j])\)

答案就是 \(\sum\limits_{i=1}^n f[n][i]\)

做法

\(n\)\(10^5\) 级别的耶

所以要把 \(i\) 滚动掉

太暴力 \(n^2\) 肯定不行的

所以就只转移 \(a[j]\) 的因子就行了

注意要从大到小转移o(不然会影响到)

时间复杂度 \(O(n\sqrt{a_i})\)

注意事项

  • 别忘了取模

AC代码

#include<bits/stdc++.h>
using namespace std;
const int N=100010;
int mod=1e9+7;
int n,f[N],ans=0;
int main(){
	scanf("%d",&n);
	bool id=0;
	f[0]=1;
	for(int i=1;i<=n;i++){
		int a;
		scanf("%d",&a);
		for(int j=max(a/n,1);j<=sqrt(a);j++){
			if(a/j>n) continue;
			if(a%j==0) f[a/j]=(f[a/j]+f[a/j-1])%mod;
		}//枚举大于sqrt(n)的因数(从大到小) 
		for(int j=min(n,(int)sqrt(a));j>=1;j--){
			if(a%j==0 && j*j!=a) f[j]=(f[j]+f[j-1])%mod;
		}//枚举小于sqrt(n)的因数(从大到小) 
	}
	for(int i=1;i<=n;i++) ans=(ans+f[i])%mod;//计算答案 
	printf("%d",ans);
}

T6 Maximum Element (CF886E)

又一道超水黑题

CF传送门

洛谷传送门

题意简述

从前有一个叫 \(\text{Petya}\) 的神仙,嫌自己的序列求 \(\max\) 太慢了,于是将序列求 \(\max\) 的代码改成了下面这个样子:

(太长了缩了一下QwQ)

int fast_max(int n,int a[]){
    int ans=0,offset=0;
    for(int i=0;i<n;++i)
        if(ans<a[i]) ans=a[i],offset=0;
        else{
            offset++;
            if(offset==k)return ans;
        }
    return ans;
}

废话不多说,求有多少长度为 \(n\) 的排列,这个函数会返回错误的结果(即返回值不是 \(n\))。答案对 \(1e9+7\) 取模

数据范围

\(1 \leq n,k \leq 10^6\)

不保证 \(n > k\)


题解

思路

直接考虑出错的情况太难啦

不妨从反面考虑,正确的情况有多少

其实就是函数在 \(n\) 之前都没有退出(只要到了 \(n\),返回值一定是正确的)

这样就可以枚举 \(n\) 的位置来计算了

还差一个问题,选出一些数放到 \(n\) 的前面,怎么计算不退出的情况数呢?

(这个问题只和元素间的相对大小有关,不妨把它们归结到 \(i\) 的排列上来(是一样的)(如果你看不懂上面这段话请当我什么都没说)


总而言之,是个很明显的 dp 了

考虑 \(dp[i]\) ,表示长为 \(i\) 的排列中函数能够运行完(即中途没有退出) 的方案总数

考虑最大值 \(\max\) (其实就是 \(i\) 但为了不混淆)的位置。显然如果 \(\max\)\([1,i-k]\) 的话,最后 \(k\) 个数肯定都比 \(\max\) 要小,那肯定就退出了(在最后一个退出也算退出)

反之,只要 \(\max\)\([i-k+1,i]\) 中,就只需要 \(\max\) 的前面没有退出就一定不会退出

所以用 \(j\) 来枚举 \(\max\) 的位置

\[\sum\limits_{j=i-k+1}^i dp[j-1] \]

别忘了要先从i-1个数中选出j-1个数排到max前面 \(C^{j-1}_{i-1}\)

max后面的数是可以随便放的 \(A^{i-j}_{i-j}=(i-j)!\)

\[dp[i]=\sum\limits_{j=i-k+1}^i dp[j-1]C^{j-1}_{i-1} (i-j)! \]

答案计算就很简单了

\[ans=n!-\sum\limits_{i=1}^n dp[i-1]C^{n-i}_{n-1}(n-i)! \]

很遗憾 \(O(n^2)\) 过不去


我们来把这个式子打开优化

\(dp[i]\)

$=\sum\limits_{j=i-k+1}^i dp[j-1] C^{j-1}_{i-1} (i-j)! $

\(=\sum\limits_{j=i-k+1}^i dp[j-1] \frac{(i-1)!}{(j-1)!(i-j)!}(i-j)!\)

\(=\sum\limits_{j=i-k+1}^i dp[j-1] \frac{(i-1)!}{(j-1)!}\)

\(=(i-1) !\sum\limits_{j=i-k+1}^i \frac{dp[j-1]}{(j-1)!}\)

\(=(i-1) !\sum\limits_{j=i-k}^{i-1} \frac{dp[j]}{j!}\)

很惊奇地发现 \(j\) 可以单独拎出来!

于是前缀和优化

\(O(n)\)

当然ans的计算也可以这样优化(不过没什么必要只是为了好写)

\(ans\)

\(=n!-\sum\limits_{i=1}^n dp[i-1]C^{n-i}_{n-1}(n-i)!\)

\(=n!-\sum\limits_{i=1}^n dp[i-1]\frac{(n-1)!}{(i-1)!}\)

做法

预处理阶乘逆元什么的

已经说得很清楚了吧?

注意事项

  • 依旧是开long long

  • 记得特判 \(n<=k\)

  • 注意初始化和特判

AC代码

#include<bits/stdc++.h>
using namespace std;
const long long N=1000005;
long long mod=1e9+7;
long long f[N],s[N],ans=0;
long long mul[N],inv[N];
long long ksm(long long a,long long x){
	long long tot=1;
	while(x){
		if(x&1) tot=tot*a%mod;
		a=a*a%mod,x>>=1;
	}
	return tot;
}
int main(){
	long long n,k; 
	scanf("%lld%lld",&n,&k);
	if(n<=k) {printf("0");return 0;}//特判 
	mul[1]=1;
	for(long long i=2;i<=n;i++) mul[i]=mul[i-1]*i%mod;
	inv[n]=ksm(mul[n],mod-2);
	for(long long i=n-1;i>=1;i--) inv[i]=inv[i+1]*(i+1)%mod;//快速处理逆元 
	
	for(long long i=1;i<=k;i++) f[i]=mul[i],s[i]=(s[i-1]+f[i]*inv[i]%mod)%mod;
	//当i<=k时,所有情况都不会退出 
	for(long long i=k+1;i<=n;i++){
		f[i]=mul[i-1]*((s[i-1]+mod-s[i-k-1])%mod)%mod;//dp 
		s[i]=(s[i-1]+f[i]*inv[i]%mod)%mod;//(f[i]/i)前缀和 
	}
	
	ans=mul[n-1];//n=1时,就是(n-1)!种,不可不计算
	for(long long i=2;i<=n;i++)
		ans=(ans+f[i-1]*mul[n-1]%mod*inv[i-1]%mod)%mod;//计算答案 
	printf("%lld",(mul[n]+mod-ans)%mod);//取补集 
}

T7 The Top Scorer (CF1096E)

CF传送门

洛谷传送门

题意简述

小明在打比赛,包括小明自己共有 \(p\) 名选手参赛,每个人的得分是一个非负整数。最后的冠军是得分最高的人,如果得分最高的人有多个,就等概率从这些人中选一个当冠军。

现在小明已知自己的得分大于等于 \(r\) ,所有选手的得分和为 \(s\) 。所有人的得分情况随机。求小明获胜的概率,结果对 \(998244353\) 取模。

数据范围

\(1 \leq p \leq 100\)

\(1 \leq r,s \leq 5000\)


题解

思路

首先来暴力dp

涉及到最大值,因此可以让所有人都不超过某个值

\(dp[s][p][m]\) 表示 共有 \(p\)总共 \(s\)所有人的分都不超过 \(m\)的方案数

状态转移方程很好写了,考虑新加入的是得分为i的选手

\(dp[s][p][m]=\sum\limits_{i=0}^m dp[s-i][p-1][m]\)

怎么计算答案呢?

考虑到有多个人并列第一的情况,不妨枚举q人并列第一并且小明得t分,计算情况总数

当然这q个人里必须包含小明

情况总数 \(tot=\sum\limits_{t=r}^s \sum\limits_{q=1}^p \frac{1}{q} C^{q-1}_{n-1} dp[s-q*t][n-q][t-1]\)

别忘了答案是求概率,还得除以总数


总数怎么计算呢?

其实就是个数学问题,插板法

假设每个人的分数为 \(a_i(a_i \geq 0)\),小明分数为 \(k(k \geq r)\)

那么就是求 \(a_1+a_2+...+a_{p-1}+k=s\) 的解集个数

转化成正整数

\((a_1+1)+(a_2+1)+...+(a_{p-1}+1)+[k-(r-1)]=s+(p-1)-(r-1)\)

即求 \(b_1+b_2+...+b_p=s+p-r (b_i \geq 1)\) 的解集个数

就是插板法,在 \(s+p-r\)\(1\) 间插入 \(p-1\)\(+\) 号,分成 \(p\) 个数

即共 \(C_{s+p-r-1}^{p-1}\) 种方案

\(ans= \frac{tot} {C^{s+p-r-1}_{p-1}}\)

写了这么多,累死我了 该出答案了吧?

木有

会发现这是 \(O(rsp)\) 的,3s根本跑不过去


那么接下来的事情就与dp无关了

看看 \(ans\) 那个式子,会发现其实我们只会用到 \(O(sp)\) 个数

所以能不能不经过递推,直接把单个dp算出来呢?

可以。

\(dp[s][p][m]\) 其实就是把 \(s\) 个球投进 \(p\) 个篮子里,使得每个篮子里的球都不超过 \(m\) 个的方案数

怎么去算呢?

直接算是不好操作的,“不超过 \(m\) 个” 不好处理

所以不妨来考虑容斥原理

先随便投,\(C^{s+p-1}_{p-1}\)

然后来考虑至少有 \(i\) 个篮子里超过 \(m\) 个球的情况数

首先要选出这 \(i\) 个篮子,\(C^i_p\)

这时候又可以来用插板法了,用 \(a\) 来表示这 \(i\)个选出的篮子,用 \(b\) 表示剩下的

\(a_1+a_2+...+a_i+b_1+b_2+..+b_{p-i}=s (a_i > m,b_i \geq 0)\)

\((a_1-m)+(a_2-m)+...+(a_i-m)+(b_1+1)+(b_2+1)+..+(b_{p-i}+1)\)

\(=s-im+(p-i)\)

\(=s-i(m+1)+p\)

\(S_i=C^{p-1}_{s+p-i(m+1)-1}\)

刚开始选出来的 \(C^i_p\) 其实就是 \(S_0\)

最后来容斥,

\(dp[s][p][m]\)

\(=S_0-S_1+S_2-S_3+...+(-1)^mS_m\)

$ =\sum\limits_{i=0}^m (-1)^iS_i$

\(S_i\) 替换掉

\(dp[s][p][m]=\sum\limits_{i=0}^m (-1)^i C^i_p C^{p-1}_{s+p-i(m+1)-1}\)

还有ans的算法

\(tot=\sum\limits_{t=r}^s \sum\limits_{q=1}^p \frac{1}{q} C^{q-1}_{n-1} dp[s-q*t][n-q][t-1]\)

\(ans= \frac{tot} {C^{s+p-r-1}_{p-1}}\)

时间复杂度最大 \(O(p^2s)\)

做法

对于每一个要求的dp值,按照公式计算即可

组合数逆元啥的很基础了

还需要解释吗?

注意事项

  • 注意特判,尤其注意 \(\leq 0\)

  • long long...

AC代码

#include<bits/stdc++.h>
using namespace std;
const long long P=110,N=5005;
long long mod=998244353;
long long mul[P+N],inv[P+N];
long long p,s,r,ans=0;

long long C(long long m,long long n){
	if(m<0 || n<0 || m-n<0) return 0;//必要的,因为会有负数 
	if(m*n==0) return 1;//特判 
	return mul[m]*inv[n]%mod*inv[m-n]%mod;
}
long long ksm(long long a,long long x){
	long long sum=1;
	while(x){
		if(x&1) sum=sum*a%mod;
		a=a*a%mod,x>>=1;
	}
	return sum;
}
long long dp(long long s,long long p,long long m){
	if(s==0 && p==0) return 1;//奇怪的特判 
	long long tot=0;
	for(long long i=0;i<=p;i++){
		long long tmp=C(p,i)*C(s+p-1-i*(m+1),p-1)%mod;
		tot= (i%2)?(tot-tmp+mod)%mod:(tot+tmp)%mod;
	}
	return tot;
}
int main(){
	scanf("%lld%lld%lld",&p,&s,&r);
	mul[0]=inv[0]=1;
	for(long long i=1;i<=p+s;i++) mul[i]=mul[i-1]*i%mod;
	inv[p+s]=ksm(mul[p+s],mod-2);
	for(long long i=p+s-1;i>=1;i--) inv[i]=inv[i+1]*(i+1)%mod;
	//预处理阶乘和逆元
	for(long long t=r;t<=s;t++)
		for(long long q=1;q<=p;q++)
			ans=(ans+C(p-1,q-1)*ksm(q,mod-2)%mod*dp(s-q*t,p-q,t-1)%mod)%mod;
	//公式计算答案
	ans=(ans*ksm(C(s-r+p-1,p-1),mod-2))%mod;//最后要除以总数 
	printf("%lld",ans);
}

Part 3 DP+状压


T8 Easy Problem (CF1096D)

全村最水的题

CF传送门

洛谷传送门

题意简述

给你一个长为 \(n\) 的字符串以及 \(a_1...n\) ,删去第 \(i\) 个字符的代价为 \(a_i\) ,你可以删去一些字符,要求使得剩下的串中不含子序列 "hard",求最小代价。
(子序列不需要连续)

数据范围

\(1 \leq n \leq 10^5\)

\(1 \leq a_i \leq 998244353\)


题解

思路

首先,显然答案只跟 \(h a r d\) 有关

其它字符就不用删也不用管了

接下来显然考虑dp

既然是子序列,不要求连续,就只跟相对位置有关

设想扫一遍,扫的过程中,hard是由har加上d得到的,而har是由ha加上r得到的

那不妨就以已经有了hard中的前几个字符为状态吧

显然是无后效性的

被动转移即可

做法

\(f[i][j]\) 表示前 \(i\) 个字符中,刚好凑成 \(hard\) 中前 \(j\) 个字符最小的代价

\(i\) 可以滚动掉

当前为h:\(f[0]->f[1], f[0]->f[0]+w[i]\) (要想保持f[0],必须删掉)

当前为a:\(f[1]->f[2], f[1]->f[1]+w[i]\)

当前为r:\(f[2]->f[3], f[2]->f[2]+w[i]\)

当前为d:\(f[3]->f[4], f[3]->f[3]+w[i]\)

\(O(n)\)

注意事项

  • 答案是min(f[0],f[1],f[2],f[3])

  • 开long long

AC代码

#include<bits/stdc++.h>
using namespace std;
const long long N=100005;
long long n,f[5],w[N];
char s[N];
int main(){
	scanf("%lld\n",&n);
	for(long long i=1;i<=n;i++) scanf("%c",&s[i]);
	for(long long i=1;i<=n;i++) scanf("%d",&w[i]);
	for(long long i=1;i<=n;i++)
		switch(s[i]){
			case 'h':f[1]=min(f[0],f[1]),f[0]+=w[i];break;
			case 'a':f[2]=min(f[1],f[2]),f[1]+=w[i];break;
			case 'r':f[3]=min(f[2],f[3]),f[2]+=w[i];break;
			case 'd':f[4]=min(f[3],f[4]),f[3]+=w[i];break;
		}
	printf("%lld",min(f[0],min(f[1],min(f[2],f[3]))));
}

T9 Kuro and Topological Parity (CF979E)

CF传送门

洛谷传送门

题意简述

给定 \(n\) 个点,每个点有黑白两种颜色(如果没有颜色,那么你可以把它任意涂成黑色或白色),同时你可以在这个图上任意加入一些边(当然不能加入重边或自环),要求加入的边必须从编号小的点指向编号大的点

我们称一条好的路径为经过的点为黑白相间的路径,如果一个图好的路径的总数 % $ 2= p $ ,那么我们称这个图为好的图,现在给定你 \(n\) 个点的情况,求这 \(n\) 个点能组成的好的图的个数,答案取模 \(1e9+7\)

数据范围

\(1 \leq n \leq 50\) $ (ex: 1 \leq n \leq 1e6) $

\(0 \leq p \leq 1\)


题解

这是 O(n) 的题解

这题在理解上有很多岔路,所以我会标出容易误解的地方

为了方便,“好的路径”基本都会用“路径”来代替

题意

解释一下,黑白相间指的是黑连向白,白连向黑

比如 \(1 \rightarrow 0 \rightarrow 1\)\(0 \rightarrow 1 \rightarrow 0 \rightarrow 1\)\(1\)

  • (一个也算!)

思路

首先第一眼,是跟奇偶性相关的计数问题

所以重心就落在了奇偶性上

然后发现加入的边必须从编号小的点指向编号大的点

显然是无环的,而且拓扑序就是顺序

直接dp好了


然而点也要选,边也要选,可能性太多了qwq

那我们先不要管点,只考虑连边

既然是按点dp,不妨来设想加入一个点,让前面的点向它连边

可是随机性太大了啊,所以要加状态来限制

首先能想到的就是颜色,要么是黑,要么是白

但这还不够。因为我们想统计的是路径数的奇偶性,而黑或白根本没有体现

所以还要再加上两种状态:连到这个点的路径总条数,要么是奇,要么是偶

综合起来,就是四种状态

\(ob\)(奇黑)\(ow\)(奇白)\(eb\)(偶黑)\(ew\)(偶白)

  • 这四个是状态,不是点的属性!不是把点分为这四类,而是统计一个点有这些状态时的方案数

  • 奇偶性指的是好的路径的总条数,而不是长度!

  • 一个点不是只能被一个前面的路径连上,可以任意连!(甚至可以不连)而这些都加进方案数里,我们要做的就是区分这些方案的奇偶性

那我们就来区分这些方案的奇偶性吧

假设我新的 \(i\) 号点是白色的,它已经连向了一些点了(或者没有连)

考虑它再连向先前的另外一个点,显然需要分情况讨论

  • 如果这个点是也白色的,那连向它也不会被算进好的路径里,不管连不连都不影响奇偶性

  • 如果这个点是偶黑,连向它方案数等于加了一个偶数,不管连不连都不影响奇偶性

  • 如果这个点是奇黑?

  • 这个时候不能再单独考虑了,需要结合整体考虑

  • 如果 \(i\) 号点一共连向了奇数个奇黑,那奇偶性会改变(奇 \(\times\)\(=\) 奇)

  • 如果 \(i\) 号点一共了连向了偶数个奇黑,那奇偶性就不会改变(奇 \(\times\)\(=\) 偶)

总结一下,如果不考虑奇黑,不管怎么连都不会改变奇偶性,所以可以随意连

那如果考虑奇黑?

其实有个很巧妙的想法,可以拿出一个奇黑来控制奇偶性

这个奇黑是可以选择连或不连的,而这两种情况奇偶性肯定不相同

举个例子,比如其它 \(i-2\) 个点已经连好,路径总数是奇条

如果果不连这个奇黑,奇的方案++

如果连这个奇黑,偶的方案++

好,那么奇或偶的方案数就是各加上 \(2^{i-2}\)

(因为选出一个奇黑控制奇偶性,其它随便选,选或不选)

噢,我们还漏了一种情况

如果一个奇黑都没有呢?

那奇偶性根本就改变不了了,只能是它自己一个点不连边,奇的方案总数……

那它也就只能做奇白了,方案数加上 \(2^{i-1}\)

(不用选出一个奇黑控制奇偶性了,随便选)

\(i\) 号点是黑的情况也能同理可得了

思路大概就是这样了,或许不是很清晰?慢慢理解吧

做法

这才是精髓啊

根据上面的解释,dp数组的值肯定是方案数

但状态呢?

再看一眼总结,会发现我们只关心有没有奇黑(或奇白)

而其它的都很巧妙地被奇偶性绕开了

存状态就只存这个!

\(f[i][id][ob][ow]\)前i个点里,路径总条数的奇偶性是偶或奇,有无奇黑,有无奇白的方案总数

\(O(n)\) 是怎么做到的呢?

每次只转移上一个

这也是为什么状态设计的是前 \(i\) 个点里的总和,而不仅仅是第 \(i\) 个点

这样转移状态的时候就不仅仅是 \(2^{i-1}\)\(2^{i-2}\) 次方了,还要\(f[i-1][][][]\)

  • 注意不是加,是乘

为什么呢?

因为 \(f[i-1][][][]\) 意思是不包含 \(i\) 号点的方案数

而转移计算的是包含 \(i\) 号点的方案数

两种情况互不干扰,可以理解成先选不包含 \(i\) ,再选包含 \(i\) ,乘法原理

答案直接统计 \(f[n][p][][]\) 即可

\(O(n)\)

注意事项

  • 初始化 \(f[0][0][0][0]=1\)

  • 别忘 \(long long\) ……

AC代码

#include<bits/stdc++.h>
using namespace std;
const long long N=1000000+5,mod=1e9+7;
long long n,p;
long long a[N],fx[N],f[N][2][2][2];
int main(){
	scanf("%lld%lld",&n,&p);
	for(long long i=1;i<=n;i++) scanf("%lld",&a[i]);
	fx[0]=1;
	for(long long i=1;i<=n;i++) fx[i]=fx[i-1]*2%mod;//预处理2^i 
	f[0][0][0][0]=1;//初始化 
	for(long long i=1;i<=n;i++)//枚举i 
		for(long long id=0;id<=1;id++)//枚举id
			for(long long ob=0;ob<=1;ob++)//枚举ob
				for(long long ow=0;ow<=1;ow++){//枚举ow
					long long owo=f[i-1][id][ob][ow];//偷懒owo 
					if(a[i]!=0)//如果涂白 
						if(ob)//如果有奇黑 
							f[i][id][ob][ow]=(f[i][id][ob][ow]+fx[i-2]*owo%mod)%mod,//偶白 
							f[i][id^1][ob][1]=(f[i][id^1][ob][1]+fx[i-2]*owo%mod)%mod;//奇白(奇白就肯定为1了) 
						else
							f[i][id^1][ob][1]=(f[i][id^1][ob][1]+fx[i-1]*owo%mod)%mod;//没有奇黑,只能转移到奇白 
					if(a[i]!=1)//如果涂黑 
						if(ow)//如果有奇白 
							f[i][id][ob][ow]=(f[i][id][ob][ow]+fx[i-2]*owo%mod)%mod,//偶黑
							f[i][id^1][1][ow]=(f[i][id^1][1][ow]+fx[i-2]*owo%mod)%mod;//奇黑(奇黑就肯定为1了)
						else
							f[i][id^1][1][ow]=(f[i][id^1][1][ow]+fx[i-1]*owo%mod)%mod;//没有奇白,只能转移到奇黑 
				}
	printf("%lld",(f[n][p][0][0]+f[n][p][0][1]+f[n][p][1][0]+f[n][p][1][1])%mod);
	//直接计算答案 
}

T10 Hero meet devil (HDU4899)

HDU传送门

题意简述

给出一个字符串 \(S\) ,这个字符串只由 '\(A\)' , '\(C\)' , '\(G\)' , '\(T\)' 四个字母组成。

对于 \(0\)~\(|S|\) 中的每一个 \(i\) ,求出满足以下条件的字符串 \(T\) 的个数:

  • 1.长度为 \(m\)

  • 2.只由 '\(A\)' , '\(C\)' , '\(G\)' , '\(T\)' 四个字母组成;

  • 3.\(LCS(S,T)=i\)

答案对 \(1e9+7\) 取模后输出

  • 延伸:对于长度为m的,随机的,由 '\(A\)' , '\(C\)' , '\(G\)' , '\(T\)' 构成的字符串求它与 \(S\) \(LCS\) 期望值
数据范围

\(1 \leq T \leq 5\)

\(1 \leq |S| \leq 15\) , \(1 \leq n \leq 1000\)

\(Time: 1.2s\) , \(Memory:8MB\)

(其实可以跑进1s,原题没这么卡不过一样的)


题解

思路

\(|S|\)\(15\)……大概要用 \(2^n\) 的算法

\(LCS\) 这个东西啊,其实并不是很好求……

可以先考虑一个字符串 \(T\)\(S\) 匹配

回想一下以前是怎么求 \(LCS\)

\(dp[i][j]\)\(S\) 的前 \(i\) 位与 \(T\) 的前 \(j\) 位匹配的 \(LCS\)

那么有

\(dp[i][j]=dp[i-1][j-1]+1 (s[i]==t[j])\)

\(dp[i][j]=max(dp[i-1][j],dp[i][j-1]) (s[i]!=t[j])\)

这样可以得到一个 \(dp\) 矩阵

那怎么去计数呢?

回到多个 \(T\)

虽然 \(T\) 是不固定的,但是 \(S\) 是固定的

也就是说,不管 \(T\) 是多少,矩阵的行数总是 \(|S|\) 不变

当然我们只关心最后一列(即 \(dp[i][|T|]\)

那对于一个确定的 \(|T|\) 来说,只有最后一列是真正有用的,而它只有 \(|S|<=15\) 个数,每个数也不超过 \(15\)

所以最多也只有这么多种状态,很好统计

可以考虑以最后一列为状态再进行一次dp,这次存储的就是方案数了


\(15\) 个数的状态有多少种呢……

先不说怎么打了, \(15^{15}\) 肯定炸了吧

但是真的有这么多种状态吗?

观察发现,它肯定是单调不减的

而且相邻两位之间差绝对值不超过 \(1\)

因为多加一位, \(LCS\) 不可能多出2位啊

所以我们就可以状压了嘛,把差分数组压成二进制

那这个大小就只有 \(2^{15}=32768\) ,好写省时省空间~

做法

\(f[i][mac]\) 为长度为i的字符串, \(LCS DP\)矩阵最后一列为mac的方案数

(这里mac可以理解成15个数)

转移就很简单了,相当于在结尾加一个字符

等等,那加一个字符mac怎么变呢?

显然用前面 \(LCS\) 的朴素算法就行了

预处理一个 \(trans[mac][i]\) 数组,就很快了

预处理时间复杂度 $ O(|S|2^{|S|}) $

\(f[i+1][trans_{mac,k}]+=f[i][mac]\)

答案怎么统计?

\(las_{mac}\) 表示 \(mac\) 的最后一个数(即\(dp[|S|][|T|]\)

\(ans[las_{mac}]+=f[m][mac]\)

时间复杂度 \(O(m2^{|S|})\)

总时间复杂度 \(O(T(|S|+m)2^{|S|})\),可以跑进 \(1s\)

注意事项

  • 初始化 \(f[0][0]=1\)

  • 这题卡空间,把 \(f\) 滚动一下即可

AC代码

#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int t,n,m;
char s[16];
int q[16],trans[33000][5],f[2][33000],ans[16];
void tran(int mac,int k){
	int a[16]={},b[16]={};//记得清零 
	for(int i=1;i<=n;i++) a[i]=a[i-1]+((mac>>(i-1))&1);
	//把状压差分打开 
	for(int i=1;i<=n;i++){
		if(k!=q[i]) b[i]=max(b[i-1],a[i]);
		if(k==q[i]) b[i]=max(b[i],a[i-1]+1);
	}//按照朴素LCS的转移 
	for(int i=1;i<=n;i++) trans[mac][k]|=((b[i]-b[i-1])<<(i-1));
	//把数组化成状压差分 
}
void pre(){
	for(int j=0;j<=(1<<n)-1;j++)
		for(int k=1;k<=4;k++)
			tran(j,k);
}
int las(int mac){
	int a[16]; a[0]=0;
	for(int i=1;i<=n;i++) a[i]=a[i-1]+((mac>>(i-1))&1);
	return a[n];
}
int main(){
	scanf("%d",&t);
	while(t--){
		memset(trans,0,sizeof(trans));
		memset(f,0,sizeof(f));
		memset(ans,0,sizeof(ans));
		//清空数组 
		scanf("%s%d",s,&m);
		n=strlen(s);
		for(int i=1;i<=n;i++)
			switch(s[i-1]){
				case 'A':q[i]=1;break;
				case 'C':q[i]=2;break;
				case 'G':q[i]=3;break;
				case 'T':q[i]=4;break;
			}//把字符转化成好处理的数字 
		pre();//预处理 
		bool id=1;//滚动 
		f[0][0]=1;//初始化(因为第一层id=1所以这里是f[1][0]=1) 
		for(int i=1;i<=m;i++){
			id=!id;
			for(int j=0;j<=(1<<n)-1;j++) f[!id][j]=0;//先清零! 
			for(int j=0;j<=(1<<n)-1;j++)//枚举mac 
				for(int k=1;k<=4;k++)//枚举k 
					f[!id][trans[j][k]]=(f[!id][trans[j][k]]+f[id][j])%mod;
		}
		for(int j=0;j<=(1<<n)-1;j++)//枚举mac 
			ans[las(j)]=(ans[las(j)]+f[!id][j])%mod;
		for(int i=0;i<=n;i++) printf("%d\n",ans[i]);
	}
} 

T11 Square (HDU5079)

HDU传送门

题意简述

给出一个 \(N \times N\) 的网格,其中有一些格子可以涂色,有一些格子已经损坏(即固定为黑色)。

定义一个网格的优美度为其中最大的白色子正方形边长

对于 \(1\) ~ \(n\) 中的每个 \(i\) ,求优美度为 \(i\) 的涂色方案数。

  • 延伸:在能涂色的格子中任意涂色,求最大的白色子正方形边长的期望值
数据范围

\(1 \leq T \leq 10\)

\(1 \leq n \leq 8\)


题解

思路

跟T10很相似(还是有些不同)

首先n才8,大概又要考虑状压了

求最大正方形边长是某个定值,肯定不如有大小关系的方便

于是记 \(ans[i]\) 为 最大白色子正方形边长小于 \(i\) 的方案数(至于为什么是小于等会就知道了)

于是只要输出 \(ans[i+1]-ans[i] (0 \leq i \leq n)\) 即可

\(ans[0]=1\) (全涂黑)

\(ans[n+1]=2^{unbroken}\)(无论怎么涂都可以)

所以只用考虑 \(i=2\) ~ \(n\)即可


先来想想最大白色子正方形边长怎么判定

还记得吗?以前我们是按点用dp来判定的

但要是仿照T10,这个dp矩阵似乎有点过于庞大(超时)

所以干脆来直接考虑边

假设我们现在考虑的是边长不超过 \(siz\) 的正方形

那一行里就会有可能作为正方形底边的 \(n-siz+1\) 条边:

\(1\) ~ \(siz\) , \(2\) ~ \(siz+1\) , \(3\) ~ \(siz+2\) , \(\dots\) , \(n-siz+1\) ~ \(n\)

可以转移的是什么呢?

从每条边开始,最多能向上延伸k行

或者说从每条边开始,能往上画出一个 \(siz \times k\) 的白色矩形

或者说这个边中的每一列向上延伸的连续正方形的最小值为k

(再不理解的去看看这个题解吧 QaQ)

这东西状态就比较少了,但显然不是二进制的

显然当 \(k>=siz\) 的时候,就已经形成了一个 \(siz \times siz\) 的白色正方形了

所以就要舍掉~

所以会发现每一位都是 \(0\) ~ \(siz-1\),也就是siz进制的

所以刚开始的时候设的是小于 \(siz\) ,而不是小于等于。不然这里每一位都是 \(1\) ~ \(siz\) ,不好处理了

做法

dp就好办了,设 f[i][mac] 为第i行,状态为mac的方案数

来暴力一点

从i-1行转移到第i行的时候,先枚举mac( \(siz^{n-siz+1}\)

再枚举第i行的涂色 ( \(2^n\) )

枚举 \(j\) ~ \(j+siz-1\) ,只要这里面的涂色有黑格,那就意味着上面无论什么正方形都被破坏掉了,\(new\)_\(mac\)的这一位变成 \(0\)

没有黑格,那 \(new\)_\(mac\)的这一位就等于 \(mac\) 的这一位 \(+1\)

只要符合条件(没有一位超过 \(siz\) ),就可以把 \(f[i-1][mac]\) 的值加进 \(f[i][new\)_\(mac]\)

外面还要套一层枚举 \(ans\)\(n\) ,枚举 \(i\)\(n\)

总时间复杂度 \(O(n^2 2^n siz^{n-siz+1})\)

于是还会发现,因为有些格子已被损坏,固定为黑格,其实不用枚举 \(2^n\)

不过不优化这个也能过啦

(值得一提,1s是有可能超的,但卡卡常还是挺快的)

注意事项

  • dp数组开1100是绝对够了,不放心还可加

  • ans数组至少要开 \(10\) !(你还要放 \(n+1\) 的)

  • 要计算的先计算出来再放到循环里,不然慢啊……

  • dp时第一层只用dp一次就好了,初始化 \(f[0][0]=1\)

  • 别忘清空哦,所有数组都要清空!

AC代码

#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int t,n,siz;
bool a[10][10];
int f[10][1100],ans[10];
int ksm(int a,int x){int sum=1; while(x) {if(x&1) sum=sum*1LL*a%mod; a=a*1LL*a%mod,x>>=1;} return sum;}
//快速幂不解释
void go(int r,int mac){
	int m[9]={},tmp[9]={};
	int q=mac;
	for(int i=1;i<=n-siz+1;i++) m[i]=q%siz,q/=siz;//把mac拆开 
	for(int i=0;i<=(1<<n)-1;i++){//枚举格子涂色 
		int p[9]={},s[9]={}; bool flag=0;
		for(int j=1;j<=n;j++){
			p[j]=(i>>(j-1))&1;
			if(p[j]==0 && a[r][j]==1){//只要有损坏的格子涂了白 
				flag=1;
				break;
			} 
			s[j]=s[j-1]+p[j];//前缀和,方便统计是否有黑格 
		}
		if(flag) continue;
		for(int j=1;j<=n-siz+1;j++) tmp[j]=m[j];//tmp即新mac 
		for(int j=1;j<=n-siz+1;j++){
			if(s[j+siz-1]-s[j-1]==0){//如果没有黑格 
				tmp[j]++;
				if(tmp[j]>=siz){
					flag=1;
					break;
				}
			}
			else tmp[j]=0;//如果有黑格 
		}
		if(flag) continue;
		int new_mac=0;
		for(int j=n-siz+1;j>=1;j--) new_mac=new_mac*siz+tmp[j];//把新mac压好 
		f[r][new_mac]=(f[r][new_mac]+f[r-1][mac])%mod;//转移 
	}
}
void solve(){
	memset(f,0,sizeof(f));//清空f
	f[0][0]=1;//初始化 
	int t=pow(siz,n-siz+1)-1;//计算的先拿出来算 
	go(1,0);//第一层一次就好 
	for(int i=2;i<=n;i++)
		for(int j=0;j<=t;j++)
			go(i,j);
	for(int mac=0;mac<=t;mac++)
		ans[siz]=(ans[siz]+f[n][mac])%mod;
}
int main(){
	scanf("%d",&t);
	while(t--){
		memset(ans,0,sizeof(ans));
		memset(a,0,sizeof(a));
		scanf("%d",&n);
		int tot=0;
		for(int i=1;i<=n;i++)
			for(int j=1;j<=n;j++){
				char c;cin>>c;
				if(c=='*') a[i][j]=1;//a[i][j]表示该格是否已被损坏 
				else tot++;//统计unbroken个数 
			}
		ans[1]=1,ans[n+1]=ksm(2,tot);
		for(int i=2;i<=n;i++) siz=i,solve();
		for(int i=0;i<=n;i++) printf("%d\n",(ans[i+1]+mod-ans[i])%mod);
	}
} 

T12 XHXJ's LIS (HDU4352)

经过前面几题,想必大家对状压很熟悉了

HDU传送门

题意简述

求区间 \([L,R]\) 中,各位数字组成的序列的 \(LIS\) (最长上升子序列)恰好为 \(k\) 的数字个数。

  • 延伸:在区间 \([L,R]\) 中任意选出一个数,求各位数字组成的序列的 \(LIS\) (最长上升子序列)的期望值。
数据范围

\(1 \leq T \leq 10^4\)

\(0 < L \leq R <2^{63}-1\) , \(1 \leq k \leq 10\)


题解

思路

和前面几题类似,还是考虑对于单个数如何求各位数组成的 \(LIS\)

但是这里不能用那个 \(O(n^2)\) 的算法(用 \(f[i]\) 表示前 \(i\) 个的 \(LIS\) ),不但时间不优,而且空间不够

有一种 \(O(nlogn)\) 的算法。不记录前 \(i\) 个的 \(LIS\) ,而是考虑用 \(f[i]\) 记录 \(LIS\)\(i\) 时最后一个数的最小值(只是为了方便理解,实际上无需记录)

一个一个地加入,用 \(ans\) 表示目前的 \(LIS\)

如果加入的这个数是最大的,那么 \(ans++\)\(f[ans]=a[i]\)

否则,在前面的 \(a[j](j<i)\) 中找到最小的,大于等于这个数的,把所有这样的替换成 \(a[i]\) ,再更新一次 \(f\)

为什么呢?

  • 对于 \(i\) 前面的,既然 \(a[j]\) 已经是大于等于 \(a[i]\) 中最小的了,它变成 \(a[i]\) 不会改变大小关系,也不会影响答案。

  • 对于 \(i\) 后面的,(如果不换)选用 \(a[i]\) 显然比选用 \(a[j]\) 更优,替换掉相当于等效

举个栗子,\(25413\)

  • 加入2, 5,\(f[1]=2\)\(f[2]=5\)

  • 加入4,4之前最小的 \(\geq4\) 的是5,因此把5换成4,24413,\(f[2]=4\)

  • 加入1,1之前最小的 \(\geq1\) 的是2,因此把2换成1,\(14413\)

  • 加入3,3之前最小的 \(\geq3\) 的是4,因此把4换成3,\(13313\)\(f[2]=3\)

  • 所以 \(LCS=2\)

当然光是这样空间是 \(10^{10}\) ,想都不用想

其实这种dp有个特性,它只关心每个数有没有出现过,却不关心具体的位置与个数

于是我们可以直接记录每个数是否出现过,空间只有 \(2^{10}=1024\)

最后的 \(LIS\) 即是出现的数的总个数了

这是状压部分,当然既然要求 \([L,R]\) 的,还得套个数位dp

发现状压和数位的加入顺序都是从高位到低位,直接dp就好了

做法

\(f[i][mac]\) 为枚举到第i位,状压为mac时的方案数

不过询问数有 \(10^4\) ,每次清零太慢了

不如再加一维 \(k\) 表示 \(LIS\)\(k\) ,直接记录不清零

数位dp我想不用再细说了吧

注意事项

  • 这里前导0是有影响的,注意判断

  • 状态更新时注意如果原来是0,加入0是无需更新的(要特判)

AC代码

#include<bits/stdc++.h>
using namespace std;
#define ll long long
ll t,l,r,k;
ll a[22],f[22][1100][11];
ll dp(ll pos,ll mac,bool lead,bool eq){//数位dp模板 
	if(pos==0){//如果枚举完了 
		ll tot=0;
		for(ll i=0;i<=9;i++) if((mac>>i)&1) tot++;
		if(tot==k) return 1;//如果LIS为k 
		else return 0;
	}
	if(!eq && !lead && f[pos][mac][k]!=-1) return f[pos][mac][k];
	ll ed=eq?a[pos]:9,ret=0;//模板 
	for(ll i=0;i<=ed;i++){
		ll new_mac=mac;
		if(!lead || i){//注意不为0或加上的数不为0才需要转移 
			for(ll j=i;j<=9;j++)
				if((mac>>j)&1){
					new_mac^=(1<<j);
					break;
				}
			new_mac|=(1<<i);
		}
		ret+=dp(pos-1,new_mac,lead && !i,eq && i==a[pos]);
	}
	if(!eq && !lead) f[pos][mac][k]=ret;
	return ret;
}
ll solve(ll x){//数位dp模板 
	memset(a,0,sizeof(a));
	while(x){
		a[++a[0]]=x%10;
		x/=10;
	}
	return dp(a[0],0,1,1);
}
int main(){
	memset(f,-1,sizeof(f));//注意初始化 
	scanf("%lld",&t);
	for(ll i=1;i<=t;i++){
		scanf("%lld%lld%lld",&l,&r,&k);
		printf("Case #%lld: %lld\n",i,solve(r)-solve(l-1));
	}
}

Part 4 DP+杂题


T13 Delivery Club (CF875E)

这题就不是dp

CF传送门

洛谷传送门

题意简述

有两个快递员,分别在 \(s_1\),$ s_2$,现在有 \(n\) 个任务,每个任务 \(x_i\) 表示要将货物送到 \(x_i\) 。即让任何一个快递员到 \(x_i\) ,另一个快递员原地不动。

由于快递员之间需要有对讲机联系,请你设计一种方案使得两个快递员之间的最长距离最短。

数据范围

\(1 \leq n \leq 10^5\)

\(1 \leq s_1,s_2 \leq 10^9\)


题解

思路

看到这不禁想起了另一道题

有两个人(A和B)和n个任务,两个人完成每个任务的时间不同,但一个任务只需一个人完成。现在要求按顺序完成,两人可以同时完成不同的任务,求完成所有任务的最小时间。

\(1 \leq n \leq 200\) , \(1 \leq a_i,b_i \leq 200\)

传送门

两个人就比较难处理

于是不难想到多开一维,记录A的时间

\(f[i][j]\) 表示进行完第i个任务,A用了j的时间时,B用的时间最小值

答案即为 \(min(max(i,f[n][i]))\)


回到这道题,会发现一个比较难堪的事情:数据范围大多了!

这种做法根本行不通的 所以不要dp了

换个思路,不如先二分答案

这样问题就变为了判断两人最长距离是否能不超过 \(k\)

然后呢?正着来肯定要dfs了吧……

正着不行就反着来

送完第 \(i\) 件货物,肯定有一个人在 \(a_i\)

现在我们关心另一个人的位置范围,假设就是 \(l_i\) ~ \(r_i\)

\(i-1\) 的情况肯定有一个人在 \(a_{i-1}\)

  • 如果 \(a_{i-1}\) 正好就在 \(l_i\) ~ \(r_i\)

\(i-1\) 的情况肯定有一个人在 \(a_{i-1}\),而另一个人的位置并没有限制(他在哪都能走到 \(a_i\)

所以 \(l_{i-1}=a_i-k\)\(r_{i-1}=a_i+k\)

  • 如果 \(a_{i-1}\) 不在 \(l_i\) ~ \(r_i\) 里呢?

那还是有一个人在 \(a_{i-1}\) 里,

那另一个人又得在 \(a_{i-1}-k\) ~ \(a_{i-1}+k\) 里,又得在 \(l_i\) ~ \(r_i\)

\(l_{i-1}=max(a_{i-1}-k,l_i)\)\(r_{i-1}=min(a_{i-1}+k,r_i)\)

  • 要是这两部分根本就没有重叠部分呢?

\(l_{i-1}>r_{i-1}\) 了, \(return\) 即可(其实不 \(return\) 也行,到最后也会 \(return\) 掉)

最后会得到 \(l_1\) ~ \(r_1\)

我们只需要一个人在这个范围里就行了

因为另一个人只要跟他距离不超过 \(k\) ,就肯定能来到 \(a_1\)

AC代码

#include<bits/stdc++.h>
using namespace std;
int n,s1,s2;
int a[100005];
bool check(int k){
	int l=-1e9,r=1e9;
	for(int i=n;i>=1;i--){
		if(l<=a[i] && a[i]<=r) l=a[i]-k,r=a[i]+k;//a[i]在范围内 
		else l=max(l,a[i]-k),r=min(r,a[i]+k);//a[i]不在范围内 
		if(l>r) continue; 
	}
	if((l<=s1 && s1<=r) || (l<=s2  && s2<=r)) return 1;//有一个在范围内 
	return 0;
}
int main(){
	scanf("%d%d%d",&n,&s1,&s2);
	for(int i=1;i<=n;i++) scanf("%d",&a[i]);
	int l=abs(s1-s2),r=1e9;//注意l不能赋成1(或者在上面判断s1,s2的距离) 
	while(l!=r){
		int mid=(l+r)>>1;
		if(check(mid)) r=mid;
		else l=mid+1;
	}
	printf("%d",l);
}

T14 Make It One (CF1043F)

CF传送门

洛谷传送门

题意简述

\(Shirley\)\(n\) 个数,她可以选出一些数,使得它们的 \(gcd\)\(1\) 。问最少能选多少个数?

如果任意选择都不能满足条件,请输出 \(−1\)

数据范围

\(1 \leq n \leq 3*10^5\)

\(1 \leq a_i \leq 3*10^5\)


题解

思路

一眼看上去很简单的样子,却又想不到什么妙方法

特性很多,不如先来研究一下答案的特征?

\(2 \times 3 \times 5 \times 7 \times 11 \times 13 < 300000 < 2 \times 3 \times 5 \times 7 \times 11 \times 13 \times 17\)

答案最长只有7,因为只要不是无解,每加入一个数必定至少从gcd中去除一个质因数

最极限的情况:(转自ZEZ题解

\(a[1]= 3*5*7*11*13*17\)

\(a[2]=2 * 5*7*11*13*17\)

\(a[3]=2*3 * 7*11*13*17\)

\(a[4]=2*3*5 * 11*13*17\)

\(a[5]=2*3*5*7 * 13*17\)

\(a[6]=2*3*5*7*11 * 17\)

\(a[7]=2*3*5*7*11*13\)


于是我们枚举答案为 \(len\),判断是否满足

接下来怎么办呢?

一个很巧妙的想法:DP!

虽然只是要求我们判断是否有满足条件的解

但我们可以来计数(虽然看似多此一举,但方便转移)

\(dp[i]\) 表示选出 \(gcd\)\(i\) 的方案数

最大公因数为 \(i\) 很不好处理,但有 \(i\) 这个因数就很好处理了

只需从能被i整除的数中选出 \(len\) 个即可

这其中还包含最大公因数不为 \(i\) 的,即都能被 \(i \times j\) 整除的

于是——

\(dp[i]=C^{len}_{cnt[i]}-\sum\limits_{j=2}dp[j]\)

其中 \(cnt[i]\) 表示 \(a[i]\) 中能被 \(i\) 整除的个数,可以预处理

显然是从大往小dp

当然我们总不能从无穷大开始吧,要从 \(gcd\) 最大可能值—— \(a[i]\) 最大值开始

只要 \(dp[1]\) 不是 \(0\) ,就表示 \(len\) 可行

注意事项

  • 做着做着你就会发现组合数太大了……

所以要模一个大质数

比如 \(988244353\) 什么的

逆元直接打表噢

  • 无需预处理

  • 记得清空

AC代码

#include<bits/stdc++.h>
using namespace std;
const long long N=300005,mod=998244353;
long long n,maxer=-1e9;
long long a[N],cnt[N],dp[N];
long long fx[8]={0,1,499122177,332748118,249561089,598946612,166374059,142606336};
long long C(long long m,long long n){
	if(m<n) return 0;
	long long ans=1;
	for(long long i=m;i>=m-n+1;i--) ans=ans*i%mod;
	ans=ans*fx[n]%mod;
	return ans;
}
int main(){
	scanf("%lld",&n);
	for(long long i=1;i<=n;i++){
		scanf("%lld",&a[i]);
		maxer=max(maxer,a[i]);
		for(long long j=1;j<=sqrt(a[i]);j++)//预处理cnt 
			if(a[i]%j==0){
				cnt[j]++;
				if(a[i]!=j*j) cnt[a[i]/j]++;
			}
	}
	for(long long len=1;len<=7;len++){//枚举答案 
		for(long long i=maxer;i>=1;i--){
			dp[i]=C(cnt[i],len);
			for(long long j=2;i*j<=maxer;j++)
				dp[i]=(dp[i]-dp[i*j])%mod;//枚举转移 
		}
		if(dp[1]) {printf("%lld",len);return 0;}//如果有解就输出 
	}
	printf("-1");
}

T15 New Year and Binary Tree Paths (CF750G)

很妙的一道题,隐藏的数位dp

CF传送门

洛谷传送门

题意简述

一颗无穷个节点的完全二叉树,编号满足线段树分配,求有多少条树上的简单路径编号和为 \(s\)

数据范围

\(1 \leq s \leq 10^{15}\)

\(Time:3s\)


题解

思路

乍看没有什么思路……

仔细一想,一条路径只有两种可能,一条链或是一个分叉(顶点即为 \(LCA\)

设顶点为链上深度最小的点。如果确定了顶点为 \(x\) ,其实有了 \(s\) 的限制,可能性也不多了


  • 一条链

设链的长度为 \(h\)

假设从 \(x\) 往下全走左边,权值和即为

\(\sum\limits_{i=0}^{h-1} 2^ix = (2^h-1)x \leq s\)

全走右边呢?

\(\sum\limits_{i=0}^{h-1} 2^i(x+1)-1 = (2^h-1)(x+1)-h \geq s\)

只有唯一解 \(x_0\)

证明:
\(x=x_0+1\) 时,$ (2^h-1)x = (2^h-1)(x_0+1) \geq s+h >s $ ,与 \((1)\) 矛盾
\(x=x_0-1\) 时,$ (2^h-1)x_0 -h \leq s-h <s$,与 \((2)\) 矛盾

所以对于一个确定的深度 \(h\) ,顶点 \(x\) 是确定的!

但并不是每个 \(h\) 都有解

设现在链上的所有节点都在左边,此时和为 \(base = (2^h-1)x\)

选出一些节点变成右边

现在来考虑把一个左边的结点变成右边,这个点到链底的深度为 \(p\) ,其它相对于父节点位置不变

这样一个变换会带来 \(\sum\limits_{i=0}^{p-1} 2^ix = (2^p-1)x\) 的贡献

只要找一些 \(p\) 使得贡献和为 \(ret = s - base\) 即可

发现 \(2^i-1 = \sum\limits_{j=0}^{i-1} 2^j > \sum\limits_{j=0}^{j-1} 2^j-1\)

直接从大到小贪心做即可

\(ret = s \bmod (2^h-1)\) , \((s>=2^h-1)\)


  • 一个分叉 \(=\) 两条链

类似的,设顶点为 \(x\)

但是为了方便,这次从子节点 \(2x\) (深度为 \(h_1\))与 \(2x+1\) (深度为 \(h_2\))开始算深度

还是先都往左走

\[\begin{aligned} base &= x+2x(2^{h_1}-1)+(2x+1)(2^{h_2}-1)\\ &=x+2^{h_1+1}x-2x+2^{h_2+1}x-2x+2^{h_2}-1\\ &=x(2^{h_1+1}+2^{h_2+1}-3)+2^{h_2}-1 \end{aligned} \]

同理仍然可以得到当 \(h_1\) , \(h_2\) 确定时,\(x\) 也是确定的

这次我们要用 \(2^1-1, 2^2-1,\dots,2^{h_1}-1,2^1-1,2^2-1,\dots,2^{h_2}-1\) 来拼凑出 \(s - base\)

显然麻烦多了,因为凑出的方法有很多

来转换一下,假设要选出 \(n\) 个数来凑,其实就是用 \(2^1, 2^2,\dots,2^{h_1},2^1,2^2,\dots,2^{h_2}\) 凑出 $ ret = s - base + n $

可以数位 DP!

\(ret = (s-2^{h_2}+1) \bmod (2^{h_1+1}+2^{h_2+1}-3)\) , \((s-2^{h_2}+1>=2^{h_1+1}+2^{h_2+1}-3)\)

做法

数位dp外面要枚举 \(h_1\) , \(h_2\) , \(n\)

这里的数位dp其实并不是传统的那种数位dp,只是借用了这个思想

顺序也不是从高往低,而是从低往高

选第 \(i\) 个数( \(2^{i+1}\) )相当于往第 \(i+1\) 位加 \(1\) ,超过了 \(2\) 就会进位

所以设 \(f[i][j][k]\) 为选到第 \(i+1\) 位,共选了 \(j\) 个数,上一位是否进到了这一位

当然需要满足 \((\)这一位选择的数量\(+k)\)%\(2==ret\)的这一位

最后的答案是 \(f[log2(ret)][n][0]\) (因为进位就不是这个数了)

注意事项

这题细节也很多

  • 计算2的幂不能直接用位运算(因为ll会溢出),需要预处理

  • 预处理还需要多处理一位,因为后面会用到

  • 计算 \(x\) 时要判断 \(x\) 存不存在,不存在就 \(break\)

  • 算分叉时要特判 \(ret==0\) (不用转移),直接加进答案

为什么?我也不知道

  • 只有当 \(ret\) 为偶数时才有解(代码中用 \(ret + n\) 代替了)

  • \(f\) 数组初始化:\(f[0][0][0]=1\) ,记得清空

  • dp时要注意判断能不能选

AC代码

#include<bits/stdc++.h>
using namespace std;
#define ll long long
ll s,bit[66],cnt,ans,f[66][155][2];
ll dp(ll s,ll tot,ll h1,ll h2){
	memset(f,0,sizeof(f));
	f[0][0][0]=1;//初始化 
	int ed=log2(s);
	for(ll i=1;i<=ed;i++)
		for(ll j=0;j<=i*2-2;j++)//j是之前选过的数的数量 
			for(ll k=0;k<=1;k++)//是否进位 
				for(ll p1=0;p1<=1;p1++){//选不选h1的 
					if(i>=h1 && p1) break;
					for(ll p2=0;p2<=1;p2++){//选不选h2的 
						if(i>=h2 && p2) break;
						if((p1+p2+k)%2==((s>>i)&1))//转移条件 
							f[i][j+p1+p2][(p1+p2+k)/2]+=f[i-1][j][k]; 
					}
				}
	return f[ed][tot][0];
}
int main(){
	scanf("%lld",&s);
	bit[0]=1;
	for(;bit[cnt]<=s;)
		cnt++,bit[cnt]=bit[cnt-1]*2;//预处理2的幂 
	bit[cnt+1]=bit[cnt]*2;//多处理一位 
	for(ll i=1;i<=cnt;i++){//链的情况 
		if(s<(bit[i]-1)) break;//x不存在 
		ll ret=s%(bit[i]-1);
		for(ll j=i;j>=1;j--) if(ret>=bit[j]-1) ret-=bit[j]-1;//贪心 
		if(!ret) ans++;
	}
	for(ll h1=1;h1<=cnt;h1++)
		for(ll h2=1;h2<=cnt;h2++){//枚举h1,h2 
			if((s-bit[h2]+1)<(bit[h1+1]+bit[h2+1]-3)) break;//x不存在 
			ll ret=(s-bit[h2]+1)%(bit[h1+1]+bit[h2+1]-3);
			if(!ret) {ans++; continue;}//特判ret=0 
			for(ll n=1;n<=h1+h2;n++)//枚举n 
				if((ret+n)%2==0)//必须是偶数才有解 
					ans+=dp(ret+n,n,h1,h2);
			}
	printf("%lld",ans);
}

完结撒花~

posted @ 2020-11-21 18:23  苹果蓝17  阅读(437)  评论(0编辑  收藏  举报