一文彻底搞懂动态规划/动态规划模板(各类dp含完整例题集)

这里面很多的练习题都可以在牛客的题库上找到,大家在看到我的例题的时候最好还是自己先去想一下,去牛客网上面交一交,要保证自己能敲出来才是王道。
这里附上牛客网的题库链接:
牛客网题库
我下面的有些题目不一定完全对标链接的,但是大致是没有差别的。

待更新:状压DP和概率DP

彻底搞懂动态规划

介绍就不多说了,主要思考下面的问题:
哪些问题可以使用动态规划
1、最优子结构
2、子问题重叠
3、无后效性
实际上,满足1、3两点的时候就已经可以使用动态规划了。
动态规划秘籍
1、状态
2、阶段
3、决策
动态规划把原问题划分为若干子问题,每个子问题的求解过程构成一个阶段,求解完前一个阶段再求解后一个阶段。根据无后效性,动态规划的求解过程构成一个有向无环图,求解的顺序就是该有向无环图的一个拓扑排序。
解决动态规划的方法:
(1)确定状态
(2)划分阶段(状态转移方程)
(3)决策选择
(4)边界条件
(5)求解目标

线性DP

具有线性阶段划分的动态规划算法。若状态包含多个维度,则每个维度都是线性划分的阶段,也属于线性DP。

走楼梯

题目描述
楼梯一共有 n 阶,上楼可以一步上一阶,也可以一步上二阶。
请求出走到第 n 阶共有多少种不同的走法。

很简单的一个入门DP,直接给递推式dp[i]=dp[i-1]+dp[i-2]。

#include<bits/stdc++.h>
#define ll long long
using namespace std;
ll f[55];
inline void solve(){
    f[1]=1,f[2]=2;
    for(int i=3;i<=50;++i)f[i]=f[i-1]+f[i-2];
}
int main(){
    solve();
    int n;cin>>n;
    cout<<f[n]<<endl;
}

最长上升子序列

题目描述
给定一个长度为 n 的数组 a1,a2,…,an,问其中的最长上升子序列的长度。也就是说,我们要找到最大的 m 以及数组 p1,p2,…,pm,满足 1≤p1<p2<⋯<pm≤n 并且 ap1<ap2<⋯<apm。

朴素解法

定义一个dp数组,dp[i]表示以i为结尾的最长上升子序列的长度。初始时dp[i]都为1,我们接着进行两层循环,看a[i]是否比a[j]大(i>j),如果是,则dp[i]=max(dp[i],dp[j]+1);

#include<bits/stdc++.h>
using namespace std;
int dp[1005],a[1005];
int main(){
    int n;cin>>n;
    for(int i=1;i<=n;++i)scanf("%d",&a[i]),dp[i]=1;
    for(int i=2;i<=n;++i)
        for(int j=1;j<i;++j)
            if(a[i]>a[j])dp[i]=max(dp[i],dp[j]+1);
    int ans=0;
    for(int i=1;i<=n;++i)ans=max(ans,dp[i]);
    cout<<ans<<endl;
}

O(nlogn)解法

思考一个贪心的思维,我们单独开辟一个数组low来维护单调递增性。每次输入一个a值以后,在这个数组上二分查找大于等于a的第一个数的下标,如果找不到我们就直接在这个数组的最后加上它。如果找到了就把这个数替换了,因为我们要在最后找到最长递增子序列,你这个值替换得越来越小有利于最后寻找到最长的序列,符合贪心的思维。注意我们是要找大于等于它的第一个数,而不是大于它的第一个数。这是因为我们要找的是单调递增的子序列,这个low数组的维护也必须要满足严格的单调递增。比如原数组为2 3 4,新增一个2以后,如果找的不是大于等于它的而是大于它的,这个数组就会变成2 2 4,这根本不是我们想要维护的结果。
可以多思考一下,实际上,low[i]的值表示的是单调递增序列长度为i时的最后一个元素值。因此当i!=j时,low[i]!=low[j]是必然的。

代码如下:

#include<bits/stdc++.h>
using namespace std;
int low[1005],cnt;
int main(){
    int n;cin>>n;
    while(n--){
        int a;scanf("%d",&a);
        int id=lower_bound(low,low+cnt,a)-low;
        if(id<cnt)low[id]=a;
        else low[cnt++]=a;
    }
    cout<<cnt<<endl;
}

顺便提一下,如果我们要求解非严格递增子序列,就把lower_bound换成upper_bound。如果要求解严格递减子序列,就把lower_bound函数加上第四个参数greater< int >(),如果要求解非严格递减子序列就把upper_bound函数加上这第四个参数。
具体的原理和思维还是要自己去思考。前面那样说了,这就举一反三一下就好了。

最长公共子序列

题目描述
给定一个长度为 n 的数组 a1,a2,…,an 以及一个长度为 m 的数组 b1,b2,…,bm,问 a 和 b 的最长公共子序列的长度。
也就是说,我们要找到最大的 k 以及数组 p1,p2,…,pk,数组 l1,l2,…,lk 满足 1≤p1<p2<⋯<pk≤n 并且 1≤l1<l2<⋯<lk≤m 并且对于所有的 i(1≤i≤k) ,api=bli。

这道题其实只需要两层循环O(nm)的复杂度,定义一个f[i][j]表示a[0~i]和b[0~j]的最长公共子序列长度。
很明显,当遍历到a[i]!=b[j]时,f[i][j]=max(f[i][j-1],f[i-1][j-1])。当a[i]==b[j]时,f[i][j]=f[i-1][j-1]+1。

#include<bits/stdc++.h>
using namespace std;
int a[1005],b[1005],f[1005][1005];
int main(){
    int n,m;cin>>n>>m;
    for(int i=1;i<=n;++i)scanf("%d",&a[i]);
    for(int i=1;i<=m;++i)scanf("%d",&b[i]);
    for(int i=1;i<=n;++i)
        for(int j=1;j<=m;++j)
            if(a[i]==b[j])f[i][j]=f[i-1][j-1]+1;
            else f[i][j]=max(f[i-1][j],f[i][j-1]);
    cout<<f[n][m]<<endl;
}

最长回文子序列

题目描述
给定一个长度为 n 的数组 a1,a2,…,an,问其中的最长回文子串长度。
定义子串 al,al+1,…,ar 为回文子串,当且仅当这个子串正着看和反着看是一样的,即有 al=ar,al+1=ar−1,…。

这道题在我的博客上有,除了dp的做法还可以用双指针的方法,也很快。我觉得两种思维都要会,推荐可以去看我的那篇文章。这里就上个dp的代码:

#include<bits/stdc++.h>
using namespace std;
int a[1005],f[1005][1005],ans;
int main(){
    int n;cin>>n;
    for(int i=1;i<=n;++i)scanf("%d",&a[i]),f[i][i]=1;
    for(int i=2;i<=n;++i)
        if(a[i]==a[i-1])f[i-1][i]=1;
    for(int len=3;len<=n;++len)
        for(int i=1;i<=n-len+1;++i){
            int j=i+len-1;
            if(a[i]==a[j]&&f[i+1][j-1])f[i][j]=1,ans=max(ans,len);
        }
    cout<<ans<<endl;
}

背包系列

可以看看我的这篇文章:
背包问题
当然这是我很久以前写的背包问题,那时候还有很多问题有点蒙蒙的,也不会用单调队列来优化多重背包。现在我会了但是懒得写在那了,反正估计那篇文章也不会有很多人看哈哈哈。

这里就当你已经有了基础了
接下里的内容有些你就当模板吧

