关于DP,它死了

一般的DP的步骤

  1. 定义状态:找通解
    1. 关注题目需要的东西
    2. 状态确定,输出就确定
    3. 将状态的准确描述记录下来
  2. 寻找状态转移方程:描述一个子问题如何用更小的子问题得到
  3. 确定边界条件:最小的子问题或不满足状态转移方程的状态

一些小小的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算法是专门解决石子合并问题,好像是线性复杂度。

算法流程大致如下:

  1. 寻找最小的满足 \(a_{k-1}\leqslant a_{k+1}\)\(k\) ,将 \(a_{k}\)\(a_{k-1}\) 合并。

  2. \(k\) 向前寻找第一个满足 \(a_j\gt a_k+a_{k-1}\)\(j\) ,将 \(a_k+a_{k-1}\) 插入在 \(a_j\) 后面。

  3. 当只剩下一个数时,那个数就是答案。

区间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}\)

则有三种排除的情况:

  1. 判断当前状态有没有种在贫瘠的土地上

    设当前状态为 \(i\),而土地状态为 \(j\),若 (i&j)!=i 则有冲突。

  2. 判断当前状态有没有相邻两个

    当前状态为 \(i\),若 (i&(i<<1))!=0 则有冲突。

  3. 判断当前状态有没有和上一个状态重复

    当前状态为 \(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. 求出 \([1,R]\)\([1,L-1]\) 中满足条件的数的个数,像前缀和一样相减
  2. 从高位到低位枚举每个数位 \(i\),并统计以不超过 \(a_i\) 开头的满足条件的整数的数量,\(a\) 为原数
  3. 预处理 \(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\) 是更优解。

\[dp_j+m+(sum_i-sum_j)\times(sum_i-sum_j)\leqslant dp_k+m+(sum_i-sum_k)\times(sum_i-sum_k) \]

\[dp_j+{sum_i}^2-2\times sum_i\times sum_j+{sum_j}^2\leqslant dp_k+{sum_i}^2-2\times sum_i\times sum_k+{sum_k}^2 \]

\[dp_j+{sum_j}^2-dp_k-{sum_k}^2\leqslant 2\times sum_i\times(sum_j-sum_k) \]

\[\dfrac{dp_j+{sum_j}^2-dp_k-{sum_k}^2}{sum_j-sum_k}\leqslant 2\times sum_i \]

现在定义一个斜率为 \(\dfrac{Y_j-Y_k}{X_j-X_k}\)

所以

\[\dfrac{Y_j-Y_k}{X_j-X_k}\leqslant 2\times sum_i \]

只要满足这个条件,\(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\) 次。

\[dp_{j,l-1}+sum_i\times sum_j-{sum_j}^2\geqslant dp_{k,l-1}+sum_i\times sum_k-{sum_k}^2 \]

\[dp_{j,l-1}-{sum_j}^2-dp_{k,l-1}+{sum_k}^2\geqslant sum_i\times sum_k-sum_i\times sum_j \]

\[\dfrac{dp_{j,l-1}-{sum_j}^2-dp_{k,l-1}+{sum_k}^2}{sum_k-sum_j}\geqslant sum_i \]

直接写就可以了

代码实现:

#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

  1. 从前往后划分,会导致最后可能有干草不能堆到塔上,所以从后往前划分
  2. \(dp_i\) 表示从塔顶到第 \(i\) 堆干草堆出的最大高度
  3. \(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]
  1. 要使得 \(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;
}
posted @ 2022-04-30 08:35  yzh_Error404  阅读(26)  评论(1)    收藏  举报