题解 DP专题汇总(15题)
DP专题
共15题
进度:15/15
完结撒花!!!
因为本人是蒟蒻所以好多题解都花了好长时间去理解
所以题解会从我自己思考的角度出发
并且 非 常 详细地解释
所以贼长(而且很啰嗦)QwQ
洛谷博客传送门
方便又快捷
Part1 DP与树
T1 GCD Counting (CF1101D)
T2 You Are Given a Tree (CF1039D)
T3 Vladislav and a Great Legend (CF1097G)
T4 Uniformly Branched Trees (CF724F)
Part2 DP与计数
T5 Multiplicity (CF1061C)
T6 Maximum Element (CF886E)
T7 The Top Scorer (CF1096E)
Part3 DP与状压
T8 Easy Problem (CF1096D)
T9 Kuro and Topological Parity (CF979E)
T10 Hero meet devil (HDU4899)
T11 Square (HDU5079)
T12 XHXJ's LIS (HDU4352)
Part4 DP与杂题
T13 Delivery Club (CF875E)
T14 Make It One (CF1043F)
T15 New Year and Binary Tree Paths (CF750G)
Part 1 DP+树
T1 GCD Counting (CF1101D)
CF传送门
洛谷传送门
数据加强版(UOJ) 100w数据
数据加强版(洛谷) 200w数据
题意简述
给出一棵有n个节点的树,树有点权
注意一个点也可以构成一条链.
数据范围
题解
思路
首先看清题意,会发现一条链上的点权
于是乎就把难以处理的gcd转换成了简单的整除问题
从约数的角度考虑。假如我已经确定了一个
显然可以用树上dp轻松搞定
但约数个数明显也不少啊
所以不妨再退回来,一次解决所有k
再回到只观察一个点,发现它只能由与它不互质的儿子转移上来
那就只转移这些就行了
再回来考虑
如果
显然 集合A{
所以约数个数至多6个
用
用
转移时直接暴力枚举即可(因为至多也就6*6=36嘛)
注意这只是从下往上的一条路径,
所以还要边dp边更新答案
把约数个数看作常数就是
注意事项
-
建双边别忘了开两倍大小(也就只有我犯了这个错吧)
-
预处理好像可以用筛加速
但是我懒所以直接打表(最大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);
}
时间测试
T2 You Are Given a Tree (CF1309D)
CF传送门
洛谷传送门
题意简述
有一棵
在树上选择一些长度为
求对于
数据范围
题解
思路
首先从特殊情况考虑
7s都无济于事
所以不妨来观察一下答案
显然,
而且
对任意一个
所以答案种数不会超过
对前
对后
做法
首先,dp部分
用
数据范围
题解
前铺知识
树形依赖背包
[复杂版]{https://blog.csdn.net/wt_cnyali/article/details/76649037}
题意:在一棵有根树上,每个节点都挂着一个物件有
选出一个包含根节点的连通块,使得体积之和不超过背包大小
[简化版]{https://www.cnblogs.com/water-mi/p/9818622.html}
题意:给定一棵有
即每个节点的体积均为1
--下面是解法--
我们可以仿照背包的思想
只不过在这里不是一个整体的背包,而是每个节点上都有一个背包
转移过程相当于把每个子节点当做一个物品,一个一个加进去
其他都与背包一样的
这道题用到的是树形依赖背包的思想
第二类斯特林数
[第二类斯特林数]{https://www.cnblogs.com/gzy-cjoier/p/8426987.html}
定义:第二类斯特林数
求法:
考虑有
如果新开一个,那就是
如果放到已有的盒子里,有
性质:
把k个不同的小球放入
但这里盒子是不同的,因此再乘上
思路
首先,看到要算的东西里有个大大的
而且
可以考虑用第二类斯特林数的那个性质转化
于是
交换和号
噢!
发现其实左边的东西都是可以很快算出来的
因此实际要算的是
于是我们把难以处理的幂换成了组合数
但这样直接去考虑好像和幂没有什么区别
不妨从整体来考虑
考虑它的实际意义,其实就是 对于每个生成树,从
选出
一个一个点来转移
解决了?
做法
NO
这题真正的难点不在于思路,在于接下来的实现
仿照树形依赖背包,设
请注意是 所有点集形成的生成树 (即要求的)而不是 所有生成树
请注意是所有而不是经过
我们是从边的角度来考虑的
也就是说,存在一种特殊情况
只连一棵子树(因为连了多棵子树就合法了),不选根节点而选它连向子节点的边
会被转移,但不会被计入答案
很好理解,就是在前面的点里先选
当然为了好写实际上是这样写的
理解到这可能又会冒出一个问题
这是按边来转移的,所以转移到父节点时,又不
当然其实可以连,也可以不连
所以dp完后还要把
最后一个问题
如何初始化?
因为选了
所以有两种情况
于是有一个东西不能转移
就是初始化时
所以还得减
看起来是
其实是
复杂度证明我也搞不明白就不放上来了
注意事项
· 很难理解,请结合题解与代码,仔细思考,不要着急
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传送门
洛谷传送门
题意简述
求有多少种不同构(即交换节点不能使两棵树完全相同)的
数据范围
题解
思路
刚拿到这个题目,显然最大的障碍就是如何处理同构(或者说如何判重)
对于一颗无根树来说判重显然是不现实的
自然就找一个根咯
这个根可不能随便乱找,要有一些有用的性质才行
比如说重心
这样一来子树的大小就不会超过
那接下来怎么做呢?
当然不是树形dp因为连一颗固定的树都没有
但还是得dp
关联的状态是什么呢?
显然有 节点个数,子树大小的上限
还有一个很妙的,根节点的子树个数
这样就方便转移了
于是用
第二种则是有子树大小为
假设有
那去掉这
但从
每颗子树有
其实就是t棵子树中,每颗子树都可不分顺序地,可重复地选
所以状态转移方程就出来了(别忘了前面的)
最后,不要得意忘形o
还有双重心要考虑(即n为偶数)
可以想象一下,就是一座桥连接着两颗全等(对称)的树,桥的两头就是两个重心
大概是这样的
\ /
/\__/\
/ \
怎么画得跟个人似的[doge]
两边的方案自然是完全一样的
这时候就会发现一种重复了
比如有两个方案
左
当然
要是这样的话,直接减去
做法
这个就很简单了
直接按
注意初始化还有枚举的区间(写在下面了QwQ)
组合数运算要除法,要用到逆元
只是阶乘的逆元,看到质数用费马小定理暴力处理就好了(反正不超过
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传送门
洛谷传送门
题意简述
从序列
题解
思路
显然是dp
发现条件是跟选出来的数列有关的
所以状态就要设计一个 选出来的数列长度
所以设
考虑从
可以不选第
也可以选,这时候就要求
所以状态转移方程
答案就是
做法
注意事项
- 别忘了取模
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传送门
洛谷传送门
题意简述
从前有一个叫
(太长了缩了一下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;
}
废话不多说,求有多少长度为
数据范围
不保证
题解
思路
直接考虑出错的情况太难啦
不妨从反面考虑,正确的情况有多少
其实就是函数在
这样就可以枚举
还差一个问题,选出一些数放到
(这个问题只和元素间的相对大小有关,不妨把它们归结到 (如果你看不懂上面这段话请当我什么都没说))
总而言之,是个很明显的dp了
考虑
考虑最大值
反之,只要
所以用
别忘了要先从i-1个数中选出j-1个数排到max前面
max后面的数是可以随便放的
答案计算就很简单了
很遗憾
我们来把这个式子打开优化
很惊奇地发现
于是前缀和优化
当然ans的计算也可以这样优化(不过没什么必要只是为了好写)
做法
预处理阶乘逆元什么的
已经说得很清楚了吧?
注意事项
-
依旧是开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传送门
洛谷传送门
题意简述
小明在打比赛,包括小明自己共有
现在小明已知自己的得分大于等于
数据范围
题解
思路
首先来暴力dp
涉及到最大值,因此可以让所有人都不超过某个值
设
状态转移方程很好写了,考虑新加入的是得分为i的选手
怎么计算答案呢?
考虑到有多个人并列第一的情况,不妨枚举q人并列第一并且小明得t分,计算情况总数
当然这q个人里必须包含小明
情况总数
别忘了答案是求概率,还得除以总数
总数怎么计算呢?
其实就是个数学问题,插板法
假设每个人的分数为
那么就是求
转化成正整数
即求
就是插板法,在
即共
写了这么多,累死我了 该出答案了吧?
木有
会发现这是
那么接下来的事情就与dp无关了
看看
所以能不能不经过递推,直接把单个dp算出来呢?
可以。
然后来考虑至少有
首先要选出这
这时候又可以来用插板法了,用
记
刚开始选出来的
最后来容斥,
把
还有ans的算法
时间复杂度最大
做法
对于每一个要求的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传送门
洛谷传送门
题意简述
给你一个长为
数据范围
题解
思路
首先,显然答案只跟
其它字符就不用删也不用管了
接下来显然考虑dp
既然是子序列,不要求连续,就只跟相对位置有关
设想扫一遍,扫的过程中,hard是由har加上d得到的,而har是由ha加上r得到的
那不妨就以已经有了hard中的前几个字符为状态吧
显然是无后效性的
被动转移即可
做法
设
当前为r:
当前为d:
注意事项
-
答案是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传送门
洛谷传送门
题意简述
给定
我们称一条好的路径为经过的点为黑白相间的路径,如果一个图好的路径的总数 %
数据范围
题解
这是 O(n) 的题解
这题在理解上有很多岔路,所以我会标出容易误解的地方
为了方便,“好的路径”基本都会用“路径”来代替
题意
解释一下,黑白相间指的是黑连向白,白连向黑
比如
- (一个也算!)
思路
首先第一眼,是跟奇偶性相关的计数问题
所以重心就落在了奇偶性上
然后发现加入的边必须从编号小的点指向编号大的点
显然是无环的,而且拓扑序就是顺序
直接dp好了
然而点也要选,边也要选,可能性太多了qwq
那我们先不要管点,只考虑连边
既然是按点dp,不妨来设想加入一个点,让前面的点向它连边
可是随机性太大了啊,所以要加状态来限制
首先能想到的就是颜色,要么是黑,要么是白
但这还不够。因为我们想统计的是路径数的奇偶性,而黑或白根本没有体现
所以还要再加上两种状态:连到这个点的路径总条数,要么是奇,要么是偶
综合起来,就是四种状态
- 注意不是加,是乘
为什么呢?
因为
而转移计算的是包含
两种情况互不干扰,可以理解成先选不包含
答案直接统计
注意事项
-
初始化
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传送门
题意简述
给出一个字符串
对于
-
1.长度为
m ; -
2.只由 '
A ' , 'C ' , 'G ' , 'T ' 四个字母组成; -
3.
LCS(S,T)=i 。
答案对
- 延伸:对于长度为m的,随机的,由 '
A ' , 'C ' , 'G ' , 'T ' 构成的字符串,求它与S 的LCS 期望值
数据范围
(其实可以跑进1s,原题没这么卡不过一样的)
题解
思路
那么有
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,这次存储的就是方案数了
可
先不说怎么打了,
但是真的有这么多种状态吗?
观察发现,它肯定是单调不减的
而且相邻两位之间差绝对值不超过
因为多加一位,
所以我们就可以状压了嘛,把差分数组压成二进制
那这个大小就只有
做法
设
(这里mac可以理解成15个数)
转移就很简单了,相当于在结尾加一个字符
等等,那加一个字符mac怎么变呢?
显然用前面
预处理一个
预处理时间复杂度
答案怎么统计?
设
时间复杂度
总时间复杂度
注意事项
-
初始化
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传送门
题意简述
给出一个
定义一个网格的优美度为其中最大的白色子正方形边长。
对于
- 延伸:在能涂色的格子中任意涂色,求最大的白色子正方形边长的期望值
数据范围
题解
思路
跟T10很相似(还是有些不同)
首先n才8,大概又要考虑状压了
求最大正方形边长是某个定值,肯定不如有大小关系的方便
于是记
于是只要输出
$ans[n+1]=2^{unbroken}$(无论怎么涂都可以)
所以只用考虑
先来想想最大白色子正方形边长怎么判定
还记得吗?以前我们是按点用dp来判定的
但要是仿照T10,这个dp矩阵似乎有点过于庞大(超时)
所以干脆来直接考虑边
假设我们现在考虑的是边长不超过
那一行里就会有可能作为正方形底边的
可以转移的是什么呢?
是从每条边开始,最多能向上延伸k行
或者说从每条边开始,能往上画出一个
或者说这个边中的每一列向上延伸的连续正方形的最小值为k
(再不理解的去看看这个题解吧 QaQ)
这东西状态就比较少了,但显然不是二进制的
显然当
所以就要舍掉~
所以会发现每一位都是
所以刚开始的时候设的是小于
做法
dp就好办了,设 f[i][mac] 为第i行,状态为mac的方案数
来暴力一点
从i-1行转移到第i行的时候,先枚举mac(
再枚举第i行的涂色 (
枚举
没有黑格,那
只要符合条件(没有一位超过
外面还要套一层枚举
总时间复杂度
于是还会发现,因为有些格子已被损坏,固定为黑格,其实不用枚举
不过不优化这个也能过啦
(值得一提,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 (最长上升子序列)的期望值。
数据范围
题解
思路
和前面几题类似,还是考虑对于单个数如何求各位数组成的
但是这里不能用那个
有一种
一个一个地加入,用
如果加入的这个数是最大的,那么
否则,在前面的
为什么呢?
-
对于
i 前面的,既然a[j] 已经是大于等于a[i] 中最小的了,它变成a[i] 不会改变大小关系,也不会影响答案。 -
对于
i 后面的,(如果不换)选用a[i] 显然比选用a[j] 更优,替换掉相当于等效
举个栗子,
-
加入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
当然光是这样空间是
其实这种dp有个特性,它只关心每个数有没有出现过,却不关心具体的位置与个数
于是我们可以直接记录每个数是否出现过,空间只有
最后的
这是状压部分,当然既然要求
发现状压和数位的加入顺序都是从高位到低位,直接dp就好了
做法
设
不过询问数有
不如再加一维
数位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传送门
洛谷传送门
题意简述
有两个快递员,分别在
由于快递员之间需要有对讲机联系,请你设计一种方案使得两个快递员之间的最长距离最短。
数据范围
题解
思路
看到这不禁想起了另一道题
有两个人(A和B)和n个任务,两个人完成每个任务的时间不同,但一个任务只需一个人完成。现在要求按顺序完成,两人可以同时完成不同的任务,求完成所有任务的最小时间。
1 \leq n \leq 200$ , $1 \leq a_i,b_i \leq 200 传送门
两个人就比较难处理
于是不难想到多开一维,记录A的时间
设
答案即为
回到这道题,会发现一个比较难堪的事情:数据范围大多了!
这种做法根本行不通的 所以不要dp了
换个思路,不如先二分答案
这样问题就变为了判断两人最长距离是否能不超过
然后呢?正着来肯定要dfs了吧……
正着不行就反着来
送完第
现在我们关心另一个人的位置范围,假设就是
那
- 如果
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 掉)
最后会得到
我们只需要一个人在这个范围里就行了
因为另一个人只要跟他距离不超过
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传送门
洛谷传送门
题意简述
题解
思路
一眼看上去很简单的样子,却又想不到什么妙方法
特性很多,不如先来研究一下答案的特征?
答案最长只有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
于是我们枚举答案为
接下来怎么办呢?
一个很巧妙的想法:DP!
虽然只是要求我们判断是否有满足条件的解
但我们可以来计数(虽然看似多此一举,但方便转移)
设
最大公因数为
只需从能被i整除的数中选出
这其中还包含最大公因数不为
于是——
其中
显然是从大往小dp
当然我们总不能从无穷大开始吧,要从
只要
注意事项
- 做着做着你就会发现组合数太大了……
所以要模一个大质数
比如
逆元直接打表噢
-
无需预处理
-
记得清空
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传送门
洛谷传送门
题意简述
一颗无穷个节点的完全二叉树,编号满足线段树分配,求有多少条树上的简单路径编号和为
数据范围
题解
思路
乍看没有什么思路……
仔细一想,一条路径只有两种可能,一条链或是一个分叉(顶点即为
设顶点为链上深度最小的点。如果确定了顶点为
- 一条链
设链的长度为
假设从
全走右边呢?
只有唯一解
证明:
当
x=x_0+1 时,(2^h-1)x = (2^h-1)(x_0+1) = s+h >s ,与(1) 矛盾当
x=x_0-1 时,(2^h-1)x_0 -h \leq s-h <s ,与(2) 矛盾
所以对于一个确定的深度
但并不是每个
设现在链上的所有节点都在左边,此时和为
选出一些节点变成右边
现在来考虑把一个左边的结点变成右边,这个点到链底的深度为
这样一个变换会带来
只要找一些
发现
直接从大到小贪心做即可
- 一个分叉
= 两条链
类似的,设顶点为
但是为了方便,这次从子节点
还是先都往左走
同理仍然可以得到当
这次我们要用
显然麻烦多了,因为凑出的方法有很多
来转换一下,假设要选出
可以数位dp!
做法
数位dp外面要枚举
这里的数位dp其实并不是传统的那种数位dp,只是借用了这个思想
顺序也不是从高往低,而是从低往高
选第
所以设
当然需要满足
最后的答案是
注意事项
这题细节也很多
-
计算2的幂不能直接用位运算(因为ll会溢出),需要预处理
-
预处理还需要多处理一位,因为后面会用到
-
计算
x 时要判断x 存不存在,不存在就break 掉 -
算分叉时要特判
ret==0 (不用转移),直接加进答案
为什么?我也不知道
-
只有当
ret 为偶数时才有解(代码中用ret + n 代替了) -
-
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);
}