01背包

题目描述
有 n 种物品要放到一个袋子里,袋子的总容量为 m,第 i 种物品的体积为 vi,把它放进袋子里会获得 wi 的收益,每种物品至多能用一次,问如何选择物品,使得在物品的总体积不超过 m 的情况下,获得最大的收益?请求出最大收益。

一维优化代码如下:

#include<bits/stdc++.h>
using namespace std;
int f[1005];
int main(){
    int n,m;cin>>n>>m;
    for(int i=1;i<=n;++i){
        int v,w;scanf("%d%d",&v,&w);
        for(int j=m;j>=v;--j)f[j]=max(f[j],f[j-v]+w);
    }
    printf("%d\n",f[m]);
}

完全背包

题目描述
有 n 种物品要放到一个袋子里,袋子的总容量为 m,第 i 种物品的体积为 vi,把它放进袋子里会获得 wi 的收益,每种物品能用无限多次,问如何选择物品,使得在物品的总体积不超过 m 的情况下,获得最大的收益?请求出最大收益。

要注意的就是j变成正的循环了,毕竟一类物品可以复用,应当覆盖下去

代码如下:

#include<bits/stdc++.h>
using namespace std;
int f[1005];
int main(){
    int n,m;cin>>n>>m;
    for(int i=1;i<=n;++i){
        int v,w;scanf("%d%d",&v,&w);
        for(int j=v;j<=m;++j)f[j]=max(f[j],f[j-v]+w);
    }
    printf("%d\n",f[m]);
}

多重背包

题目描述
有 n 种物品要放到一个袋子里,袋子的总容量为 m,第 i 种物品的体积为 vi,把它放进袋子里会获得 wi 的收益,一共有 li 个。问如何选择物品,使得在物品的总体积不超过 m 的情况下,获得最大的收益?请求出最大收益。

朴素多重背包

其实也就是选择每种物品最多可选的数量,把每种物品每个物品当成单独的一个,把整个程序当做01背包来写就好了。

#include<bits/stdc++.h>
using namespace std;
int f[105];
int main(){
    int n,m;cin>>n>>m;
    for(int i=1;i<=n;++i){
        int v,w,l;scanf("%d%d%d",&v,&w,&l);
        int num=min(m/v,l);
        for(int j=1;j<=num;++j)
            for(int k=m;k>=v;--k)f[k]=max(f[k],f[k-v]+w);
    }
    printf("%d\n",f[m]);
}

二进制优化多重背包

这个其实也简单,考虑到我们已经知道了某种物品最多可选的数量,我们可以把这个数量进行不一样的分割。比如5,在之前的程序中我们看做是5个体积为v,效益为w的物品。但是在这个程序里面我们可以当做是4+1,也就是1个体积为4v,效益为4w以及1个体积为v,效益为w的两个物品。这是因为5的二进制为0101,这个泳衣实现,只需要单独开个数字k进行不断的左移。

#include<bits/stdc++.h>
using namespace std;
int f[2005];
int main(){
    int n,m;cin>>n>>m;
    for(int i=1;i<=n;++i){
        int v,w,l;scanf("%d%d%d",&v,&w,&l);
        int num=min(m/v,l);
        for(int k=1;num>0;k<<=1){
            //错误示范:if(k>num)num=k;可能会使得k过分大,装的物品数量就会受到影响
            //正确示范:
            if(k>num)k=num;
            num-=k;
            for(int j=m;j>=k*v;--j)f[j]=max(f[j],f[j-k*v]+k*w);
        }
    }
    cout<<f[m]<<endl;
}

单调队列优化的多重背包

朴素的方法复杂度O(n^3),而二进制优化的复杂度已经非常可观了,接近O(n^2)。但是还有一个更快的方法,也就是利用单调队列来优化,维护一下单调性会使得最后的复杂度贴近O(n),能处理的数大概能有2e6这么大!
当然,我们必须要明白什么是单调队列。

单调队列

单调队列,也就是具有单调性的队列,即队列中的元素保持单调递增或者单调递减。
单调队列的原型可以用滑动窗口来引入,给出洛谷的两道入门题:

扫描

题目描述
有一个1×n 的矩阵,有n个整数。
现在给你一个可以盖住连续k个数的木板。
一开始木板盖住了矩阵的第1∼k 个数,每次将木板向右移动一个单位,直到右端与第n个数重合。
每次移动前输出被覆盖住的数字中最大的数是多少。

考虑对于一个队列,我们需要维护两个地方:
(1)遍历到某个点以后有个下标i,那么这个单调队列的所有点都必须要保存好下标且满足i于这个下标的差小于k,如果大于等于k就把这个元素删除。毕竟我们要找某个元素(包括这个元素)之前的k个数的最大值。
(2)当我们输入一个value以后,我们可以在队列中判断队尾的大小是否比这个数小,如果比输入的数更小,则删除这个数,维护这个队列的单调性。
鉴于此,我们可以知道两个点:
(1)这个队列的维护分别在队头队尾都需要删除元素,这属于双端队列的领域。
(2)要维护需要保证元素有两个参数:下标和值。
那么deque天然适合这道题,当然要注意判断队列非空,防止运行错误。

deque实现
#include<bits/stdc++.h>
using namespace std;
struct node{
    int pos,value;
};
deque<node>q;
int main(){
    int n,k;cin>>n>>k;
    for(int i=1;i<=n;++i){
        int num;scanf("%d",&num);
        while(!q.empty()&&i-q.front().pos>=k)q.pop_front();
        while(!q.empty()&&q.back().value<num)q.pop_back();
        q.push_back({i,num});
        if(i>=k)printf("%d\n",q.front().value);
    }
}

其实deque的方法不错了,但是我在后面做完全背包问题的单调队列的时候我发现用deque会超时,这主要是因为pop和push操作还是会花上些许时间的,在数据量超大的时候并不可取。更好的方法当然是不用STL封装的方法了。而是自己写一个数组来模拟双端队列的头插、尾插和删除的操作。
这个操作也很简单容易理解。

用数组模拟双端队列实现
#include<bits/stdc++.h>
using namespace std;
struct node{
    int pos,value;
}q[2000005];
int main(){
    int n,k;cin>>n>>k;
    int head=0,tail=-1;
    for(int i=1;i<=n;++i){
        int num;scanf("%d",&num);
        while(head<=tail&&i-q[head].pos>=k)head++;
        while(head<=tail&&q[tail].value<num)--tail;
        q[++tail]={i,num};
        if(i>=k)printf("%d\n",q[head].value);
    }
}

再来一题稍微巩固一下,自己敲出来

滑动窗口

题目描述
有一个长为 n 的序列 a,以及一个大小为 k 的窗口。现在这个从左边开始向右滑动,每次滑动一个单位,求出每次滑动后窗口中的最大值和最小值。

这题我就给出deque的方法,这是之前刷的,最好能用数组模拟deque是最好的。

#include<bits/stdc++.h>
using namespace std;
struct node{
    int pos,value;
};
deque<node>minn,maxn;
int m[1000005];
int main(){
    int n,k,pos=0;cin>>n>>k;
    for(int i=1;i<=n;++i){
        int num;scanf("%d",&num);
        while(!minn.empty()&&i-minn.front().pos>=k)minn.pop_front();
        while(!minn.empty()&&num<minn.back().value)minn.pop_back();
        minn.push_back({i,num});
        while(!maxn.empty()&&i-maxn.front().pos>=k)maxn.pop_front();
        while(!maxn.empty()&&num>maxn.back().value)maxn.pop_back();
        maxn.push_back({i,num});
        if(i>=k){
            printf("%d%c",minn.front().value,i==n?'\n':' ');
            m[++pos]=maxn.front().value;
        }
    }
    for(int i=1;i<=pos;++i)printf("%d%c",m[i],i==pos?'\n':' ');
}
单调队列具体实现多重背包

