动态规划做题笔记
线性 DP
[NOIP1999 提高组] 导弹拦截
题目链接。
第一问求最长不上升子序列,第二问可以考虑贪心,从左到右依次枚举每个导弹。假设现在有若干个导弹拦截系统可以拦截它,那么我们肯定选择这些系统当中位置最低的那一个。如果不存在任何一个导弹拦截系统可以拦截它,那我们只能新加一个系统了,这个过程等价于求最长上升子序列。
\(O(n^2)\) 的算法如下:
状态:\(f_{i}\) 表示以 \(a_i\) 结尾的最长上升子序列的长度。
阶段:子序列的结尾在原序列中的位置。
转移:\(f_{i}= \underset{0 \le j < i,a_{i} < a_{j}}{\max}\{f_{j}+1\}\)。
边界:\(f_{0}=0\)
目标:\(\underset{1 \le i \le n}{max}\)
求最长不下降子序列的方法与之类似。
代码如下:
#include<bits/stdc++.h>
using namespace std;
int a[100005],t,n,f[100005],ans;
int main(){
while(cin>>t){
a[++n]=t;
f[n]=1;
}
for(int i=1;i<=n;i++){
for(int j=0;j<i;j++){
if(a[j]>=a[i]){
f[i]=max(f[i],f[j]+1);
}
}
}
for(int i=1;i<=n;i++){
ans=max(ans,f[i]);
}
cout<<ans<<endl;
ans=0;
memset(f,0,sizeof(f));
for(int i=1;i<=n;i++){
for(int j=0;j<i;j++){
if(a[i]>a[j]){
f[i]=max(f[i],f[j]+1);
}
}
}
for(int i=1;i<=n;i++){
ans=max(ans,f[i]);
}
cout<<ans;
return 0;
}
这个算法可以通过原数据,但是对于本题来说,我们需要使用 \(O(n \log n)\) 的算法。我们可以考虑用二分优化 DP。
定义 \(f_{i}\) 表示对于所有长度为 \(i\) 的单调不上升子序列,它的最后一项最大值。随着 \(i\) 的增大,\(f\) 一定单调不上升。
记 \(f\) 的长度为 \(len\)。对于每一个 \(a_{i}\),分两种情况讨论:
-
若 \(a_{i} \le f_{len}\),则将 \(a_{i}\) 从 \(f\) 的末尾加入。
-
若 \(a_{i} > f_{len}\),则在 \(f\) 中二分查找第一个小于 \(a_{i}\) 的数并用 \(a_{i}\) 替换它。
为什么可以这么处理第二种情况呢?这是因为若以一个较小的数作为不上升子序列的结尾,则在接下来的尝试中,这个子序列“下降”的空间较小。若以一个较大的数作为不上升子序列的结尾,则在接下来的尝试中,这个子序列“下降”的空间较大。
求最长上升子序列的优化方法与之类似。
代码如下:
#include<bits/stdc++.h>
using namespace std;
int a[100005],t,n,f[100005],ans;
int main(){
while(cin>>t){
a[++n]=t;
}
f[1]=a[1];
ans++;
for(int i=2;i<=n;i++){
if(f[ans]>=a[i]) f[++ans]=a[i];
else f[upper_bound(f+1,f+1+ans,a[i],greater<int>())-f]=a[i];
}
cout<<ans<<endl;
memset(f,0,sizeof(f));
ans=0;
f[1]=a[1];
ans++;
for(int i=2;i<=n;i++){
if(f[ans]<a[i]) f[++ans]=a[i];
else f[lower_bound(f+1,f+1+ans,a[i])-f]=a[i];
}
cout<<ans;
return 0;
}
尼克的任务
题目链接。
直觉告诉我们,可以定义 \(f_{i}\) 表示到从第 \(1\) 分钟到第 \(i\) 分钟的最大空闲时间,经过尝试后发现很难推出状态转移方程,于是正难则反,定义 \(f_{i}\) 表示第 \(i\) 分钟到第 \(n\) 分钟的最大空闲时间,采用逆序遍历的方式。
状态:\(f_{x}\) 表示从第 \(x\) 分钟到第 \(n\) 分钟的最大空闲时间。
阶段:第 \(x\) 分钟。
转移:\(f_x = \begin{cases} f_{x+1} + 1 & \text{无任务开始} \\ \underset{1 \le i \le y}{\max}\{f_{x+a_i}\} & \text{有任务开始} \\ \end{cases}\)。
边界:\(f_{n}=0\)。
目标:\(f_{1}\)。
代码如下:
#include<bits/stdc++.h>
using namespace std;
int n,k,f[10005];
vector<int> v[10005];
int main(){
cin>>n>>k;
for(int i=1;i<=k;i++){
int t1,t2;
cin>>t1>>t2;
v[t1].push_back(t2);
}
for(int i=n;i>=1;i--){
if(v[i].size()==0){
f[i]=f[i+1]+1;
continue;
}
for(int j=0;j<v[i].size();j++){
f[i]=max(f[i],f[i+v[i][j]]);
}
}
cout<<f[1];
return 0;
}
双子序列最大和
题目链接。
我们先来考虑如何求一个序列的最大子段和。
状态:\(f_i\) 表示以在原序列中的第 \(i\) 个位置结尾的最大子段和。
转移:\(f_i=\max(f_{i-1},a_i)\)。
边界:\(f_1=a_1\)。
目标:\(f_n\)。
当然也可以使用线段树来解决这个问题,这里不赘述。
那么如何求解本题呢?
由于选出的两个子段必须至少间隔一个位置,所以我们可以考虑间隔的一个位置 \(i\),那么答案就是第 \(i\) 个位置前的最大子段和与第 \(i\) 个位置后的最大子段和,并且两段子段不相交,满足题意。
状态:\(f_{i}\) 表示在原序列中以 \(i\) 结尾的最大子段和,\(g_{i}\) 表示在原序列中以 \(i\) 开头的最大子段和。
转移:\(f_{i}=\max(f_{i-1},a_{i}),g_{i}=\max(g_{i+1},a_{i})\)。
目标:记 \(x_{i}=\underset{1 \le j \le i}{max}\{f_{j}\},y_{i}=\underset{1 \le j \le i}{max}\{g_{j}\}\),目标即为 \(\underset{2 \le i \le n-1}{max}\{x_{i-1}+y_{i+1}\}\)。
边界:\(f_{1}=a_{1},g_{n}=a_{n}\)。
代码如下:
#include<bits/stdc++.h>
using namespace std;
int n,a[1000005],f1[1000005],f2[1000005],ans=-(1<<30);
int main(){
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
f1[1]=a[1];
f2[n]=a[n];
for(int i=2;i<=n;i++) f1[i]=max(f1[i-1]+a[i],a[i]);
for(int i=2;i<=n;i++) f1[i]=max(f1[i-1],f1[i]);//这里求的就是目标中的x[i],表示的不是以i结尾的最大子段和,而是1~i的最大子段和
for(int i=n-1;i>=1;i--) f2[i]=max(f2[i+1]+a[i],a[i]);
for(int i=n-1;i>=1;i--) f2[i]=max(f2[i+1],f2[i]);//这里求得就是目标中的y[i],表示的不是以i开头的最大子段和,而是i~n的最大子段和
for(int i=2;i<n;i++) ans=max(ans,f1[i-1]+f2[i+1]);
cout<<ans;
return 0;
}
Flowers
题目链接。
本题的实质就是给最长上升子序列加了一个权值。
状态:\(f_i\) 表示以在原序列中的第 \(i\) 个位置结尾的满足条件的最大答案。
转移:\(f_{i}= \underset{0 \le j < i,a_{i} < a_{j}}{\max}\{f_{j}|h_{j} < h_{i}\}+a_i\)。
目标:\(\underset{1\le i \le n}{\max}\{f_i\}\)。
边界:\(f_i=a_i(1\le i \le n)\)。
时间复杂度为 \(O(n^2)\),本题的数据范围为 $ 1\ \leq\ n\ \leq\ 2\ ×\ 10^5 $,无法承受。
在状态转移方程中,求满足条件的最大的 \(f_{j}\) 的操作可以使用线段树优化。
优化后的时间复杂度为 \(O(n \log n)\),可以通过本题。
代码如下:
#include<bits/stdc++.h>
using namespace std;
int n,h[200005],a[200005];
long long f[200005],ans=-(1<<30);
struct node{
int l,r;
long long dat;
#define l(x) t[x].l
#define r(x) t[x].r
#define dat(x) t[x].dat
}t[200005*4];
void build(int p,int l,int r){
l(p)=l;
r(p)=r;
if(l==r){
dat(p)=0;
return;
}
int mid=(l+r)>>1;
build(p<<1,l,mid);
build(p<<1|1,mid+1,r);
dat(p)=0;
}
void change(int p,int x,long long v){
if(l(p)==r(p)){
dat(p)=v;
return;
}
int mid=(l(p)+r(p))>>1;
if(x<=mid) change(p<<1,x,v);
else change(p<<1|1,x,v);
dat(p)=max(dat(p<<1),dat(p<<1|1));
}
long long ask(int l,int r,int p){
if(l<=l(p) && r>=r(p)) return dat(p);
int mid=(l(p)+r(p))>>1;
long long val=0;
if(l<=mid) val=max(val,ask(l,r,p<<1));
if(r>mid) val=max(val,ask(l,r,p<<1|1));
return val;
}
int main(){
cin>>n;
for(int i=1;i<=n;i++) cin>>h[i];
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1;i<=n;i++) f[i]=a[i];
build(1,1,n);
for(int i=1;i<=n;i++){
f[i]=ask(1,h[i],1)+a[i];
change(1,h[i],f[i]);
}
for(int i=1;i<=n;i++) ans=max(ans,f[i]);
cout<<ans<<endl;
return 0;
}
SERVICE - Mobile Service
题目链接。
先确定动态规划的阶段为“当前完成了 \(i\) 个任务”,即当“当前完成了 \(i-1\) 个任务”可以转移到“当前完成了 \(i\) 个任务”。
为了计算员工移动的最小代价,我们还需要知道每个员工的位置,一个直接的想法是定义 \(f_{i,x,y,z}\) 表示当前完成了 \(i\) 个任务,三名员工分别位于 \(x,y,z\) 上,然后设计出相应的状态转移方程。
这个算法的时间复杂度约为 \(O(NL^3)\),无法通过本题。
我们注意到,在完成第 \(i\) 个任务后,必定有一名员工位于 \(p_i\) 上,所以我们只需要在状态中记录另外两个员工的位置即可。
状态:\(f_{i,x,y}\) 表示完成了 \(i\) 个任务,一名员工位于 \(p_i\) 上,另外两名员工分别位于 \(x,y\) 上时的最小代价。
转移:\(\forall i \in [0,N-1],\forall x \in[1,L],\forall y \in[1,L]\begin{cases}f_{i+1,x,y}=\min(f_{i+1,x,y},f_{i,x,y}+c_{p_i,p_{i+1}}) & \text{x!=p[i+1] and y!=p[i+1]} \\ f_{i+1,p_i,y}=\min(f_{i+1,p_i,y},f_{i,x,y}+c_{x,p_{i+1}}) & \text{x!=p[i+1] and p[i]!=p[i+1]} \\ f_{i+1,x,p_i}=\min(f_{i+1,x,p_i},f_{i,x,y}+c_{y,p_{i+1}}) & \text{y!=p[i+1] and p[i]!=p[i+1]}\end{cases}\)。
边界:\(f_{0,1,2}=0\),其余均为无穷大。
目标:\(\underset{1 \le x,y \le L}{\max}f_{N,x,y}\)。
代码如下:
#include<bits/stdc++.h>
#define int long long
using namespace std;
int T,l,n,c[205][205],x[1005],f[1005][205][205],ans;
signed main(){
cin>>T;
while(T--){
memset(f,0x3f,sizeof(f));
memset(c,0,sizeof(c));
memset(x,0,sizeof(x));
f[0][1][2]=0;
x[0]=3;
cin>>l>>n;
for(int i=1;i<=l;i++){
for(int j=1;j<=l;j++){
cin>>c[i][j];
}
}
for(int i=1;i<=n;i++) cin>>x[i];
for(int i=0;i<n;i++){
for(int j=1;j<=l;j++){
for(int k=1;k<=l;k++){
if(j==k || x[i]==j || x[i]==k) continue;
if(j!=x[i+1] && k!=x[i+1]) f[i+1][j][k]=min(f[i+1][j][k],f[i][j][k]+c[x[i]][x[i+1]]);
if(j!=x[i+1] && x[i]!=x[i+1]) f[i+1][j][x[i]]=min(f[i+1][j][x[i]],f[i][j][k]+c[k][x[i+1]]);
if(k!=x[i+1] && x[i]!=x[i+1]) f[i+1][x[i]][k]=min(f[i+1][x[i]][k],f[i][j][k]+c[j][x[i+1]]);
}
}
}
ans=0x3f3f3f3f;
for(int i=1;i<=l;i++){
for(int j=1;j<=l;j++){
ans=min(ans,f[n][i][j]);
}
}
cout<<ans<<endl;
}
return 0;
}
区间 DP
石子合并
题目链接。
以求最小分数为例,求最大分数的方法与之类似。
显然,一个区间 \([l,r]\) 一定是由 \([l,k]\) 和 \([k+1,r]\) 转移到的\((l \le k < r)\)。这就意味着两个长度较短的区间上的信息想一个长度较长的区间发生了转移,所以,我们可以把区间长度作为阶段。
那么我们应该如何处理环形呢?只需要将给定的序列 \(a\),复制一份并接在 \(a\) 的末尾即可。
状态:\(f_{i,j}\) 表示将 \([l,r]\) 这个区间内的石子全部合并得到的最小分数。
转移:\(f_{l,r}=\underset{l \le k <r}{min}\{f_{l,k}+f_{k+1,r}\}+\sum^r_{i=l}a_{i}\)。
边界:\(\forall i \in [1,2 \times n],f_{i,i}=0\),其余均为无穷大。
目标:\(\underset{1 \le i \le n}{min}\{f_{i,i+n-1}\}\)。
代码如下:
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,a[500],sum[500],ans,f[500][500];
signed main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
a[i+n]=a[i];
}
memset(f,0x3f,sizeof(f));
for(int i=1;i<=(n<<1);i++){
sum[i]=sum[i-1]+a[i];
f[i][i]=0;
}
for(int i=2;i<=n;i++){
for(int l=1;l+i-1<=(n<<1);l++){
int r=l+i-1;
for(int k=l;k<r;k++){
f[l][r]=min(f[l][r],f[l][k]+f[k+1][r]);
}
f[l][r]+=sum[r]-sum[l-1];
}
}
ans=0x3f3f3f3f;
for(int i=1;i<=n;i++){
ans=min(ans,f[i][i+n-1]);
}
cout<<ans<<endl;
memset(f,0xcf,sizeof(f));
for(int i=1;i<=(n<<1);i++) f[i][i]=0;
for(int i=2;i<=n;i++){
for(int l=1;l+i-1<=(n<<1);l++){
int r=l+i-1;
for(int k=l;k<r;k++){
f[l][r]=max(f[l][r],f[l][k]+f[k+1][r]);
}
f[l][r]+=sum[r]-sum[l-1];
}
}
ans=-1;
for(int i=1;i<=n;i++){
ans=max(ans,f[i][i+n-1]);
}
cout<<ans<<endl;
return 0;
}
[HNOI2010] 合唱队
题目链接。
在本题中,仅用区间左端点 \(l\) 与区间右端点 \(r\) 无法描述一个状态原因是新加入的人有可能从队伍的有段加入,也有可能从队伍的左端加入。
状态:\(f_{l,r,0}\) 表示当前正在被考虑的那个人从左边加入队伍的方案数,\(f_{l,r,1}\) 表示当前正在被考虑的那个人从右边加入队伍的方案数。
转移:
if(a[l]<a[l+1]) f[l][r][0]+=f[l+1][r][0];//第 l 个人从左边加入
if(a[l]<a[r]) f[l][r][0]+=f[l+1][r][1];//第 l 个人从右边加入
if(a[r]>a[l]) f[l][r][1]+=f[l][r-1][0];//第 r 个人从左边加入
if(a[r]>a[r-1]) f[l][r][1]+=f[l][r-1][1];//第 r 个人从右边加入
写公式好麻烦,所以直接用代码表示了
边界:\(\forall i \in [1,n] , f_{i,i,0}=1\)
目标:\(f_{1,n,0}+f_{1,n,1}\);
需要注意的是,根据题意,第一个人是直接插入到队伍中的,只有一种方案,所以边间不是 \(\forall i \in [1,n] , f_{i,i,0}=f_{i,i,1}=1\) 而是 \(\forall i \in [1,n] , f_{i,i,0}=1\)。
代码如下:
#include<bits/stdc++.h>
using namespace std;
int n,a[1005],f[1005][1005][2];
int main(){
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1;i<=n;i++) f[i][i][0]=1;
for(int i=1;i<=n;i++){
for(int l=1;l+i-1<=n;l++){
int r=l+i-1;
if(a[l]<a[l+1]) f[l][r][0]+=f[l+1][r][0]%19650827;
if(a[l]<a[r]) f[l][r][0]+=f[l+1][r][1]%19650827;
if(a[r]>a[r-1]) f[l][r][1]+=f[l][r-1][1]%19650827;
if(a[r]>a[l]) f[l][r][1]+=f[l][r-1][0]%19650827;
}
}
cout<<(f[1][n][0]+f[1][n][1])%19650827;
return 0;
}
Zuma
题目链接。
在这道题中,想到使用区间 DP 解决并根据设计出的状态推出状态转移方程较为简单,这里不再赘述。
状态:\(f_{l,r}\) 表示消除原区间 \([l,r]\) 的最小步数。
转移:\(f_{l,r}=\begin{cases} f_{l+1,r-1} & if(a_l = a_r) \\ \underset{l\le k < r}{min}\{f_{l,k}+f_{k+1,r}\} \end{cases}\)。
边界 \(1\):\(\forall i \in [1,n] f_{i,i}=1\)。
边界 \(2\):\(\forall i \in [1,n-1] f_{i,i+1}=\begin{cases} f_{i,i} & if(a_{i}=a_{i+1}) \\ f_{i,i}+1 & if(a_{i} \neq a_{i+1}) \end{cases}\)。
目标: \(f_{1,n}\)。
容易写出以下代码:
#include<bits/stdc++.h>
using namespace std;
int n,a[505],f[505][505];
int main(){
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
memset(f,0x3f,sizeof(f));
for(int i=1;i<=n;i++){
f[i][i]=1;
if(a[i]==a[i+1]) f[i][i+1]=f[i][i];
else f[i][i+1]=f[i][i]+1;
}
for(int i=2;i<=n;i++){
for(int l=1;l+i-1<=n;l++){
int r=l+i-1;
if(a[l]==a[r]){
if(l+1<=r-1){
f[l][r]=f[l+1][r-1];
}
}
else{
for(int k=l;k<r;k++){
f[l][r]=min(f[l][r],f[l][k]+f[k+1][r]);
}
}
}
}
cout<<f[1][n];
return 0;
}
但是这份代码存在一个问题,当 \(a_l=a_r\) 时,\(f_{l,r}\) 只被 \(f_{l+1,r-1}\) 转移,没有考虑被 \(\underset{l\le k < r}{min}\{f_{l,k}+f_{k+1,r}\}\) 转移的情况,故得到了错误的答案。
正确的代码如下:
#include<bits/stdc++.h>
using namespace std;
int n,a[505],f[505][505];
int main(){
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
memset(f,0x3f,sizeof(f));
for(int i=1;i<=n;i++){
f[i][i]=1;
if(a[i]==a[i+1]) f[i][i+1]=f[i][i];
else f[i][i+1]=f[i][i]+1;
}
for(int i=2;i<=n;i++){
for(int l=1;l+i-1<=n;l++){
int r=l+i-1;
if(a[l]==a[r]){
if(l+1<=r-1){
f[l][r]=f[l+1][r-1];
}
}
for(int k=l;k<r;k++){
f[l][r]=min(f[l][r],f[l][k]+f[k+1][r]);
}
}
}
cout<<f[1][n];
return 0;
}
树形 DP
没有上司的舞会
题目链接。
“上司”与“下属”的关系构成了一棵树,每个节点能否被计算进答案只和该节点的父节点是否被计算进答案有关,所以我们可以定义 \(f_{i,0/1}\) 表示在以 \(i\) 节点为根的子树中,当 \(i\) 节点不算进答案/算进答案时,答案的最大值。我们设 \(S(i)\) 表示 \(i\) 的子节点集合,可以推出如下转移方程:
以及
代码如下:
#include<bits/stdc++.h>
using namespace std;
int t1,t2,n,f[12001][2],happy[12001],s;
bool have_father[12001];
vector<int> have_son[12001];
void dfs(int now){
f[now][0]=0;
f[now][1]=happy[now];
for(int i=0;i<have_son[now].size();i++){
dfs(have_son[now][i]);
f[now][0]+=max(f[have_son[now][i]][0],f[have_son[now][i]][1]);
f[now][1]+=f[have_son[now][i]][0];
}
}
int main(){
cin>>n;
for(int i=1;i<=n;i++) cin>>happy[i];
for(int i=1;i<n;i++){
cin>>t1>>t2;
have_father[t1]=1;
have_son[t2].push_back(t1);
}
for(int i=1;i<=n;i++){
if(!have_father[i]){
s=i;
break;
}
}
dfs(s);
cout<<max(f[s][0],f[s][1]);
return 0;
}
[ZJOI2008] 骑士
题目链接。
前置知识:基环树
\(N\) 个点与 \(N-1\) 条边可以构成一棵树,如果在这棵树上再加一条边,那么就会形成一个环,这时,这棵树就被称为基环树。比如下图就是一棵基环树。
讲解
本题与上一题的唯一区别在于本题骑士间的仇恨构成了基环树森林,我们只需要先找到唯一的环,并断开环上的任意一条边,然后按照上一题的方法解题即可。
代码如下:
#include<bits/stdc++.h>
using namespace std;
inline int read(){register int t1=0,t2=0;register char x=getchar();while(x<'0' ||x>'9'){if(x=='-') t2|=1;x=getchar();}while(x>='0' && x<='9'){t1=(t1<<1)+(t1<<3)+(x^48),x=getchar();}return t2?-t1:t1;}
inline void write(long long x){register int sta[105],top=0;if(x<0) putchar('-'),x=-x;do{sta[top++]=x%10,x/=10;}while(x);while(top) putchar(sta[--top]+48);}
int n,head[2000005],ber[2000005],ver[2000005],nxt[2000005],dfn[1000005];
bool vis1[1000005],v[1000005],flag,vis2[1000005];
long long g[1000005][5],f[1000005][5],sum,w[1000005],num,bro,root1,root2,tot;
void add(int x,int y){
ver[++tot]=y;
ber[tot]=x;
nxt[tot]=head[x];
head[x]=tot;
}
void get(int x,int fa){
if(flag) return;
v[x]=1;
for(int i=head[x];i;i=nxt[i]){
int y=ver[i];
if(!v[y]){
get(y,x);
}
else if(y!=fa){
root1=ber[i];
root2=ver[i];
bro=i;
flag=1;
return;
}
}
}//找环,并断开环上任意一条边,从该边的两个端点开始树形 DP
void dfs(int x){
dfn[x]=++num;
for(int i=head[x];i;i=nxt[i]){
int y=ver[i];
if(!dfn[y]){
dfs(y);
}
}
}//标记同一森林内的节点,防止重复标记
void dp1(int x){
f[x][1]=w[x];
vis1[x]=1;
for(int i=head[x];i;i=nxt[i]){
int y=ver[i];
if(!vis1[y] && (i^1)!=bro){
dp1(y);
f[x][0]+=max(f[y][0],f[y][1]);
f[x][1]+=f[y][0];
}
}
}
void dp2(int x){
g[x][1]=w[x];
vis2[x]=1;
for(int i=head[x];i;i=nxt[i]){
int y=ver[i];
if(!vis2[y] && (i^1)!=bro){
dp2(y);
g[x][0]+=max(g[y][0],g[y][1]);
g[x][1]+=g[y][0];
}
}
}
signed main(){
n=read();
tot++;
for(int i=1;i<=n;i++){
w[i]=read();
int t=read();
add(i,t);
add(t,i);
}
for(int i=1;i<=n;i++){
if(!dfn[i]){
flag=0;
dfs(i);
get(i,0);
dp1(root1);
dp2(root2);
sum+=max(f[root1][0],g[root2][0]);
}
}
write(sum);
return 0;
}
数位 DP
[SCOI2009] windy 数
题目链接。
本题需要使用数位 DP 算法。
什么是数位 DP
数位 DP 是用来解决“在区间 \([L,R]\) 中,有多少个数满足条件 \(K\)”的一种算法,其中,条件 \(K\) 一般与数字的大小无关,而是与数字的构成有关。
如何实现数位 DP
在这里,我们仅讨论使用记忆化搜索实现数位DP的方法。
DFS函数设计
我们需要记录当前还剩几个位置没有填上(记为 \(x\))以及最高位限制标记(记为 \(lim\))还有是否前导零(记为 \(lead\))。在本题中,每一次填数的决策是否合法与上一次填的数有关,所以我们需要记录上一次填的数。
对于其他的数位 DP 题,状态的设计需要依据题意改变。
一些细节
前导零标记
当输入数据为 1 200
时,\(18\),\(14\) 等数字显然都是合法的,但是考虑到区间右端点 \(200\) 为三位数,所以我们搜索到的数字实际上是 \(018\) 和 \(014\),如果不加上前导零标记,这些数字都将被判定为不合法导致答案错误,所以我们需要在状态中加入前导零标记。
-
如果当前位置填上的数字为 \(0\) 并且 \(lead=True\),则说明当前位置上填的数也是前导零,我们将 \(pre\) 改为无穷小,然后继续考虑下一个位置。
-
如果当前位置填上的数字不为 \(0\)(记为 \(i\))并且 \(lead=True\),则说明当前填的数应该作为最高位,我们将 \(pre\) 改为 \(i\),将 \(lead\) 改为 \(False\),然后继续考虑下一个位置。
最高位限制标记
当输入数据为 1 666
时,假设当前填好的数为 \(6??\),则下一位只能从 \(0\sim 6\) 中选一个,而不能从 \(0 \sim 9\) 中选一个,最高位限制标记的作用就是防止程序填出一个大于区间右端点的数。
具体地,最高位限制标记在本题中有以下三种变化的可能:
-
若当前位 \(lim=True\) 而且已经取到了能取到的最高位时,下一位 \(limit=True\)。
-
若当前位 \(lim=True\) 但是没有取到能取到的最高位时,下一位 \(limit=False\)。
-
若当前位 \(lim=False\),下一位 \(lim=False\)。
记忆数组
我们将记忆数组记为 \(f_{x,pre}\),搜索开始前,\(f\) 数组的每一位都等于 \(-1\)。
在本题中,因为有最高位限制以及前导零的存在,当 \(lead=True\) 并且 \(lim=True\) 时,即使 \(f_{x,pre}\neq -1\),也不能直接返回 \(f_{x,pre}\) 中的值。
同样,当 \(lead=True\) 并且 \(lim=True\) 时,也不应该改变 \(f_{x,pre}\) 中的值。
Code
代码如下:
#include<bits/stdc++.h>
using namespace std;
int a,b,n[20],tot,f[20][20],ans1,ans2;
int dfs(int x,int pre,bool lim,bool lead){
if(!x) return 1;
if(!lim && !lead && f[x][pre]!=-1) return f[x][pre];
int res=lim?n[x]:9,cnt=0;
for(int i=0;i<=res;i++){
if(abs(pre-i)<2) continue;
if(!i && lead) cnt+=dfs(x-1,-114514,(i==res && lim),1);
else cnt+=dfs(x-1,i,(i==res && lim),0);
}
if(!lim && !lead) f[x][pre]=cnt;
return cnt;
}
int main(){
cin>>a>>b;
a--;
while(a){
n[++tot]=a%10;
a/=10;
}
memset(f,-1,sizeof(f));
ans1=dfs(tot,-114514,1,1);
memset(n,0,sizeof(n));
tot=0;
while(b){
n[++tot]=b%10;
b/=10;
}
memset(f,-1,sizeof(f));
ans2=dfs(tot,-114514,1,1);
cout<<ans2-ans1;
return 0;
}
SAC#1 - 萌数
题目链接。
本题的难点在于如何高效的判断一个数是否是“萌的”,我们可以按照以下两个条件将回文数分成两类:
-
每个位置上的数字都相等的数。
-
不满足条件 \(1\) 但也是回文数的数。
显然,一个回文数只能满足以上两个条件中的一个,满足第一个条件的回文数可以从形如 AA
的回文数扩展而来,满足第二个条件的回文数可以从形如 BAB
的回文数扩展而来,这启示我们在设计 DFS 函数是可以将上一次填的数以及上一次的上一次填的数放在参数中。
代码如下:
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int mod=(1e9)+7;
string t,l,r;
bool flag;
int n[1005],tot,ans1,ans2,f[1005][15][15][5];
int dfs(int x,int pre1,int pre2,bool lim,bool lead,bool is){
if(!x) return is;
if(!lim && !lead && pre1!=-114514 && pre2!=-114514 && f[x][pre1][pre2][is]!=-1) return (f[x][pre1][pre2][is]%mod+mod)%mod;
int res=lim?n[x]:9,cnt=0;
for(int i=0;i<=res;i++){
if(is && pre1!=-114514 && pre2!=-114514){
if(lead && !i) cnt+=(dfs(x-1,-114514,pre1,(lim && i==res),1,is)%mod+mod)%mod;
else cnt+=(dfs(x-1,i,pre1,(lim && i==res),0,is)%mod+mod)%mod;
}
else{
if((i==pre1 && pre1!=-114514) || (i==pre2 && pre2!=-114514)){
if(lead && !i) cnt+=(dfs(x-1,-114514,pre1,(lim && i==res),1,1)%mod+mod)%mod;
else cnt+=(dfs(x-1,i,pre1,(lim && i==res),0,1)%mod+mod)%mod;
}
else{
if(lead && !i) cnt+=(dfs(x-1,-114514,pre1,(lim && i==res),1,0)%mod+mod)%mod;
else cnt+=(dfs(x-1,i,pre1,(lim && i==res),0,0)%mod+mod)%mod;
}
}
}
if(!lim && !lead && pre1!=-114514 && pre2!=-114514) f[x][pre1][pre2][is]=(cnt%mod+mod)%mod;
return (cnt%mod+mod)%mod;
}
signed main(){
cin>>t>>r;
if(t[t.size()-1]!='0') t[t.size()-1]--;
else{
int temp;
for(int i=t.size()-1;i>=0;i--){
if(t[i]!='0'){
temp=i;
break;
}
}
t[temp]--;
for(int i=temp+1;i<t.size();i++){
t[i]='9';
}
}
for(int i=0;i<t.size();i++){
if(t[i]!='0') flag=1;
if(flag){
string c=" ";
c[0]=t[i];
l.append(c);
}
}
if(t[0]=='0' && t.size()==1){
l.append("0");
}
for(int i=l.size()-1;i>=0;i--) n[++tot]=l[i]-'0';
memset(f,-1,sizeof(f));
ans1=(dfs(tot,-114514,-114514,1,1,0)%mod+mod)%mod;
memset(n,0,sizeof(n));
tot=0;
for(int i=r.size()-1;i>=0;i--) n[++tot]=r[i]-'0';
memset(f,-1,sizeof(f));
ans2=(dfs(tot,-114514,-114514,1,1,0)%mod+mod)%mod;
cout<<((ans2-ans1)%mod+mod)%mod;
return 0;
}