关于DP,它死了
一般的DP的步骤
- 定义状态:找通解
- 关注题目需要的东西
- 状态确定,输出就确定
- 将状态的准确描述记录下来
- 寻找状态转移方程:描述一个子问题如何用更小的子问题得到
- 确定边界条件:最小的子问题或不满足状态转移方程的状态
一些小小的DP
例1. 摆花
先看一眼题面,容易设计状态。
设 \(dp_{i,j}\) 表示前 \(i\) 种花一共摆了 \(j\) 盆时的种类数。
然后让每一位加上前面可以达到的位的答案。
答案即为 \(dp_{n,m}\) 。
代码实现:
int main()
{
scanf("%d%d",&n,&m);
for(register int i=1;i<=n;i++)
scanf("%d",&a[i]);
dp[0][0]=1;
for(register int i=1;i<=n;i++)
for(register int j=0;j<=m;j++)
for(register int k=0;k<=a[i];k++)
if(j>=k)dp[i][j]=(dp[i][j]+dp[i-1][j-k])%MOD;//需要保证摆的花不超过j盆且不超过限制a[i]
printf("%d",dp[n][m]);
return 0;
}
例2. 木棍加工
一眼可以看出是求下降子序列的个数。
根据某个著名的定理,下降子序列的个数等于最长上升子序列的长度。
根据一维排序再求最长上升子序列长度即可。
代码实现:
struct node
{
int l,r;
}e[MAXN];
int n;
inline bool cmp(node x,node y)
{
if(x.l==y.l)return x.r>y.r;
return x.l>y.l;
}
int dp[MAXN];
int main()
{
scanf("%d",&n);
for(register int i=1;i<=n;i++)
scanf("%d%d",&e[i].l,&e[i].r);
sort(e+1,e+1+n,cmp);
for(register int i=1;i<=n;i++)
{
for(register int j=1;j<i;j++)
if(e[j].r<e[i].r)dp[i]=max(dp[i],dp[j]+1);
}
int maxn=-0x7f7f7f7f;
for(register int i=1;i<=n;i++)
maxn=max(maxn,dp[i]);
printf("%d",maxn+1);
return 0;
}
例3. 书本整理
不整齐度和剩下的书有关,所以状态和剩下的书有关。
设 \(dp_{i,j}\) 表示前 \(i\) 本书中留下了 \(j\) 本且第 \(i\) 本一定选的最小不整齐度。
寻找每一个可以与当前遍历到的 \(i\) 匹配的 \(p\) 即可。
代码实现:
int main()
{
scanf("%d%d",&n,&k);
for(register int i=1;i<=n;i++)
scanf("%d%d",&e[i].l,&e[i].r);
sort(e+1,e+1+n,cmp);
for(register int i=1;i<=n;i++)
for(register int j=1;j<=n-k;j++)
dp[i][j]=0x7f7f7f7f;
for(register int i=1;i<=n;i++)
dp[i][0]=dp[i][1]=0;
for(register int i=1;i<=n;i++)
for(register int j=1;j<=min(i,n-k);j++)
for(register int p=j-1;p<=i-1;p++)
dp[i][j]=min(dp[i][j],dp[p][j-1]+abs(e[i].r-e[p].r));
int minn=0x7f7f7f7f;
for(register int i=n-k;i<=n;i++)
minn=min(minn,dp[i][n-k]);
printf("%d",minn);
return 0;
}
例4. Modulo Sum
直接搞 DP 是 \(O(nm)\) 的,肯定会炸。
考虑一个小优化:当 \(n\ge m\) 时必定有解。
我们假设 \(dp_{i,j}\) 表示考虑在前 \(i\) 个数中选数,是否可能使得它们的和除以 \(m\) 的余数为 \(j\) ,初始状态 \(dp_{i,a_{i}}=1\),枚举每个数和余数进行转移即可。
代码实现:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e3+5;
int n,m;
bool dp[MAXN][MAXN];
int a[MAXN];
int main()
{
scanf("%d%d",&n,&m);
if(n>m)
{
puts("YES");
return 0;
}
for(register int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
a[i]%=m;
dp[i][a[i]]=1;
if(!a[i])
{
puts("YES");
return 0;
}
}
for(register int i=1;i<=n;i++)
{
for(register int j=0;j<m;j++)
{
dp[i][j]|=dp[i-1][j];
dp[i][(j+a[i])%m]|=dp[i-1][j];
}
if(dp[i][0])
{
puts("YES");
return 0;
}
}
puts("NO");
return 0;
}
现在有一点图的变化。
例6. 挖地雷
有很多方法,比如暴搜。
可能第一印象是拓扑+DP
手玩可以发现,一定是从编号小的地窖跑到编号较大的地窖。
所以二维循环即可。
代码实现:
区间DP
一般是求一个区间内的最大值,最小值,方案数。
判别:
-
从不同位置开始递推得到的结果可能不一样。
-
合并类或拆分类。
区间DP一般有三个循环:
-
第一个循环一般是枚举阶段(子问题)
-
第二个循环枚举所有的状态(情形)
-
第三个循环枚举决策点(从哪里转移)
例1. 石子合并(加强版)
区间DP/GarsiaWachs算法板子题
GarsiaWachs算法是专门解决石子合并问题,好像是线性复杂度。
算法流程大致如下:
-
寻找最小的满足 \(a_{k-1}\leqslant a_{k+1}\) 的 \(k\) ,将 \(a_{k}\) 与 \(a_{k-1}\) 合并。
-
从 \(k\) 向前寻找第一个满足 \(a_j\gt a_k+a_{k-1}\) 的 \(j\) ,将 \(a_k+a_{k-1}\) 插入在 \(a_j\) 后面。
-
当只剩下一个数时,那个数就是答案。
区间DP:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=305;
int n,val[MAXN],sum[MAXN];
int dp[MAXN][MAXN];
int main()
{
scanf("%d",&n);
memset(dp,0x7f,sizeof dp);
for(register int i=1;i<=n;i++)
{
scanf("%d",&val[i]);
sum[i]=sum[i-1]+val[i];
dp[i][i]=0;
}
for(register int len=2;len<=n;len++)
for(register int i=1;i+len-1<=n;i++)
{
int j=i+len-1;
for(register int k=i;k<j;k++)
dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]+sum[j]-sum[i-1]);
}
printf("%d",dp[1][n]);
return 0;
}
GarsiaWachs:
#include<bits/stdc++.h>
#define int long long
using namespace std;
int ans,n;
vector<int>v;
inline int merge()
{
int k=v.size()-2;
for(register int i=0;i<v.size()-2;i++)
if(v[i]<=v[i+2])
{
k=i;
break;
}
int now=v[k]+v[k+1];
v.erase(v.begin()+k);
v.erase(v.begin()+k);
int wh=-1;
for(register int i=k-1;i>=0;i--)
if(v[i]>now)
{
wh=i;
break;
}
v.insert(v.begin()+wh+1,now);
return now;
}
signed main()
{
scanf("%lld",&n);
for(register int i=1;i<=n;i++)
{
int op;
scanf("%lld",&op);
v.push_back(op);
}
for(register int i=1;i<n;i++)
ans+=merge();
printf("%lld",ans);
return 0;
}
例2. 248 G
还是区间DP板子。
只要两边相等,就可以将两边合并。
代码实现:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=305;
int n,val[MAXN];
int dp[MAXN][MAXN];
int main()
{
scanf("%d",&n);
memset(dp,-0x7f,sizeof dp);
for(register int i=1;i<=n;i++)
{
scanf("%d",&val[i]);
dp[i][i]=val[i];
}
for(register int len=2;len<=n;len++)
for(register int i=1;i+len-1<=n;i++)
{
int j=i+len-1;
for(register int k=i;k<j;k++)
if(dp[i][k]==dp[k+1][j])dp[i][j]=max(dp[i][j],dp[i][k]+1);
}
int maxn=0;
for(register int i=1;i<=n;i++)
for(register int j=i;j<=n;j++)
maxn=max(maxn,dp[i][j]);
printf("%d",maxn);
return 0;
}
例3. Cheapest Palindrome G
对于一个区间,强制它要是一个回文串。
所以对于每一个区间 \([i,j]\) ,有以下转移:
\(dp_{i,j}=\min\begin{cases}dp_{i+1,j}+\min(ins_i,del_i)\\dp_{i,j-1}+\min(ins_j,del_j)\\\text{if }a_i=a_j~~~~~~~~~~dp_{i+1,j-1}\end{cases}\)
代码实现:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=2005;
int n,m;
string a;
int dp[MAXN][MAXN];
map<char,int>ins,del;
signed main()
{
scanf("%d%d",&n,&m);
cin>>a;
a=' '+a;
memset(dp,0x7f,sizeof dp);
for(register int i=1;i<=n;i++)
{
char op;
int in,de;
cin>>op>>in>>de;
ins[op]=in;
del[op]=de;
}
for(register int i=1;i<=m;i++)
dp[i][i]=0;
for(register int len=2;len<=m;len++)
for(register int i=1;i+len-1<=m;i++)
{
int j=i+len-1;
if(a[i]==a[j])
{
if(len==2)dp[i][j]=0;
else dp[i][j]=min(dp[i][j],dp[i+1][j-1]);
}
dp[i][j]=min(dp[i][j],dp[i][j-1]+ins[a[j]]);
dp[i][j]=min(dp[i][j],dp[i][j-1]+del[a[j]]);
dp[i][j]=min(dp[i][j],dp[i+1][j]+ins[a[i]]);
dp[i][j]=min(dp[i][j],dp[i+1][j]+del[a[i]]);
}
printf("%d",dp[1][m]);
return 0;
}
例4. 合唱队
首先套路设 \(dp_{i,j}\) 为区间 \([i,j]\) 的方案数。
然后发现状态设计有问题。
所以再加一维表示最后一个是从哪边进来的。
有一个小坑点:只有一个人时从左边右边进来都一样,只要初始化一边即可。
代码实现:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=2005;
const int MOD=19650827;
int n;
int a[MAXN];
int dp[MAXN][MAXN][2];
int main()
{
scanf("%d",&n);
for(register int i=1;i<=n;i++)
scanf("%d",&a[i]);
for(register int i=1;i<=n;i++)
dp[i][i][0]=1;
for(register int len=2;len<=n;len++)
for(register int i=1;i+len-1<=n;i++)
{
int j=i+len-1;
if(a[i]<a[i+1])dp[i][j][0]=(dp[i][j][0]+dp[i+1][j][0])%MOD;
if(a[i]<a[j])dp[i][j][0]=(dp[i][j][0]+dp[i+1][j][1])%MOD;
if(a[j]>a[i])dp[i][j][1]=(dp[i][j][1]+dp[i][j-1][0])%MOD;
if(a[j]>a[j-1])dp[i][j][1]=(dp[i][j][1]+dp[i][j-1][1])%MOD;
}
printf("%d",(dp[1][n][0]+dp[1][n][1])%MOD);
return 0;
}
例5. 关路灯
有了上一题的经验,一上来就可以设 \(dp_{i,j,0/1}\) 表示在区间 \([i,j]\) 内最后一个是 \(i/j\) 的最小代价。
转移也十分套路,按照思路模拟即可。
需要注意的是初始化。
因为给定了初始位置为 \(c\) 。
所以 \(dp_{i,i}=|a_c-a_i|\times (sum_n-w_c)\)
代码实现:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=55;
int n,c;
int a[MAXN],w[MAXN];
int sum[MAXN];
int dp[MAXN][MAXN][2];
int main()
{
scanf("%d%d",&n,&c);
for(register int i=1;i<=n;i++)
{
scanf("%d%d",&a[i],&w[i]);
sum[i]=sum[i-1]+w[i];
}
for(register int i=1;i<=n;i++)
dp[i][i][0]=dp[i][i][1]=abs(a[i]-a[c])*(sum[n]-w[c]);
for(register int len=2;len<=n;len++)
for(register int i=1;i+len-1<=n;i++)
{
int j=i+len-1;
dp[i][j][0]=min(dp[i+1][j][0]+(a[i+1]-a[i])*(sum[n]+sum[i]-sum[j]),dp[i+1][j][1]+(a[j]-a[i])*(sum[n]+sum[i]-sum[j]));
dp[i][j][1]=min(dp[i][j-1][0]+(a[j]-a[i])*(sum[n]+sum[i-1]-sum[j-1]),dp[i][j-1][1]+(a[j]-a[j-1])*(sum[n]+sum[i-1]-sum[j-1]));
}
printf("%d",min(dp[1][n][0],dp[1][n][1]));
return 0;
}
换根DP
对于一类树形DP,若节点不确定,且答案会随着根节点的不同而变换,这种树形DP可以称之为换根DP。
例1. STA-Station
步骤1. 定义 \(dp_i\) 表示以 \(i\) 为根节点的子树的最大深度和,然后任意指定节点跑一遍树形DP。
步骤2. 定义 \(f_i\) 表示以 \(i\) 为全局根节点时的最大深度和,然后以 \(rt\) 再跑一遍树形DP。
步骤3. 在 \(f_1\) 到 \(f_n\) 中取最大值即为答案。
代码实现:
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=2e6+5;
struct node
{
int to,nxt;
}e[MAXN];
int head[MAXN],cnt;
inline void add(int x,int y)
{
e[++cnt].to=y;
e[cnt].nxt=head[x];
head[x]=cnt;
}
int n;
int f[MAXN],dep[MAXN],siz[MAXN];
inline void dfs1(int x,int fa)
{
dep[x]=dep[fa]+1;
siz[x]=1;
for(register int i=head[x];i;i=e[i].nxt)
{
int y=e[i].to;
if(y==fa)continue;
dfs1(y,x);
siz[x]+=siz[y];
}
}
inline void dfs2(int x,int fa)
{
for(register int i=head[x];i;i=e[i].nxt)
{
int y=e[i].to;
if(y==fa)continue;
f[y]=f[x]-siz[y]+(n-siz[y]);
dfs2(y,x);
}
}
signed main()
{
scanf("%lld",&n);
for(register int i=1;i<n;i++)
{
int x,y;
scanf("%lld%lld",&x,&y);
add(x,y);
add(y,x);
}
dfs1(1,0);
for(register int i=1;i<=n;i++)
f[1]+=dep[i];
dfs2(1,0);
int maxn=-0x7f7f7f7f,id;
for(register int i=1;i<=n;i++)
if(f[i]>maxn)
{
maxn=f[i];
id=i;
}
printf("%lld",id);
return 0;
}
例2. Great Cow Gathering G
差不多,也是板子。
代码实现:
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=4e5+5;
struct node
{
int to,nxt,len;
}e[MAXN];
int head[MAXN],cnt;
inline void add(int x,int y,int z)
{
e[++cnt].to=y;
e[cnt].len=z;
e[cnt].nxt=head[x];
head[x]=cnt;
}
int n,sum;
int val[MAXN];
int dp[MAXN],f[MAXN],siz[MAXN];
inline void dfs1(int x,int fa)
{
siz[x]=val[x];
for(register int i=head[x];i;i=e[i].nxt)
{
int y=e[i].to,z=e[i].len;
if(y==fa)continue;
dfs1(y,x,i);
siz[x]+=siz[y];
dp[x]=dp[x]+dp[y]+siz[y]*z;
}
}
inline void dfs2(int x,int fa)
{
for(register int i=head[x];i;i=e[i].nxt)
{
int y=e[i].to,z=e[i].len;
if(y==fa)continue;
f[y]=f[x]-siz[y]*z+(sum-siz[y])*z;
dfs2(y,x);
}
}
signed main()
{
scanf("%lld",&n);
for(register int i=1;i<=n;i++)
{
scanf("%lld",&val[i]);
sum+=val[i];
}
for(register int i=1;i<n;i++)
{
int x,y,z;
scanf("%lld%lld%lld",&x,&y,&z);
add(x,y,z);
add(y,x,z);
}
dfs1(1,0,0);
f[1]=dp[1];
// cout<<f[1]<<endl;
dfs2(1,0);
int minn=0x7f7f7f7f7f7f;
for(register int i=1;i<=n;i++)
minn=min(minn,f[i]);
printf("%lld",minn);
return 0;
}
状压DP
定义:当状态的维度很多,而每一个维度的取值是 bool 值时,则可以用二进制数值去表示一个状态,这种DP被称为状压DP
基本的位运算:
按位与( & ):两个整数在二进制下逐位比较,同一位有 \(2\) 个 \(1\) ,则结果为 \(1\) ,否则为 \(0\) 。
按位或( | ):两个整数在二进制下逐位比较,同一位有 \(1\) ,则结果为 \(1\) ,否则为 \(0\) 。
按位异或( ^ ): 两个整数在二进制下逐位比较,同一位不同则为 \(1\) ,否则为 \(0\) 。
按位取反( ~ ):字面意思。
常见位操作意义:
-
一个二进制数位
&1得到本身 -
一个二进制数位
^1取反 -
一个二进制数位
&0则赋值为 \(0\) -
一个二进制数位
|1则赋值为 \(1\) -
(n>>k)&1取出二进制下 \(n\) 的第 \(k\) 位(从右往左) -
n&((1<<k)-1)取出二进制下 \(n\) 的右 \(k\) 位 -
n^(1<<k)将二进制下的第 \(k\) 位取反 -
n|(1<<k)将二进制下的第 \(k\) 位赋值 \(1\) -
n&(~(1<<k))将二进制下 \(n\) 的第 \(k\) 位赋值 \(0\)
例1. 海贼王之伟大航路
其实是状压DP板子
设 \(dp_{i,j}\) 为当前走过的岛的状态为 \(i\) ,现在在第 \(j\) 个岛时的最小代价。
这里的 \(i\) 是一个二进制数,大概等同于一个 \(vis\) 数组。
状态转移方程还是比较容易推的。
代码实现:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=17;
int n;
int dp[1<<MAXN][MAXN];
int a[MAXN][MAXN];
int main()
{
scanf("%d",&n);
for(register int i=1;i<=n;i++)
for(register int j=1;j<=n;j++)
scanf("%d",&a[i][j]);
memset(dp,0x7f,sizeof dp);
dp[1][1]=0;//只经过1时的最小代价当然是0
for(register int i=1;i<=(1<<n)-1;i++)//遍历所有的状态
for(register int j=1;j<=n;j++)//看现在到了哪一个点
{
if(!((i>>(j-1))&1))continue;//如果状态中没有经过这一个点,就排除掉
for(register int k=1;k<=n;k++)
if(((i>>(k-1))&1))dp[i][j]=min(dp[i][j],dp[i^(1<<(j-1))][k]+a[k][j]);//现在看这个点是从哪个点过来的,上一个状态显然是将i的第j为改为0,
}
printf("%d",dp[(1<<n)-1][n]);
return 0;
}
例2. 售货员的难题
和上一题差不多,但是卡空间。
题目要求最后回到原点,最后加个距离即可。
卡空间卡的很严,下标均要从 \(1\) 开始。
代码实现:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=20;
int n;
int dp[1<<MAXN][MAXN];
int a[MAXN][MAXN];
int main()
{
scanf("%d",&n);
for(register int i=0;i<n;i++)
for(register int j=0;j<n;j++)
scanf("%d",&a[i][j]);
memset(dp,0x7f,sizeof dp);
dp[1][0]=0;
for(register int i=0;i<(1<<n);i++)
for(register int j=0;j<n;j++)
{
if(!((i>>j)&1))continue;
for(register int k=0;k<n;k++)
if(((i>>k)&1))dp[i][j]=min(dp[i][j],dp[i^(1<<j)][k]+a[k][j]);
}
int minn=0x7f7f7f7f;
for(register int i=0;i<n;i++)
minn=min(minn,dp[(1<<n)-1][i]+a[i][0]);
printf("%d",minn);
return 0;
}
例3. Corn Fields G
设 \(dp_{i,j}\) 表示前 \(i\) 行且第 \(i\) 行的种地状态为 \(j\) 的方案数。
答案即为 \(\sum\limits_{i\lt n}^{i=0} dp_{m,i}\)
则有三种排除的情况:
-
判断当前状态有没有种在贫瘠的土地上
设当前状态为 \(i\),而土地状态为 \(j\),若
(i&j)!=i则有冲突。 -
判断当前状态有没有相邻两个
当前状态为 \(i\),若
(i&(i<<1))!=0则有冲突。 -
判断当前状态有没有和上一个状态重复
当前状态为 \(i\),上一个状态为 \(j\) ,若
(i&j)!=0则有冲突。
代码实现:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=12;
const int MOD=1e9;
int m,n;
int a[MAXN+5];
int dp[MAXN+5][1<<MAXN];
int main()
{
scanf("%d%d",&m,&n);
for(register int i=1;i<=m;i++)
for(register int j=1;j<=n;j++)
{
int op;
scanf("%d",&op);
a[i]=(a[i]<<1)+op;
}
dp[0][0]=1;
for(register int i=1;i<=m;i++)
for(register int j=0;j<(1<<n);j++)
{
if((j&a[i])!=j)continue;
if((j&(j<<1))!=0)continue;
for(register int k=0;k<(1<<n);k++)
if((j&k)==0)dp[i][j]=(dp[i][j]+dp[i-1][k])%MOD;
}
int ans=0;
for(register int i=0;i<(1<<n);i++)
ans=(ans+dp[m][i])%MOD;
printf("%d",ans);
return 0;
}
数位DP
解决计数的问题。
基础问法:求区间 \([L,R]\) 中满足要求的数有多少个。
解决策略:
- 求出 \([1,R]\) 和 \([1,L-1]\) 中满足条件的数的个数,像前缀和一样相减
- 从高位到低位枚举每个数位 \(i\),并统计以不超过 \(a_i\) 开头的满足条件的整数的数量,\(a\) 为原数
- 预处理 \(dp_{i,j}\) 表示 \(i\) 位数以 \(j\) 开头的满足条件的整数个数
for(register int i=1;i<=cnt;i++)
for(register int j=0;j<=9;j++)
for(register int k=0;k<=9;k++)
if(j!=4&&!(j==6&&k==2))dp[i][j]+=dp[i-1][k];
例1. 不要62
基础数位DP题。
代码实现:
#include<cstdio>
#include<cstring>
using namespace std;
const int MAXN=10;
int n,m;
int dp[MAXN][MAXN];
int a[MAXN];
inline int ask(int x)
{
memset(a,0,sizeof a);
int cnt=0,ans=0;
while(x)//将x分开放入a中
{
a[++cnt]=x%10;
x/=10;
}
for(register int i=cnt;i>=1;i--)//枚举每一个数位i
{
for(register int j=0;j<a[i];j++)//当前数位可以选择放小于a[i]的任意一个数,或者是默认放a[i]
if(j!=4&&!(a[i+1]==6&&j==2))ans+=dp[i][j];//满足条件
if(a[i]==4||(a[i+1]==6&&a[i]==2))break;//如果当前已经出现了不满足条件的数位,就直接不再枚举下去
}
return ans;
}
int main()
{
dp[0][0]=1;
for(register int i=1;i<=MAXN;i++)
for(register int j=0;j<=9;j++)
for(register int k=0;k<=9;k++)//初始化,i为数位,j为当前位放的数,k为后一位放的数
if(j!=4&&!(j==6&&k==2))dp[i][j]+=dp[i-1][k];
while(scanf("%d%d",&n,&m))
{
if(!n&&!m)break;
printf("%d\n",ask(m+1)-ask(n));//ask(i)表示[1,i-1]中满足条件的数的个数
}
return 0;
}
例2. beautiful number
现在不能含前导 \(0\),所以首位要分开讨论。
代码实现:
#include<cstdio>
#include<cstring>
using namespace std;
const int MAXN=13;
int dp[MAXN][MAXN];
int a[MAXN];
inline int ask(int x)
{
memset(a,0,sizeof a);
int cnt=0,ans=0;
while(x)
{
a[++cnt]=x%10;
x/=10;
}
for(register int i=1;i<a[cnt];i++)//首位小于a[cnt],一定可以
ans+=dp[cnt][i];
for(register int i=1;i<cnt;i++)
for(register int j=1;j<=9;j++)//枚举所有位数不为cnt的满足条件的数
ans+=dp[i][j];
for(register int i=cnt-1;i>=1;i--)//枚举所有不为首位的数位
{
for(register int j=1;j<a[i];j++)//枚举当前数位可以放的数
if(a[i+1]!=0&&a[i+1]%j==0)ans+=dp[i][j];//如果满足条件(需要满足上一位不为0)就累加答案
if(a[i]==0||a[i+1]%a[i]!=0)break;
}
return ans;
}
int t,n,m;
int main()
{
for(register int i=1;i<=9;i++)
dp[1][i]=1;
for(register int i=2;i<=10;i++)
for(register int j=1;j<=9;j++)
for(register int k=1;k<=j;k++)
if(j%k==0)dp[i][j]+=dp[i-1][k];
scanf("%d",&t);
while(t--)
{
scanf("%d%d",&n,&m);
printf("%d\n",ask(m+1)-ask(n));
}
return 0;
}
斜率优化DP
状态转移方程的特征: \(dp_i=\min(dp_i,A\times dp_j+i与j的乘积项+C)\)
例1. Print Article
将一个序列分成连续的若干段,每段的代价为此段数的和的平方加 \(m\),求代价最小。
设当前到了第 \(i\) 个数,接下来有一些决策点可以前往。
实现步骤:
假设有两个决策点 \(k\) 与 \(j\),且满足 \(k\lt j\),\(j\) 是更优解。
现在定义一个斜率为 \(\dfrac{Y_j-Y_k}{X_j-X_k}\)
所以
只要满足这个条件,\(j\) 就是两者中的更优解,\(k\) 就可以润了。
代码实现:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=5e5+5;
int n,m;
int c[MAXN],sum[MAXN];
int dp[MAXN];
int q[MAXN],l,r;
inline int up(int j,int k)
{
return dp[j]+sum[j]*sum[j]-dp[k]-sum[k]*sum[k];
}
inline int down(int j,int k)
{
return sum[j]-sum[k];
}
int main()
{
while(scanf("%d%d",&n,&m)!=EOF)
{
l=1,r=0;
memset(dp,0,sizeof dp);
memset(q,0,sizeof q);
for(register int i=1;i<=n;i++)
{
scanf("%d",&c[i]);
sum[i]=sum[i-1]+c[i];
}
q[++r]=0;
for(register int i=1;i<=n;i++)
{
while(l+1<=r&&up(q[l+1],q[l])<=2*sum[i]*down(q[l+1],q[l]))l++;//如果q[l+1]比q[l]要优,就弹出q[l]
int j=q[l];//现在的q[l]一定是当前最优的决策点
dp[i]=dp[j]+m+(sum[i]-sum[j])*(sum[i]-sum[j]);
while(l+1<=r&&up(i,q[r])*down(q[r],q[r-1])<=up(q[r],q[r-1])*down(i,q[r]))r--;//需要满足队列中相邻两个元素之间的斜率是递增的,所以当斜率要小一些的时候,就将q[r]弹出
q[++r]=i;
}
printf("%d\n",dp[n]);
}
return 0;
}
例2. 序列分割
\(dp_{i,j}\) 表示到 \(i\) 时刚好切 \(j\) 次的最大得分。
\(j\) 表示上一个切到的点,\(k\) 表示切的次数。
\(dp_{i,k}=dp_{j,k-1}+sum_j\times(sum_i-sum_j)=dp_{j,k-1}+sum_i\times sum_j-{sum_j}^2\)
现在有 \(j\) 和 \(k\) 两个决策点,且 \(j\) 更优,切了 \(l\) 次。
直接写就可以了
代码实现:
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=1e5+5;
int dp[MAXN][205];
int n,k;
int a[MAXN],sum[MAXN];
int q[MAXN],L,R;
int pre[MAXN][205];
inline int up(int j,int k,int l)
{
return dp[j][l-1]-sum[j]*sum[j]-dp[k][l-1]+sum[k]*sum[k];
}
inline int down(int j,int k,int l)
{
return sum[k]-sum[j];
}
signed main()
{
scanf("%lld%lld",&n,&k);
for(register int i=1;i<=n;i++)
{
scanf("%lld",&a[i]);
sum[i]=sum[i-1]+a[i];
}
for(register int l=1;l<=k;l++)
{
L=1,R=0;
q[++R]=0;
for(register int i=1;i<=n;i++)
{
while(L+1<=R&&up(q[L+1],q[L],l)>=sum[i]*down(q[L+1],q[L],l))L++;
int j=q[L];
dp[i][l]=dp[j][l-1]+sum[i]*sum[j]-sum[j]*sum[j];
pre[i][l]=j;
while(L+1<=R&&up(i,q[R],l)*down(q[R],q[R-1],l)<=up(q[R],q[R-1],l)*down(i,q[R],l))R--;
q[++R]=i;
}
}
printf("%lld\n",dp[n][k]);
for(register int i=n,x=k;x>=1;x--)
{
i=pre[i][x];
printf("%lld ",i);
}
return 0;
}
单调队列优化DP
单调队列优化DP的一般形式:
\(dp_i=\min(dp_i,dp_j+A\times a_i+B\times a_j+C)\)
例1. Tower of Hay G
- 从前往后划分,会导致最后可能有干草不能堆到塔上,所以从后往前划分
- \(dp_i\) 表示从塔顶到第 \(i\) 堆干草堆出的最大高度
- \(w_i\) 表示前 \(i\) 堆草堆堆出的最大高度为 \(dp_i\) 时,最后一层的干草的宽度
if(sum[i]-sum[j]>=w[j])
dp[i]=max(dp[i],dp[j]+1)
w[i]=sum[i]-sum[j]
- 要使得 \(dp_i\) 尽量的大,等价于要使得 \(w_i\) 尽量的小,等价于 \(sum_j\) 要尽量的大
代码实现:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e5+5;
int n;
int a[MAXN],w[MAXN],sum[MAXN];
int dp[MAXN];
int q[MAXN],l,r;
inline int calc(int x)
{
return w[x]+sum[x];
}
int main()
{
scanf("%d",&n);
for(register int i=n;i>=1;i--)
scanf("%d",&a[i]);
for(register int i=1;i<=n;i++)
sum[i]=sum[i-1]+a[i];
l=1,r=0;
for(register int i=1;i<=n;i++)
{
while(l<=r&&sum[i]>=calc(q[l]))l++;
int j=q[l-1];
w[i]=sum[i]-sum[j];
dp[i]=dp[j]+1;
while(l<=r&&calc(i)<=calc(q[r]))r--;
q[++r]=i;
}
printf("%d",dp[n]);
return 0;
}

浙公网安备 33010602011771号