多重背包的原始状态转移方程为
f[i][j]=max(f[i-1][j],f[i-1][j-v]+w,…,f[i-1][j-s*v]+sw)
继续推下去,直到背包的体积不能够被用为止
f[i][j-v]=max(f[i-1][j-v],f[i-1][j-2*v]+w,…,f[i-1][j-(s+1)v]+sw)
f[i][j-2v]=max(f[i-1][j-2v],f[i-1][j-3v]+w,…,f[i-1][j-(s+2)v]+sw)

f[i][r+sv]=max(f[i-1][r+sv],f[i-1][r+(s-1)v]+w,…,f[i-1][r]+sw)
f[i][r+(s-1)v]=max(f[i-1][r+(s-1)v],f[i-1][r+(s-2)v]+w,…,f[i-1][r]+(s-1)w)

f[i][r+2v]=max(f[i-1][r+2v],f[i-1][r+v]+w,f[i-1][r]+2w)
f[i][r+v]=max(f[i-1][r+v],f[i-1][r]+w)
f[i][r]=f[i-1][r]

其中r=j%vi,可以理解为取到不能再取之后剩下的余数,那么我们可以遍历这个余数,接着通过这个余数来遍历一下可以允许放置的物品数量。
我们会发现一个“滑动窗口求最大值”的模型,具体为:
f[i][r]=f[i-1][r]
f[i][r+v]=max(f[i-1][r+v],f[i-1][r]+w)
f[i][r+2v]=max(f[i-1][r+2v],f[i-1][r+v]+w,f[i-1][r]+2w)

f[i][r+(s-1)v]=max(f[i-1][r+(s-1)v],f[i-1][r+(s-2)v]+w,…,f[i-1][r]+(s-1)w)
f[i][r+sv]=max(f[i-1][r+sv],f[i-1][r+(s-1)v]+w,…,f[i-1][r]+sw) 滑动窗口已满,开始滑动
f[i][r+(s+1)v]=max(f[i-1][r+(s+1)v],f[i-1][r+sv]+w,…,f[i-1][r+v]+sw)滑动窗口已满

f[i][j-2v]=max(f[i-1][j-2v],f[i-1][j-3v]+w,…,f[i-1][j-(s+2)v]+sw)
f[i][j-v]=max(f[i-1][j-v]m,f[i-1][j-2v]+w,…,f[i-1][j-(s+1)v]+sw)
f[i][j]=max(f[i-1][j],f[i-1][j-v]+w,…,f[i-1][j-sv]+sw)
这就很像之前的窗口滑动,当容量为r时相当于之前的窗口长度不足k无法进行滑动!
于是通过该滑动窗口,我们就能在线性的时间里求出i阶段里,所有满足j%v=r的f[i][j]。
而滑动窗口求最大值的实现,只需利用队列在队头维护一个最大值的单调递减的单调队列即可。
为了更新所有i阶段里的状态f[i][j],我们只需要枚举完全部的合理的r即可
不要忘记滑动窗口内部比较最大值的时候,不是单独拿f数组的值来做比较,后面还加了某个倍数的w呢!
具体就是当前下标和该最大值下标之间差了x个v,那么就要加上x个w
滑动窗口的长度为s+1

二维朴素版

二维朴素版本在代码源里面只能过90%,10个样例里面超时了一个样例。这还是有点缺陷的,但是比较好理解。

#include<bits/stdc++.h>
using namespace std;
int f[20005][20005],q[20005];
int main(){
    int n,m;cin>>n>>m;
    for(int i=1;i<=n;++i){
        int v,w,l;scanf("%d%d%d",&v,&w,&l);
        for(int r=0;r<v;++r){//遍历余数,0~v-1
            int head=0,tail=-1;//记得初始一下
            for(int j=r;j<=m;j+=v){//满足j%v=r
            	//背包剩余容量为q[head],j-q[head]表示还能装物品的空间
            	//显然这个空间大于l*v绝对是不合理的,滑也滑不到那里去。。。
                while(head<=tail&&j-q[head]>l*v)head++;
                //f[i-1][j]比队尾的价值更大,就删尾
                //这个就相当于f[i-1][j]>f[i-1][j-k*v]+k*w
                //而q[tail]放置的就是j-k*v,也就是这个容量下的体积
                while(head<=tail&&f[i-1][q[tail]]+(j-q[tail])/v*w<f[i-1][j])--tail;
                q[++tail]=j;//更新容量
                f[i][j]=f[i-1][q[head]]+(j-q[head])/v*w;
            }
        }
    }
    cout<<f[n][m]<<endl;
}
一维优化

有关于体积的东西是正着遍历才符合窗口移动的原则,利用滚动数组会导致重复,不符合无后效性的特点。
虽然有些大佬还真的能有别的一些巧妙方法来让数组滚起来,但是我觉得拷贝数组是一样的意思。

#include<bits/stdc++.h>
using namespace std;
int f[20005],g[20005],q[20005];
int main(){
    int n,m;cin>>n>>m;
    for(int i=1;i<=n;++i){
        int v,w,l;scanf("%d%d%d",&v,&w,&l);
        memcpy(g,f,sizeof(f));
        for(int r=0;r<v;++r){
            int head=0,tail=-1;
            for(int j=r;j<=m;j+=v){
                while(head<=tail&&j-q[head]>l*v)head++;
                while(head<=tail&&g[q[tail]]+(j-q[tail])/v*w<=g[j])tail--;
                q[++tail]=j;
                f[j]=g[q[head]]+(j-q[head])/v*w;
            }
        }
    }
    cout<<f[m]<<endl;
}

分组背包

题目描述
有 n 种物品要放到一个袋子里,袋子的总容量为 m。第i 个物品属于第 ai 组,每组物品我们只能从中选择一个。第 i 种物品的体积为 vi,把它放进袋子里会获得 wi 的收益。问如何选择物品,使得在物品的总体积不超过 m 的情况下,获得最大的收益?请求出最大收益。

对于一个分组,我们直接可以将背包容量从后往前遍历,接着再将这个分组的所有物品拿出来做处理。错误的方式是先遍历这个分组的所有物品,再遍历背包容量。因为这样很可能会使得一个分组内有不止一个物品放入背包。如果先遍历背包容量,是不会出现覆盖的情况。

#include<bits/stdc++.h>
using namespace std;
struct node{
    int v,w;
};
int f[1005];
vector<node>a[1005];
int main(){
    int n,m;cin>>n>>m;
    for(int i=1;i<=n;++i){
        int x,v,w;scanf("%d%d%d",&x,&v,&w);
        a[x].push_back({v,w});
    }
    for(int i=1;i<=1000;++i){
        if(a[i].size()==0)continue;
        for(int j=m;j>=0;--j)
            for(int k=0;k<a[i].size();++k)
                if(j>=a[i][k].v)f[j]=max(f[j],f[j-a[i][k].v]+a[i][k].w);
        /*
        错误示范
        for(int k=0;k<a[i].size();++k)
            for(int j=m;j>=a[i][k].v;--j)
                f[j]=max(f[j],f[j-a[i][k].v]+a[i][k].w);
        */
    }
    printf("%d\n",f[m]);
}

二维背包

题目描述
有 n 种物品要放到一个袋子里,袋子的总容量为 m,我们一共有 k 点体力值。第 i 种物品的体积为 vi,把它放进袋子里会获得 wi 的收益,并且消耗我们 ti 点体力值,每种物品只能取一次。问如何选择物品,使得在物品的总体积不超过 m 并且花费总体力不超过 k 的情况下,获得最大的收益?请求出最大收益。

就是多了个代价的方式,照样用01背包的方式走就行。这类题型可能会以一种隐含的形式表示出来,比如题目在01背包的基础上说了最多选不超过多少的物品,我们就可以当做每选一个物品除了体积的代价,还有个数的代价,照样用二维背包的思想去解决就好了。

#include<bits/stdc++.h>
using namespace std;
int f[105][105];
int main(){
    int n,m,k;cin>>n>>m>>k;
    for(int i=1;i<=n;++i){
        int v,w,t;scanf("%d%d%d",&v,&w,&t);
        for(int j=m;j>=v;--j)
            for(int x=k;x>=t;--x)
                f[j][x]=max(f[j][x],f[j-v][x-t]+w);
    }
    printf("%d\n",f[m][k]);
}

混合背包

题目描述
有 n 种物品要放到一个袋子里,袋子的总容量为 m。
物品一共有 3 类,第 i 种物品属于第 ai 类,它的体积为 vi,把它放进袋子里会获得 wi 的收益。

如果它属于第 1 类物品,每种只能用一次。

如果它属于第 2 类物品,每种可以用无限多次。

如果它属于第 3 类物品,每种可以用 li 次。

问如何选择物品,使得在物品的总体积不超过 m 的情况下,获得最大的收益?请求出最大收益。

这道题没啥好说的,就是多种背包类型混合在一起而已,算个综合板子题吧。

#include<bits/stdc++.h>
using namespace std;
int f[1005],g[1005],q[1005];
int main(){
    int n,m;cin>>n>>m;
    for(int i=1;i<=n;++i){
        int a;scanf("%d",&a);
        if(a==1){
            int v,w;scanf("%d%d",&v,&w);
            for(int j=m;j>=v;--j)f[j]=max(f[j],f[j-v]+w);
        }
        if(a==2){
            int v,w;scanf("%d%d",&v,&w);
            for(int j=v;j<=m;++j)f[j]=max(f[j],f[j-v]+w);
        }
        if(a==3){
            int v,w,l;scanf("%d%d%d",&v,&w,&l);
            memcpy(g,f,sizeof(f));
        for(int r=0;r<v;++r){
            int head=0,tail=-1;
            for(int j=r;j<=m;j+=v){
                while(head<=tail&&j-q[head]>l*v)head++;
                while(head<=tail&&g[q[tail]]+(j-q[tail])/v*w<=g[j])tail--;
                q[++tail]=j;
                f[j]=g[q[head]]+(j-q[head])/v*w;
            }
        }
        }
    }
    cout<<f[m]<<endl;
}

区间DP

区间dp的秘籍就是i到j的这个区间的所期望求的值可能是i到k的值和k+1到j的值之和,很明显越大的区间要覆盖更小的区间所得到的值。
因此区间dp的第一层循环大多是遍历区间长度,接着循环起点(注意起点要合理),由循环得到的起点可以推出终点。最后再进行操作。

石子合并

题目描述
有 n 堆石子排成一排,第 i 堆石子有 ai 颗,每次我们可以选择相邻的两堆石子合并,代价是两堆石子数目的和,现在我们要一直合并这些石子,使得最后只剩下一堆石子,问总代价最少是多少?

首先遍历区间长度,接着遍历合理的起点,得到合理的终点,再遍历起点到终点的某些点,取得最小的代价。时间复杂度为O(n^3)。
当然了,f[i][j]除了要算上f[i][k]+f[k+1][j],还得算上最后一步i和j这两堆石头进行合并的代价。这种实现我们只需要利用前缀和就可以轻松实现。

#include<bits/stdc++.h>
using namespace std;
const int INF=1e9;
int f[505][505],sum[505];
int main(){
    int n;cin>>n;
    for(int i=1;i<=n;++i){
        int a;scanf("%d",&a);
        sum[i]=sum[i-1]+a;
    }
    for(int len=2;len<=n;++len)
    for(int i=1;i<=n-len+1;++i){
        int j=i+len-1;f[i][j]=INF;
        for(int k=i;k<j;++k)
        	f[i][j]=max(f[i][j],f[i][k]+f[k+1][j]+sum[j]-sum[i-1]);
    }
    cout<<f[1][n]<<endl;
}

石子合并2

题目描述
有 n 堆石子围成一个圈,第 i 堆石子有 ai 颗,每次我们可以选择相邻的两堆石子合并,代价是两堆石子数目的和,现在我们要一直合并这些石子,使得最后只剩下一堆石子,问总代价最少是多少?

这里就是思考一下,当遍历到某个区间长度的时候,某些点为起点可能会使得超过这n个数,接下来还得从头开始,这就是循环的过程。但是容易验证的是,既然区间长度最后一定不可能比n大,那我们把前n-1个元素继续放到第n个元素的后面,接下来的问题就变成式子合并1了。最后的答案我们只需要找区间长度为n的且代价最小的方案即可。

#include<bits/stdc++.h>
using namespace std;
const int INF=1e9;
int a[505],f[505][505],sum[505];
int main(){
    int n;cin>>n;
    for(int i=1;i<=n;++i)scanf("%d",&a[i]);
    for(int i=1;i<n;++i)a[i+n]=a[i];
    for(int i=1;i<=2*n-1;++i)sum[i]=sum[i-1]+a[i];
    for(int len=2;len<=2*n-1;++len)
    for(int i=1;i<=2*n-len;++i){
        int j=i+len-1;f[i][j]=INF;
        for(int k=i;k<j;++k)
        	f[i][j]=max(f[i][j],f[i][k]+f[k+1][j]+sum[j]-sum[i-1]);
    }
    int ans=INF;
    for(int i=1;i<=n;++i)ans=min(ans,f[i][i+n-1]);
    cout<<ans<<endl;
}

石子合并的四边形不等式优化

就以石子合并的第一道题为例吧。我们可以思考上面代码的步骤,首先可以确定的是区间长度是必然都要遍历过去的,无法进行优化;起点终点也是必然都考虑进去的,也无法优化,能够优化的就是k这个地方,实际上,我们每次k都从i到j-1这么遍历过去太浪费时间了,而之前的遍历过程中确立了一个k,那么其他的点不可能再成为最优的使得代价最小的中间点了。我们要做的就是定义一个s[i][j]来记录i到j的过程中使得合并代价最小的中间点k的位置。接着对于每次i和j,我们只需要遍历s[i][j-1]到s[i+1][j]的点即可,这样会使得复杂度降到接近O(n^2)。

#include<bits/stdc++.h>
using namespace std;
const int INF=1e9;
int f[505][505],s[505][505],sum[505];
int main(){
    int n;cin>>n;
    for(int i=1;i<=n;++i){
        int a;scanf("%d",&a);
        sum[i]=sum[i-1]+a;
        s[i][i]=i;
    }
    for(int len=2;len<=n;++len)
    for(int i=1;i<=n-len+1;++i){
        int j=i+len-1;f[i][j]=INF;
        for(int k=s[i][j-1];k<=s[i+1][j];++k){
            int temp=f[i][k]+f[k+1][j]+sum[j]-sum[i-1];
            if(temp<f[i][j])f[i][j]=temp,s[i][j]=k;
        }
    }
    cout<<f[1][n]<<endl;
}

括号序列

题目描述
给定一个长度为 n 的字符串 s,字符串由 (, ), [, ] 组成,问其中最长的合法子序列有多长?也就是说,我们要找到最大的 m,使得存在 i1,i2,…,im 满足 1≤i1<i2<⋯<im≤n 并且 si1si2…sim 是一个合法的括号序列。

合法的括号序列的定义是:

空串是一个合法的括号序列。

若 A 是一个合法的括号序列,则 (A), [A] 也是合法的括号序列。

若 A, B 都是合法的括号序列,则 AB 也是合法的括号序列。

这道题可以用堆栈来做,相信大家都会,当然也可以用dp来做。我们只需要开一个一维数组来记录从起点到这个点的括号序列的长度。那么遍历s的长度。对于s[i],如果我们保证s[i-1-f[i-1]]的位置和s[i]形成匹配关系,那么就可以进行加。而s[i-1-f[i-1]]之前的地方可能也会有合法的括号序列长度,当然要进行累加。

#include<bits/stdc++.h>
using namespace std;
int f[1000005];
string s;
int main(){
    int n;cin>>n;cin>>s;
    int ans=0;
    for(int i=0;i<s.size();++i){
        if(s[i]=='('||s[i]=='[')continue;
        else if(s[i-1-f[i-1]]=='('&&s[i]==')'||s[i-1-f[i-1]]=='['&&s[i]==']')
            f[i]=f[i-2-f[i-1]]+f[i-1]+2;
        ans=max(ans,f[i]);
    }
    cout<<ans<<endl;
}

序列删除

题目描述
有 n 个数字 a1,a2,…,an,我们要把除了 a1,an 之外的其他数字删除,删除一个数字的代价是它乘上它相邻两个还没有被删除的数字的值,请求出最小代价是多少。

和石子合并也差不多

#include<bits/stdc++.h>
using namespace std;
int a[505],f[505][505],s[505][505];
int main(){
    int n;cin>>n;
    for(int i=1;i<=n;++i)scanf("%d",&a[i]),s[i][i]=i;
    for(int len=3;len<=n;++len)
        for(int i=1;i<=n-len+1;++i){
            int j=i+len-1;f[i][j]=1e9;
            for(int k=i+1;k<=j-1;++k)
                f[i][j]=min(f[i][j],f[i][k]+f[k][j]+a[i]*a[k]*a[j]);
        }
    printf("%d\n",f[1][n]);
}

树形DP

树形DP指在树型结构上实现的动态规划。动态规划是多阶段决策问题,而树形结构又有明显的层次性,正好对应动态规划的多个阶段。
树形DP一般自底向上,将子树从小到大作为DP的阶段,将结点编号作为DP状态的第一维,代表以该节点为根的子树。
树形DP一般采用深度优先遍历,递归求解每颗子树,回溯时从子结点向上进行状态转移。在当前结点的所有子树都求解完毕后,才可以求解当前结点。

逐层递进,先入门再精通。

统计人数

题目描述
一家公司里有 n 个员工,除了公司 CEO 外,每个人都有一个直接上司。我们想知道,每个人的团队(包括他/她自己、他的直接下属和间接下属)一共有多少人?

很简单,就是普通的从根结点往下,接着底层回溯的过程,我们只是用一个数组来进行记录罢了,算是一种动态规划的原型了吧!我这里存图的方法用的是邻接表。

#include<bits/stdc++.h>
using namespace std;
vector<int>e[100005];
int dp[100005];
inline int dfs(int u){
    if(dp[u])return dp[u];
    if(!e[u].size()){dp[u]=1;return dp[u];}
    for(int i=0;i<e[u].size();++i){
        int v=e[u][i];
        //cout<<v<<endl;
        dp[u]+=dfs(v);
    }
    dp[u]++;
    return dp[u];
}
int main(){
    int n,root;cin>>n;
    for(int i=2;i<=n;++i)scanf("%d",&root),e[root].push_back(i);
    dfs(1);
    for(int i=1;i<=n;++i)printf("%d%c",dp[i],i==n?'\n':' ');
}

没有上司的舞会

题目描述
一家公司里有 n 个员工,除了公司 CEO 外,每个人都有一个直接上司。今天公司要办一个舞会,为了大家玩得尽兴,如果某个员工的直接上司来了,他/她就不想来了。第 i 个员工来参加舞会会为大家带来 ai 点快乐值。请求出快乐值最大是多少。

这题深度递归dfs下去我们想要的自然是孩子的最大快乐值之和。拿这题和上题进行比较,其实也就是多了个限制:员工和直接上司只能来一个。我们就只需要加上一个限制。刷多了动态规划的人其实很快就能反应过来了,在dp数组上加一维,0表示该结点不参加,1表示该结点参加聚会,然后通过各个点之间的关系进行决策。自底向上,很符合动态规划的思维,具体看代码:

#include<bits/stdc++.h>
#define ll long long
using namespace std;
vector<int>e[100005];
ll dp[100005][2];
inline void dfs(int u){
    if(!e[u].size())return;
    for(int i=0;i<e[u].size();++i){
        int v=e[u][i];
        dfs(v);
        //当u结点不选的时候,v可选可不选,不要以为v肯定选,画图就知道!
        dp[u][0]+=max(dp[v][0],dp[v][1]);
        //u选了v绝对不会再选
        dp[u][1]+=dp[v][0];
    }
}
int main(){
    int n,a;cin>>n;
    for(int i=2;i<=n;++i)scanf("%d",&a),e[a].push_back(i);
    for(int i=1;i<=n;++i)scanf("%lld",&dp[i][1]);
    dfs(1);//先自顶向下递归,到底后再自底向上回溯
    printf("%lld\n",max(dp[1][0],dp[1][1]));//CEO也是可以不去的
}

没有上司的舞会2

题目描述
一家公司里有 n 个员工,除了公司 CEO 外,每个人都有一个直接上司。今天公司要办一个舞会,为了大家玩得尽兴,如果某个员工的直接上司来了,他/她就不想来了。第 i 个员工来参加舞会会为大家带来 ai 点快乐值。由于场地有大小限制,场地最多只能容纳 m 个人。请求出快乐值最大是多少。

比起之前又多了个限制,场地能收下的总人数不能超过m,那么就在dp数组上再开一维,表示某个结点的人数,那么我们的dp[i][j][k]中,i表示结点编号,j表示这个结点的孩子中的参与人数,k表示这个结点是否去参与聚会,为0就没参加,为1就是参加了。
可以容易的直到对于一个j,可能其孩子结点也就是下属去了k个(k<j),那么可能他的下属中的最多只能去j-k个(全都是包括结点本身的)。而j我们应当要从后往前遍历,防止重复。

#include<bits/stdc++.h>
using namespace std;
vector<int>e[505];
int n,m;
int dp[505][505][2];
inline void dfs(int u){
    int len=e[u].size();
    for(int i=0;i<len;++i){
        int v=e[u][i];
        dfs(v);
        for(int j=m;j>=0;--j)
            for(int k=0;k<=j;++k)
                dp[u][j][1]=max(dp[u][j][1],dp[u][k][1]+dp[v][j-k][0]),
                dp[u][j][0]=max(dp[u][j][0],dp[u][k][0]+max(dp[v][j-k][0],dp[v][j-k][1]));
    }
}
int main(){
    cin>>n>>m;
    for(int i=2;i<=n;++i){
        int root;scanf("%d",&root);
        e[root].push_back(i);
    }
    for(int i=1;i<=n;++i)scanf("%d",&dp[i][1][1]);
    dfs(1);
    printf("%d\n",max(dp[1][m][1],dp[1][m][0]));
}

开会

题目描述
一家公司里有 n 个员工,除了公司 CEO 外,每个人都有一个直接上司。公司现在要召开全员大会。为了充分传递会议信息,每个员工和他/她的直接上司至少得有一个人参加会议。由于场地限制等原因,现在我们想使得参会人数最少。请问最少需要几个人参会?

这道题就会比较简单了,只需要注意当一个结点被选了之后,其直接子结点可以选也可以不选。

#include<bits/stdc++.h>
using namespace std;
vector<int>e[100005];
int dp[100005][2];
inline void dfs(int u){
    dp[u][1]=1;
    for(int i=0;i<e[u].size();++i){
        int v=e[u][i];
        dfs(v);
        dp[u][1]+=min(dp[v][0],dp[v][1]);
        dp[u][0]+=dp[v][1];
    }
}
int main(){
    int n;cin>>n;
    for(int i=2;i<=n;++i){
        int root;scanf("%d",&root);
        e[root].push_back(i);
    }
    dfs(1);
    printf("%d\n",min(dp[1][0],dp[1][1]));
}

换根DP(也算是树形的DP)

对于之前的树形DP,都是以一个固定的结点也就是1来作为根求解最优解d额动态规划思想。而换根DP,顾名思义就是根不一定只有1,这涉及到换根的操作。最笨的方法是以每个点作为根分别进行之前的树形DP操作,但是实际上这样的操作基本上都是会超时的。正确的方法是只求解一个根,并且你要看出来当选择其他点作为根(当然这个点肯定是先从原先被求解的那个根的孩子)时,和父亲的关系。这里也设计到了递归的操作,不断地递归找孩子,并且通过某种关系得到对应解。

距离和

题目描述
有一棵 n 个点的树,请求出每个点到其他所有点的距离的和。
定义两个点的距离为它们的简单路径上经过了多少条边。

这题如果不会换根操作,那么就只会对每个点分别作为一次根结点来做dfs操作必然超时。正确的方法是要思考父亲和孩子之间的关系。
这道题画出来的以1为根的树如下:

在这里插入图片描述

我们先求解1为根的到其他所有点的距离之和。显然dp[u]等于u的所有子结点的dp[v]之和,dp[1]的求解就可以很简单,只需要遍历到底,根据深度逐次叠加即可得到dp[1]=5。而在这个树上,容易发现,1到其他所有点的距离之和和2到其他所有点的距离之和有关系。观察一下,1和2是父子结点关系,拿1和2比较,容易发现,2这个点对dp[1]的贡献相当于2的所有子结点(包括2结点本身)+1,那么在1和2之间可以知道以2为根的时候首先拥有dp[1]-son[2]的距离和,这里计算的出来的距离和实际上就是以2为根的树中,2的直接孩子所带来的贡献。除了这里,我们还需要计算其他点对2这个点的贡献。只需要n-son[2],就可以计算出有多少点不是2的直接孩子,刚好就是对2的贡献。这个地方不是很好讲,我觉得自己多画几个图会更好理解,比如在5结点的后面多加几个孩子等等。(其实也就是n加上k(比如5多上k个孩子)以后,肯定到2的距离就会加上k)

#include<bits/stdc++.h>
#define ll long long
using namespace std;
vector<int>e[100005];
ll dp[100005],son[100005];
int n;
bool vis[100005];
inline void dfs(int u,int depth){
    vis[u]=true;
    son[u]=1;dp[u]=depth;
    for(int i=0;i<e[u].size();++i){
        int v=e[u][i];
        if(vis[v])continue;
        dfs(v,depth+1);
        dp[u]+=dp[v];son[u]+=son[v];
    }
}
inline void find(int x){
    vis[x]=true;
    for(int i=0;i<e[x].size();++i){
        int y=e[x][i];
        if(vis[y])continue;
        dp[y]=dp[x]-son[y]+(n-son[y]);
        find(y);
    }
}
int main(){
    cin>>n;
    for(int i=1;i<n;++i){
        int u,v;scanf("%d%d",&u,&v);
        e[u].push_back(v);e[v].push_back(u);
    }
    dfs(1,0);
    memset(vis,false,sizeof(vis));find(1);
    for(int i=1;i<=n;++i)printf("%lld\n",dp[i]);
}

最长路径

题目描述
有一棵 n 个点的树,请求出经过每个点的长度最长的简单路径有多长。

定义一条路径的长度为这条路径上经过了多少条边。

这道题。。。不知道该说些什么了,太难了我只能说,如果我不搜题解是做不出来的,但是我觉得还是分享出来比较好。毕竟我感觉理解起来起码不太难,至于其中的思维,就让大家自己思考好了。能看到这里肯定思考完这道题不是问题。

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
vector<int>e[N];
int f[N][2][2];
//一维表示每个节点
//二维中0表示最长,1表示次长
//三维中0表示该路径的长度,1表示其前驱结点
int n;
int dis[N];
//记录结点y的父亲x作为其子树时从里面连上来的到y的最长路径
bool vis[N];//记录结点
inline void dfs(int u){
    vis[u]=true;
    for(int i=0;i<e[u].size();++i){
        int v=e[u][i];
        if(vis[v])continue;
        dfs(v);
        if(f[v][0][0]+1>f[u][0][0]){
            //比原先的最长长度要更长
            f[u][1][0]=f[u][0][0];//原先的最长变成次长
            f[u][1][1]=f[u][0][1];//原先最长长度到终点的前驱结点变成次长长度到终点的前驱结点
            f[u][0][0]=f[v][0][0]+1;//更新最长
            f[u][0][1]=v;//u(终点)的前驱结点为v
        }
        else if(f[v][0][0]+1>f[u][1][0]){
            //比原先的次长长度要更长
            f[u][1][0]=f[v][0][0]+1;//更新长度
            f[u][1][1]=v;//更新前驱结点
        }
    }
}
inline void change_root(int u){
    vis[u]=true;
    for(int i=0;i<e[u].size();++i){
        int v=e[u][i];
        if(vis[v])continue;
        if(f[u][0][1]==v)
            dis[v]=max(dis[u],f[u][1][0])+1;
        else
            dis[v]=max(dis[u],f[u][0][0])+1;
        change_root(v);
    }
}
int main(){
    cin>>n;
    for(int i=1;i<n;++i){
        int u,v;scanf("%d%d",&u,&v);
        e[u].push_back(v);e[v].push_back(u);
    }
    dfs(1);
    //printf("%d %d %d %d\n",f[1][0][0],f[1][0][1],f[1][1][0],f[1][1][1]);
    memset(vis,false,sizeof(vis));change_root(1);
    for(int i=1;i<=n;++i)
        printf("%d\n",f[i][0][0]+f[i][1][0]+dis[i]-min(min(f[i][0][0],f[i][1][0]),dis[i]));
}

状压DP

数位DP

数位DP是与数位相关的一类计数类DP,一般用于统计[l,r]区间满足特定条件的元素个数。数位指的是个位、十位、百位、千位等,数位DP就是在数位上进行动态规划。数位DP实质上是一种有策略的穷举方式,在子问题求解完毕后将其结果记忆化就可以了。
如何枚举?
枚举[0,386]区间的所有数时,首先从百位开始枚举,百位可能是0、1、2、3。枚举时不超过386即可。
百位为0、1、2时,十位的枚举没有限制,在0~9之间;当百位枚举到3时,十位只能枚举0~8,同理,当前两位枚举成38以后,个位只能枚举0~6,否则无限制。

数位DP需要注意的几个问题
(1)记忆化
枚举无限制时,可以记忆化;有限制时,不可以记忆化,需要继续根据限制进行枚举。

枚举[0,386]区间的所有数,当百位为0、1、2时,十位和个位都没有限制,都是一样的,采用记忆化递归,只需计算一次并将结果存储起来,下次判断若已赋值,则直接返回该值即可。百位是3时,十位限制在0到8,;十位是0到7时,个位无限制;十位是8时,个位限制在0到6。

(2)上界限制
当高位枚举刚好达到上界时,紧接着的下一位枚举就有上界限制了。可以设置一个变量limit标记是否有上界限制。

(3)高位枚举0
为啥高位要枚举0?因为百位枚举0相当于此时枚举的这个数最多两位,这样是可行的,而且必然需要考虑在内。

(4)前导0
有时会有前导0的问题,可以设置一个lead变量表示有前导0。例如统计数字里面0出现的次数。若有前导0,例如008,数字8不包含0,则不应该统计8前面的两个0。若没有前导0,例如108,则应该统计8前面的1个0。

通过这样的思想,我们可以引入这么一个递归的思路,通过limit查看遍历是否有限制,给出经典题目:不要62

递归实现树形DP

不要62

题目描述
不吉利的数字为所有含有4或62的号码。例如:62315,73418,88914都属于不吉利号码。但是,61152虽然含有6和2,但不是62连号,所以不属于不吉利数字之列。
对于每次给出的一个牌照区间号,推断出交管局今后又要实际上给多少辆新的士车上牌照了。

我们首先将数位全部提取出来,再进行递归,递归代码中的参数必须要有pos表示当前遍历的下标(这里是从后往前遍历),limit表示是否有限制。而在这题中,4的判断很简单,只要是4就跳过即可。但是62的判断就没那么简单了,因此需要再给一个参数pre表示之前一层的递归是否是6,以此来判断62即可。

#include<bits/stdc++.h>
using namespace std;
int a[25],cnt;
int dp[25][2];
inline int dfs(int pos,int pre,int state,bool limit){
    if(!pos)return 1;
    if(!limit&&dp[pos][state]!=-1)return dp[pos][state];
    int up=limit?a[pos]:9;
    int temp=0;
    for(int i=0;i<=up;++i){
        if(pre==6&&i==2)continue;
        if(i==4)continue;
        temp+=dfs(pos-1,i,i==6,limit&&i==a[pos]);
    }
    if(!limit)dp[pos][state]=temp;
    return temp;
}
inline int solve(int x){
    cnt=0;
    while(x)a[++cnt]=x%10,x/=10;
    return dfs(cnt,-1,0,true);
}
int main(){
    int l,r;
    while(~scanf("%d%d",&l,&r)){
        if(!l&&!r)return 0;
        memset(dp,-1,sizeof(dp));
        printf("%d\n",solve(r)-solve(l-1));
    }
}

非递归实现数位DP

我一直觉得只要非递归的代码不长,那么他就是比递归要好用的,毕竟用非递归的方式看起来没有那么的复杂,而且在数据量比较大的情况下,非递归一般也不会出现爆栈的情况。非递归实现的核心是要心中有树。

数字游戏

题目描述
科协里最近很流行数字游戏。某人命名了一种不降数,这种数字必须满足从左到右各位数字成小于等于的关系,如123,446。现在大家决定玩一个游戏,指定一个整数闭区间[a,b],问这个区间内有多少个不降数。

想象这棵树,从最高位开始枚举的时候并没有什么限制,枚举完以后要开始逐渐枚举低位的位置,但是需要保证后一位的值要大于等于前一位的值。
树的根枚举an作为枚举的最高位,左子树枚举0~an-1,左子树这样枚举后后面的数字不再有限制,而右子树则会有限制,接着继续遍历低位。
需要注意的是,当我们遍历到没有限制的时候,实际上满足一种关系。
我们先思考对于枚举的数字所拥有的性质,那就是枚举了几位数,以及最高位是哪一位。定义一个f[i][j]表示枚举长度为i的数字满足题目条件的且最高位为j的个数。容易知道f[i][j]=f[i-1][j]+f[i-1][j+1]+…+f[i-1][9]。

代码如下:

//数字游戏
#include<bits/stdc++.h>
using namespace std;
const int N=15;
int f[N][N];//f[i][j]表示i位的数字里面最高位位j的不降数的个数
//递推公式为f[i][j]=f[i-1][j]+f[i-1][j+1]+...+f[i-1][9]
inline void init(){
    for(int i=0;i<=9;++i)f[1][i]=1;
    for(int i=2;i<N;++i)
        for(int j=0;j<=9;++j)
            for(int k=j;k<=9;++k)f[i][j]+=f[i-1][k];
}
inline int dp(int n){
    if(!n)return 1;
    vector<int>nums;
    while(n)nums.push_back(n%10),n/=10;
    int res=0,last=0;
    for(int i=nums.size()-1;i>=0;--i){
        int x=nums[i];
        for(int j=last;j<x;++j)res+=f[i+1][j];
        if(x<last)break;
        last=x;
        if(!i)res++;//能走到这一步就相当于到达了树的最右子结点
    }
    return res;
}
int main(){
    init();
    int l,r;
    while(~scanf("%d%d",&l,&r))printf("%d\n",dp(r)-dp(l-1));
}

Windy数(对前导0的处理)

题目描述
Windy定义了一种Windy数:不含前导零且相邻两个数字之差至少为2的正整数被称为Windy数。
Windy想知道,在A和B之间,包括A和B,总共有多少个Windy数?

这道题和之前的题目有些许不一样,首先前导0是一种特殊的情况,虽然这里题目说不包含前导0,实际上假如我们要枚举的数的范围为1-100,那么1这样的数字应该被枚举,但是9不应该被看做是09或者009,不然0和9的差满足了Windy数,但是9实际上不应该算作是Windy数。因此我们需要对前导0进行特殊的判断,即直接从1开始枚举i,逐次加上f[i][j]即可。当然j肯定不为0,这里当做的是第i个数为j,前面的高位的数字都是前导0的情况。
这里提一下,f[i][j]表示枚举i位的数的时候,最高位为j的情况数。

//Windy数
#include<bits/stdc++.h>
using namespace std;
const int N=25;
int f[N][N];
inline void init(){
    for(int i=0;i<=9;++i)f[1][i]=1;
    for(int i=2;i<N;++i)
        for(int j=0;j<=9;++j)
            for(int k=0;k<=9;++k)
                if(abs(j-k)>=2)f[i][j]+=f[i-1][k];
}
inline int dp(int n){
    if(!n)return 0;
    vector<int>nums;
    while(n)nums.push_back(n%10),n/=10;
    int res=0,last=-2;
    //毕竟差距要是2以上,last设置成和所有会枚举的数字差距至少为2的数
    for(int i=nums.size()-1;i>=0;--i){
        int x=nums[i];
        //如果枚举的是第一位,那么j从1开始枚举,否则从0开始枚举
        for(int j=i==nums.size()-1;j<x;++j)
            if(abs(j-last)>=2)res+=f[i+1][j];
        if(abs(x-last)>=2)last=x;
        else break;
        if(!i)res++;
    }
    //特殊处理一下有前导0的数
    for(int i=1;i<nums.size();++i)
        for(int j=1;j<=9;++j)res+=f[i][j];
    return res;
}
int main(){
    init();
    int l,r;cin>>l>>r;
    printf("%d\n",dp(r)-dp(l-1));
}

数字游戏2(一道难题)

题目描述
由于科协里最近真的很流行数字游戏,某人又命名了一种取模数,这种数字必须满足各位数字之和modN为0。现在大家又要玩游戏了,指定一个整数闭区间[a,b],问这个区间内有多少个取模数。

这道题真的有够难的,数位DP难在哪里?实际上树状的结构啥的并不难理解,往往最难的就是初始化。因为初始化既不能出错,而且还要保证所有的因素都考虑在内,且要尽量使得状态方程好转移。
说实话,状态方程的转移思路的思考路程根本说不清楚,感觉只能多刷题涨涨经验吧!
这题就是要考虑以下三个元素:枚举位数、最高位、以及mod N的一个余数。为什么这么想呢?
首先可以确定的是,枚举的位数和最高位是必须要有的,需要思考的问题主要是为啥要保存一个mod N的余数。试想一下,我们的一个数字枚举成了an-1 x …表示最高位an-1已经枚举完毕,若这个数字想要满足题目要求,即[an-1 + x +(…) ]mod N=0(小括号的内容表示x后面的各种位数之和),显然(an-1 +x)%N+(…)%N=0,那么(…)%N的值就为[-(an-1 +x )]%N,这时候我们就会发现,想要最后的各个位数之和满足题目要求,显然会有多种情况,那么我们只需要假设an-1 + x 的值为k,我们只需要满足-k%N,把方案数全部枚举完就好。

代码如下:

//数字游戏2
#include<bits/stdc++.h>
using namespace std;
int f[15][15][105];
//f[i][j][k]表示i位的数字,最高位为j的时候,且j位之后的各位数字之和mod N的值为k的时候的个数
//f[i][j][k]=f[i-1][x][(k-j)%N](x的范围0~9)
int P;//取模数
inline int mod(int x,int y){
    return (x%y+y)%y;//得到正余数
}
inline void init(){
    memset(f,0,sizeof(f));
    for(int i=0;i<=9;++i)f[1][i][i%P]++;
    for(int i=2;i<15;++i)
        for(int j=0;j<=9;++j)
            for(int k=0;k<P;++k)//对于一个余数k
                for(int x=0;x<=9;++x)//最高位定下来,开始枚举次高位
                	//这里的取余要注意一下,可能k-j的值是负的,但是实际上这种情况不存在
                	//因此我们自己写一个函数来计算得到正余数
                    f[i][j][k]+=f[i-1][x][mod(k-j,P)];
     //这里的所有的可能出现的余数都要考虑过去,毕竟我们要通过某个余数k来算-k mod P
}
inline int dp(int n){
    if(!n)return 1;
    vector<int>nums;
    while(n)nums.push_back(n%10),n/=10;
    int res=0,last=0;
    for(int i=nums.size()-1;i>=0;--i){
        int x=nums[i];
        for(int j=0;j<x;++j)res+=f[i+1][j][mod(-last,P)];
        last+=x;
        if(!i&&last%P==0)res++;
    }
    return res;
}
int main(){
    int l,r;
    while(~scanf("%d%d%d",&l,&r,&P)){
        init();
        printf("%d\n",dp(r)-dp(l-1));
    }
}

不要62

和上面的一道用递归写的题目是一样的,这里使用了非递归的使用方式。注意这里是可以不用像windy数那样考虑前导0对结果的影响的,直接把前导0一起遍历就好了。
我这里一开始是像windy数那样解题,一开始wa了,是因为我只考虑了一开始的0就返回1,但是漏了对于别的数,0也是一种情况,所以一开始res要变成1才能AC。(如果我自己不那么多事就好咯)

考虑前导0

#include<bits/stdc++.h>
using namespace std;
int f[10][10];
inline void init(){
    for(int i=0;i<=9;++i)
        if(i==4)continue;
        else f[1][i]=1;
    for(int i=2;i<=8;++i)
        for(int j=0;j<=9;++j)
            if(j==4)continue;
            else{
                for(int k=0;k<=9;++k)
                    if(j==6&&k==2||k==4)continue;
                    else f[i][j]+=f[i-1][k];
            }
}
inline int dp(int n){
    if(!n)return 1;
    vector<int>nums;
    while(n)nums.push_back(n%10),n/=10;
    int res=1,last=0;
    for(int i=nums.size()-1;i>=0;--i){
        int x=nums[i];
        for(int j=i==nums.size()-1;j<x;++j)
            if(j==4||last==6&&j==2)continue;
            else res+=f[i+1][j];
        if(x==4||last==6&&x==2)break;
        last=x;
        if(!i)res++;
    }
    for(int i=1;i<nums.size();++i)
        for(int j=1;j<=9;++j)res+=f[i][j];
    return res;
}
int main(){
    init();int l,r;
    while(cin>>l>>r,l||r){
        printf("%d\n",dp(r)-dp(l-1));
    }
}

不考虑前导0

#include<bits/stdc++.h>
using namespace std;
int f[10][10];
inline void init(){
    for(int i=0;i<=9;++i)
        if(i==4)continue;
        else f[1][i]=1;
    for(int i=2;i<=8;++i)
        for(int j=0;j<=9;++j)
            if(j==4)continue;
            else{
                for(int k=0;k<=9;++k)
                    if(j==6&&k==2||k==4)continue;
                    else f[i][j]+=f[i-1][k];
            }
}
inline int dp(int n){
    if(!n)return 1;
    vector<int>nums;
    while(n)nums.push_back(n%10),n/=10;
    int res=0,last=0;
    for(int i=nums.size()-1;i>=0;--i){
        int x=nums[i];
        for(int j=0;j<x;++j)
            if(j==4||last==6&&j==2)continue;
            else res+=f[i+1][j];
        if(x==4||last==6&&x==2)break;
        last=x;
        if(!i)res++;
    }
    return res;
}
int main(){
    init();int l,r;
    while(cin>>l>>r,l||r){
        printf("%d\n",dp(r)-dp(l-1));
    }
}

数数

题目描述
请问 [l,r] 中有多少个数字满足数字中任意相邻两个数位的差的绝对值不超过 2。

需要考虑前导0,最后单独考虑如果有前导0的情况怎么办即可

#include<bits/stdc++.h>
#define ll long long
using namespace std;
ll f[20][15];
inline void init(){
    for(int i=0;i<=9;++i)f[1][i]=1;
    for(int i=2;i<=17;++i)
        for(int j=0;j<=9;++j)
            for(int k=0;k<=9;++k)
                if(abs(j-k)<=2)f[i][j]+=f[i-1][k];
}
inline ll dp(ll n){
    if(!n)return 0;
    if(n/10==0)return n;
    vector<int>nums;
    while(n)nums.push_back(n%10),n/=10;
    ll res=0;int last=nums[nums.size()-1];
    for(int i=1;i<last;++i)res+=f[nums.size()][i];
    for(int i=nums.size()-2;i>=0;--i){
        int x=nums[i];
        for(int j=0;j<x;++j)
            if(abs(last-j)<=2)res+=f[i+1][j];
        if(abs(last-x)<=2)last=x;
        else break;
        if(!i)res++;
    }
    for(int i=1;i<nums.size();++i)
        for(int j=1;j<=9;++j)res+=f[i][j];
    return res;
}
int main(){
    init();
    ll l,r;cin>>l>>r;
    printf("%lld\n",dp(r)-dp(l-1));
}

概率DP

  • 12
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

布布要成为最负责的男人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值