DP全家桶(长期)
DP
序言
动态规划(DP)是一种通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。
运用DP必须满足两个条件:
- 最优子结构:即当前子状态是最优的,不会出现更优情况。
- 无后效性:即当前状态的改变不会对后续状态产生影响。
其实第一个性质是大部分题目都满足的,而无后效性可能就需要选手们自己分析要求,改变思路来使其无后效性(其实这种题也不多)
一般来说,DP解题顺序为:
- 分析题目主要性质
- 根据性质特点列出DP状态
- 分析DP转移方程
- 解剖细节
- 敲代码
其实最主要的就是列出DP状态与转移方程,这也是DP为什么会成为大部分初学者的噩梦的原因,因为通常DP题的题面会给人很强的误导性,可能会让大家想到搜索、退火等算法。
但从个人角度来说,DP最主要的就是思维的敏捷性,比拼的是能否快速地分析题干要求,去除不必要的性质。
本文将会从各大DP中总结规律以及举例题来帮助理解这一算法。
ex:本文是一篇个人记述学习总结的文章,且更偏向省选级别选手的难度,但还是只会从基本开始,不过思路可能会非常简洁(时间不够)
线性DP
最最最基本的DP,这种一般会有很强的子结构特性。一般来说都是能很轻易地分析的。这里就放一道例题的优化吧
B3637 最长上升子序列
典中典的题,相信也是所有DP初学者的必做题目。
考虑定义 \(f_{i}\) 表示前 \(i\) 个元素构成的最长上升子序列的长度。
显然能发现 \(f_i\) 中必定单调不减,这使我们联想到了单调栈。考虑当前元素 \(a_i\) 在 \(f\) 中的位置,可以使用二分或者树状数组实现。然后判断当前位置的序号与当前最长上升子序列长度的大小,若大,意味着当前元素必定可以使最长上升子序列长度增加,故答案加一即可。
#include<bits/stdc++.h>
#define int long long
using namespace std;
template<typename P>
inline void read(P &x){
P res=0,f=1;
char ch=getchar();
while(ch<'0' || ch>'9'){
if(ch=='-') f=-1;
ch=getchar();
}
while(ch>='0' && ch<='9'){
res=res*10+ch-'0';
ch=getchar();
}
x=res*f;
}
int n;
int a[5020];
int dp[5020];
signed main(){
read(n);
for(int i=1;i<=n;++i) read(a[i]);
dp[1]=a[1];
int ans=1;
for(int i=2;i<=n;++i){
int l=1,r=ans,mid;
while(l<=r){
mid=(l+r)>>1;
if(a[i]<=dp[mid]) r=mid-1;
else l=mid+1;
}
dp[l]=a[i];
if(l>ans) ++ans;
}
cout<<ans<<endl;
return 0;
}
由于线性DP过于板,这里不再详述。
区间DP
其实也算是线性DP的一种,不过还是单独拿出来吧。
通常解题思路是考虑某一段区间的DP状态,枚举区间断点进行两边的合并更新答案。
P1880 [NOI1995] 石子合并
还是板子题,设 \(f_{i,j}\) 表示 \(i\) 到 \(j\) 之间的答案,那么只需要枚举一个 \(k,i\le k < j\),然后就有 \(f_{i,j}=f_{i,k}+f_{k+1,j}\),这道题只需要维护两个即可。
#include<bits/stdc++.h>
using namespace std;
int n;
int a[300];
int dpma[300][300];
int dpmi[300][300];
int s[300];
inline int d(int i,int j){
return s[j]-s[i-1];
}
int main()
{
scanf("%d",&n);
int ans=0;
for(int i=1;i<=n;++i){
scanf("%d",&a[i]);
a[i+n]=a[i];
}
for(int i=1;i<=2*n;++i){
dpma[i][i]=dpmi[i][i]=0;
s[i]=s[i-1]+a[i];
}
for(int len=1;len<=n;++len){
for(int i=1,j=i+len;(j<2*n) && (i<=2*n);++i,j=i+len){
dpmi[i][j]=0x3f3f3f3f;
for(int k=i;k<j;++k)
{
dpma[i][j]=max(dpma[i][j],dpma[i][k]+dpma[k+1][j]+d(i,j));
dpmi[i][j]=min(dpmi[i][j],dpmi[i][k]+dpmi[k+1][j]+d(i,j));
}
}
}
int minl=0x3f3f3f3f;
int maxx=-1;
for(int i=1;i<=n;++i)
{
maxx=max(maxx,dpma[i][i+n-1]);
minl=min(minl,dpmi[i][i+n-1]);
}
cout<<minl<<endl<<maxx;
return 0;
}
背包DP
其实这个我都觉得没有太大必要,网上已经很详尽了,这里给一点思路即可。
0-1 背包:
设 \(f_{i,j}\) 表示前 \(i\) 个选了重量为 \(j\) 的物品的价值。那么当前物品可选可不选,不选时答案不变,即 \(f_{i,j}=f_{i-1,j}\)。
要选时答案即为前一个物品没算当前物品重量的价值加上当前价值,即 \(f_{i,j}=f_{i-1,j-w_i}+v_i\),二者合起来求最大就ok了
完全背包:
其实就是将 0-1 背包代码中倒着枚举的 \(j\) 正着枚举就ok了,读者们可以想一想为什么。
多重背包:
多重背包也是 0-1 背包的一个变式。与 0-1 背包的区别在于每种物品有 \(k_i\) 个,而非一个。
朴素的想法是每一个物品都处理一次。
也可以二进制分组。
混合背包:
前三种组合在一起。
有的只能取一次,有的能取无限次,有的只能取 \(k\) 次。
分讨一下,或者全用二进制分组。
二维费用背包:
选一个物品会消耗两种重量(如经费,时间)。
不就是再开一维嘛
分组背包:
每组中的物品只能选一个。
每组内做一次01。见代码。
#include<bits/stdc++.h>
using namespace std;
int v,n,t;
int x;
int g[205][205];
int i,j,k;
int w[10001],z[10001];
int b[10001];
int dp[10001];
int main(){
cin>>v>>n;
for(i=1;i<=n;i++){
cin>>w[i]>>z[i]>>x;
t=max(t,x);
b[x]++;
g[x][b[x]]=i;
}
for(i=1;i<=t;i++){
for(j=v;j>=0;j--){
for(k=1;k<=b[i];k++){
if(j>=w[g[i][k]]){
dp[j]=max(dp[j],dp[j-w[g[i][k]]]+z[g[i][k]]);
}
}
}
}
cout<<dp[v];
return 0;
}
状压DP
将一种状态压缩为一个数进行DP。通常来说数据规模极小,不超过 \(20\)。
P1896 [SCOI2005] 互不侵犯
也是板子,用二进制表示上一行国王在哪儿,枚举下一行状态转移。
#include<bits/stdc++.h>
#define int long long
template<typename P>
inline void read(P &x){
P res=0,f=1;
char ch=getchar();
while(ch<'0' || ch>'9'){
if(ch=='-') f=-1;
ch=getchar();
}
while(ch>='0' && ch<='9'){
res=res*10+ch-'0';
ch=getchar();
}
x=res*f;
}
using namespace std;
int T=1;
int n,K;
int bitcount(unsigned int n){
int count=0;
while(n){
count++;
n&=(n-1);
}
return count;
}
int dp[10][82][(1<<10)];
bool pd[(1<<9)+5],pd2[(1<<9)+5][(1<<9)+5];
void Init(){
for(int i=0;i<(1<<9);++i){
int p=i,las=-1;
bool flag=1;
while(p>0){
int now=p%2;
p/=2;
if(now==las && now==1){
pd[i]=0;
flag=0;
break;
}
else las=now;
}
pd[i]=flag;
if(pd[i]){
for(int j=0;j<(1<<9);++j){
bool o=1;
for(int w=0,ww=1;w<9;ww<<=1,++w){
bool i1=i&ww;
bool j1=j&ww;
bool i2=i&(ww<<1);
bool j2=j&(ww<<1);
if(i1+i2+j1+j2>1){
o=0;
break;
}
}
pd2[i][j]=o;
}
}
}
}
signed main(){
read(n),read(K);
Init();
for(int i=0;i<(1<<n);++i) dp[1][bitcount(i)][i]=1;
for(int i=2;i<=n;++i){
for(int k=0;k<=K;++k){
for(int S=0;S<(1<<n);++S){
if(pd[S] && bitcount(S)<=k)
for(int Z=0;Z<(1<<n);++Z){
if(pd2[S][Z])dp[i][k][S]+=dp[i-1][k-bitcount(S)][Z];
}
}
}
}
int ans=0;
for(int S=0;S<(1<<n);++S) ans+=dp[n][K][S];
cout<<ans<<endl;
return 0;
}
P2704 [NOI2001] 炮兵阵地
一个炮兵会影响两行,压两行就行。
ex:插头DP,后面讲
树形DP
就是把DP的操作搞到树上。基本思路无异。
P1352 没有上司的舞会
要么父亲来儿子必定不来,要么父亲不来儿子可来可不来。3种情况。
#include<bits/stdc++.h>
using namespace std;
#define MAXN 6005
int h[MAXN];
int v[MAXN];
vector<int> son[MAXN];
int f[MAXN][2];
void dp(int x){
f[x][0]=0;
f[x][1]=h[x];
for(int i=0;i<son[x].size();i++){
int y=son[x][i];
dp(y);
f[x][0]+=max(f[y][0],f[y][1]);
f[x][1]+=f[y][0];
}
}
int main(){
int n;
cin>>n;
for(int i=1;i<=n;i++) cin>>h[i];
for(int i=1;i<=n-1;i++){
int x,y;
cin>>x>>y;
son[y].push_back(x);
v[x]=1;
}
int root;
for(int i=1;i<=n;i++)if(!v[i]) {root=i;break;}
dp(root);
cout<<max(f[root][0],f[root][1])<<endl;
return 0;
}
P2014 [CTSC1997] 选课
树上背包板子,先枚举子树内情况进行背包,然后进行背包合并到父亲,向上递归。
#include<bits/stdc++.h>
#define int long long
using namespace std;
template<typename P>
inline void read(P &x){
P res=0,f=1;
char ch=getchar();
while(ch<'0' || ch>'9'){
if(ch=='-') f=-1;
ch=getchar();
}
while(ch>='0' && ch<='9'){
res=res*10+ch-'0';
ch=getchar();
}
x=res*f;
}
int T=1;
const int Max=320;
int n,m;
struct edge{
int to,nxt;
}e[Max<<1];
int head[Max],cnt=0;
void add(int u,int v){
e[++cnt].nxt=head[u];
e[cnt].to=v;
head[u]=cnt;
}
int dp[Max][Max];
void work(int u){
for(int i=head[u];i;i=e[i].nxt) work(e[i].to);
for(int i=head[u];i;i=e[i].nxt){
int to=e[i].to;
for(int j=m;j>0;--j){
for(int k=0;k<j;++k){
dp[u][j]=max(dp[u][j],dp[u][j-k]+dp[to][k]);
}
}
}
}
signed main(){
read(n),read(m);
++m;
for(int i=1;i<=n;++i){
int fa;
read(fa),read(dp[i][1]);
add(fa,i);
}
work(0);
cout<<dp[0][m]<<endl;
return 0;
}
换根DP
树上的DP,每次会将树根 \(root\) 进行改变,其实只需要分析改变根会造成的影响,可能会有分讨。
P3478 [POI2008] STA-Station
其实每次换根使 \(root \rightarrow u\),其实会使答案 \(-size_u+size_1-size_u\),还是很显然的,代码也很好写。
#include<bits/stdc++.h>
#define int long long
using namespace std;
template<typename P>
inline void read(P &x){
P res=0,f=1;
char ch=getchar();
while(ch<'0' || ch>'9'){
if(ch=='-') f=-1;
ch=getchar();
}
while(ch>='0' && ch<='9'){
res=res*10+ch-'0';
ch=getchar();
}
x=res*f;
}
const int Max=1e6+10;
int n;
struct edge{
int to,nxt;
}e[Max<<1];
int head[Max],cnt=0;
void add(int u,int v){
e[++cnt].nxt=head[u];
e[cnt].to=v;
head[u]=cnt;
}
int siz[Max];
int dp[Max];
void dfs1(int u,int fa,int dep){
siz[u]=1;
for(int i=head[u];i;i=e[i].nxt){
int to=e[i].to;
if(to==fa) continue;
dfs1(to,u,dep+1);
siz[u]+=siz[to];
}
dp[1]+=dep;
}
void dfs2(int u,int fa){
for(int i=head[u];i;i=e[i].nxt){
int to=e[i].to;
if(to==fa) continue;
dp[to]=max(dp[to],dp[u]-siz[to]*2+siz[1]);
dfs2(to,u);
}
}
signed main(){
read(n);
for(int i=1;i<n;++i){
int u,v;
read(u),read(v);
add(u,v);
add(v,u);
}
dfs1(1,0,0);
dfs2(1,0);
int maxx=*max_element(dp+1,dp+n+1);
for(int i=1;i<=n;++i) if(dp[i]==maxx) {cout<<i<<endl;break;}
return 0;
}
数位DP
这个是我比较喜欢的DP了,代表性很强,而且很有意思。
一般是讨论 \([l,r]\) 区间内每个数字出现次数、所有数字之和...总之就是跟数字每一位有关。
一般会转换为 \([1,r]-[1,l-1]\)
P2602 [ZJOI2010] 数字计数
就是去DP每个数位能填的数字,分别记录答案。注意要判断是否达上界和前导 \(0\) 即可。
#include<bits/stdc++.h>
#define int long long
using namespace std;
template<typename P>
inline void read(P &x){
P res=0,f=1;
char ch=getchar();
while(ch<'0' || ch>'9'){
if(ch=='-') f=-1;
ch=getchar();
}
while(ch>='0' && ch<='9'){
res=res*10+ch-'0';
ch=getchar();
}
x=res*f;
}
int a,b;
int f[15][2][15][2];
int num[15];
int dfs(int now,int x,int sum,bool op0,bool lim){
int res=0;
if(now==0) return sum;
if(f[now][lim][sum][op0]!=-1) return f[now][lim][sum][op0];
for(int i=0;i<=9;++i){
if(lim && i>num[now]) break;
res+=dfs(now-1,x,sum+((!op0 || i) && (i==x)),(op0) && (i==0),!((!lim) || (i<num[now])));
}
f[now][lim][sum][op0]=res;
return res;
}
int work(int sum,int x){
int len=0;
while(sum){
num[++len]=sum%10;
sum/=10;
}
memset(f,-1,sizeof(f));
return dfs(len,x,0,1,1);
}
signed main(){
read(a),read(b);
for(int i=0;i<=9;++i){
cout<<work(b,i)-work(a-1,i)<<' ';
}
return 0;
}
P4999 烦人的数学作业
跟上一题几乎一模一样,但这个不需要枚举每一个数字,其他地方稍微改改就行。
#include<bits/stdc++.h>
#define int long long
using namespace std;
template<typename P>
inline void read(P &x){
P res=0,f=1;
char ch=getchar();
while(ch<'0' || ch>'9'){
if(ch=='-') f=-1;
ch=getchar();
}
while(ch>='0' && ch<='9'){
res=res*10+ch-'0';
ch=getchar();
}
x=res*f;
}
int T=1;
int f[200][200];
int num[1000010];
int a,b;
const int mod=1e9+7;
int dfs(int now,int sum,bool lim){
if(now==0) return sum;
int res=0;
if(f[now][sum]>=0 && !lim) return f[now][sum]%mod;
int upp=lim?num[now]:9;
for(int i=0;i<=upp;++i){
res+=dfs(now-1,sum+i,lim && i==num[now]);
res%=mod;
}
if(!lim) f[now][sum]=res%mod;
return res%mod;
}
int work(int sum){
int len=0;
while(sum){
num[++len]=sum%10;
sum/=10;
}
memset(f,-1,sizeof(f));
return dfs(len,0,1)%mod;
}
signed main(){
auto solve=[&](){
read(a),read(b);
int ans=0;
ans+=work(b)-work(a-1)+mod;
ans%=mod;
cout<<ans<<endl;
};
read(T);
while(T--) solve();
return 0;
}
计数DP
顾名思义,就是统计方案数的DP,DP的过程没有本质区别。,其实市面上很多求方案数的题都跟这个挂钩,读者们可以注意一下。
P2327 [SCOI2005] 扫雷
典典典,这道题其实只需要根据第一个格子的数字再往后判断是否合法即可,不难,所以我们改一下题目:
有一些格子不知道是多少。
这样呢,就发现前面的会影响后面的。
但其实只会影响周围俩,所以还是可以做的。
其实只需要设 \(f_{i,0/1,0/1}\) 表示前 \(i\) 个位置,第 \(i\) 个位置有没有雷,第 \(i-1\) 个格子有没有雷。对应状态转移即可。没写代码。
P2051 [AHOI2009] 中国象棋
令 \(f_{i,j,k}\) 为考虑到第 \(i\) 行,有 \(j\) 列填了1个棋子,\(k\) 列填了2个棋子的方案数。
然后枚举行,处理每一列的情况。有6种,一个都不放有一种,放一个有两种放的位置(一个上面已经放了一个,一个上面还是空的),放两个有三种放的位置,都放上面空的、一个空一个有一个的、两个都放头上有一个的。
#include<bits/stdc++.h>
#define int long long
using namespace std;
template<typename P>
inline void read(P &x){
P res=0,f=1;
char ch=getchar();
while(ch<'0' || ch>'9'){
if(ch=='-') f=-1;
ch=getchar();
}
while(ch>='0' && ch<='9'){
res=res*10+ch-'0';
ch=getchar();
}
x=res*f;
}
const int mod=9999973;
int dp[120][120][120];
int n,m;
int bal(int x){
return (x*(x-1)/2)%mod;
}
signed main(){
read(n),read(m);
int ans=0;
dp[0][0][0]=1;
for(int i=1;i<=n;++i){
for(int j=0;j<=m;++j){
for(int k=0;k<=m-j;++k){
dp[i][j][k]=dp[i-1][j][k];
if(k>=1) dp[i][j][k]=(dp[i][j][k]+dp[i-1][j+1][k-1]*(j+1))%mod;
if(j>=1) dp[i][j][k]=(dp[i][j][k]+dp[i-1][j-1][k]*(m-j+1-k))%mod;
if(k>=1) dp[i][j][k]=(dp[i][j][k]+dp[i-1][j][k-1]*j*(m-j-k+1))%mod;
if(j>=2) dp[i][j][k]=(dp[i][j][k]+dp[i-1][j-2][k]*bal(m-j+2-k));
if(k>=2) dp[i][j][k]=(dp[i][j][k]+dp[i-1][j+2][k-2]*bal(j+2));
dp[i][j][k]%=mod;
}
}
}
for(int i=0;i<=m;++i) for(int j=0;j<=m;++j) ans+=dp[n][i][j],ans%=mod;
cout<<ans<<endl;
return 0;
}
CF559C Gerald and Giant Chess
接考虑不能走黑色,数据范围太大不好做。
考虑容斥一下,不走黑色的方案数=总的方案数-经过黑点的方案数
不考虑限制从点 \(i\) 走到点 \(j\) 的方案数为:\(C_{x_i+y_i-x_j-y_j}^{x_i-x_j}\)
先把黑点按照坐标排序。
定义 \(f_i\) 表示不经过黑点走到第 \(i\) 个点的方案数。
那么:\(f[i]=C_{x_i+y_i-2}^{x_i-1}-\sum_{j=1}^{i-1}f[j]*C_{x_i+y_i-x_j-y_j}^{x_i-x_j}\)
表示随便走到第 \(i\) 个点的方案数减去经过前面一个黑点后剩下随便走的方案数。
令 \((h,w)\) 为第 \(n+1\) 个黑点,最终答案就是 \(f_{n+1}\)。
代码还没改对,之后上传。
期望DP
倒数第二不会的来了。
实际就是DP套个期望/概率。
- 期望的一些性质
$ E(X+Y)=E(X)+E(Y)$
\(E(XY)=E(X)E(Y)(X,Y相互独立)\)
\(E(aX+b)=aE(X)+b\)
\(E(c)=c\)
CF148D Bag of mice
设 \(f(i,j)\) 表示有 \(i\) 只白鼠,\(j\) 只黑鼠时A先手胜的概率
初始状态
全白时,显然先手必胜
有一只黑鼠时,先手若抽到黑鼠则后手必胜,所以先手首回合必须抽到白鼠
\(f(i,0)=1,f(i,1)=\frac{i}{i+1}\)
转移方程 \(f(i,j)\)
先手抽到白鼠,胜:\(\frac{i}{i+j}\)
先手抽到黑鼠,后手抽到白鼠,败: \(0\)
先手抽到黑鼠,后手抽到黑鼠,跑一只白鼠:\(\frac{j}{i+j}\times \frac{j-1}{i+j-1}\times \frac{i}{i+j-2}\times f(i-1,j-2)\)
先手抽到黑鼠,后手抽到黑鼠,跑一只黑鼠:\(\frac{j}{i+j}\times \frac{j-1}{i+j-1}\times \frac{j-2}{i+j-2}\times f(i,j-3)\)
\(f(i,j)=\frac{i}{i+j}+\frac{j}{i+j}\times \frac{j-1}{i+j-1}\times \frac{i}{i+j-2}\times f(i-1,j-2)+\frac{j}{i+j}\times \frac{j-1}{i+j-1}\times \frac{j-2}{i+j-2}\times f(i,j-3)\)
\(O(wb)\)
#include<bits/stdc++.h>
using namespace std;
double f[1010][1010];
int w,b;
double dfs(int nw,int nb){
if(nw==0) return 0.0;
if(nb==0) return 1.0;
if(f[nw][nb]>0) return f[nw][nb];
double ans=0;
ans+=1.0*nw/(nw+nb);
if(nb==2)
ans+=1.0*nb/(nw+nb)*(nb-1)/(nw+nb-1)*dfs(nw-1,nb-2);
else if(nb>=3)
ans+=1.0*nb/(nw+nb)*(nb-1)/(nw+nb-1)*(1.0*nw/(nw+nb-2)*dfs(nw-1,nb-2)+1.0*(nb-2)/(nw+nb-2)*dfs(nw,nb-3));
return f[nw][nb]=ans;
}
signed main(){
cin>>w>>b;
printf("%.9lf",dfs(w,b));
return 0;
}
插头DP
最不会的。
还没懂,懂了补上。
DP优化全家桶
顾名思义,DP优化就是对复杂度很高的DP方程进行一系列转化或者初始化,使其复杂度下降的方式。常用的方式有前缀和优化、单调队列优化、线段树(平衡树)优化、四边形不等式优化、斜率优化等。
前缀和优化
其实就是DP方程状态是由一段前缀的状态转移来的,用前缀和优化。
P2513 [HAOI2009] 逆序对数列
令\(f_{i,j}\) 为前 \(i\) 小的数排列,有 \(j\) 对逆序对的方案数。
\(f_{i,j}=\displaystyle\sum_{k=1}^{k\leq i}f_{i-1,j-k}\)。
转移是一段前缀,用前缀和优化。
#include<bits/stdc++.h>
#define int long long
using namespace std;
template<typename P>
inline void read(P &x){
P res=0,f=1;
char ch=getchar();
while(ch<'0' || ch>'9'){
if(ch=='-') f=-1;
ch=getchar();
}
while(ch>='0' && ch<='9'){
res=res*10+ch-'0';
ch=getchar();
}
x=res*f;
}
int dp[1020][1020],sum=0;
int n,k;
const int mod=1e4;
signed main(){
read(n),read(k);
dp[1][0]=1;
for(int i=2;i<=n;++i){
sum=0;
for(int j=0;j<=k;++j){
sum+=dp[i-1][j],sum%=mod;
dp[i][j]=sum;
if(j-i+1>=0) sum-=dp[i-1][j-i+1],sum%=mod,sum+=mod,sum%=mod;
}
}
cout<<dp[n][k]%mod;
return 0;
}
P2627 [USACO11OPEN] Mowing the Lawn G
令 \(f_{i,0/1}\) 表示考虑到前 \(i\) 只奶牛,第 \(i\) 只选/不选的方案数。
\(f_{i,0}=max(f_{j,0},f_{j,1})(j<i)\)
\(f_{i,1}=max(f_{j,0}-S_{j-1})+S_i\),其中 \(S_i\) 为前缀和。
#include<bits/stdc++.h>
#define int long long
#define endl "\n"
#define PII pair<int,int>
using namespace std;
template <typename T>
void read(T &x){
T res=0,f=1;
char ch=getchar();
while(ch<'0'||ch>'9'){
if(ch=='-')f=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9'){
res=res*10+ch-'0';
ch=getchar();
}
x=res*f;
}
int n,k;
int a[5000010];
int dp[5000010];
int sum[5000010];
int d[5000010];
int q[5000010];
int head=0,tail=1;
int get_sum(int x){
d[x]=dp[x-1]-sum[x];
while(head<=tail && d[q[tail]]<=d[x]) --tail;
q[++tail]=x;
while(head<=tail && q[head]<x-k) ++head;
return d[q[head]];
}
signed main(){
read(n);read(k);
for(int i=1;i<=n;++i){
read(a[i]);
sum[i]=a[i]+sum[i-1];
}
for(int i=1;i<=n;++i){
dp[i]=sum[i]+get_sum(i);
}
cout<<dp[n]<<endl;
return 0;
}
P2511 [HAOI2008] 木棍分割
“最大的最小”显然二分。二分出这个长度后再求方案数。
令 \(f_{i,j}\) 表示前 \(i\) 根木棍分为 \(j\) 组的方案数。
\(f_{i,j}=\displaystyle\sum_{k=t}^{i-1}f_{k,j-1}\)
其中 \(t\) 是最小的满足 \(\displaystyle\sum_{k=t+1}^{i}a_k\leq 二分出的长度\)
每次找 \(t\) 是 \(n^3\) 的,可以 \(n^2\) 预处理出每个位置对应的 \(t\)。
相应的求和用前缀和优化。
卡空间,用滚动数组。
#include<bits/stdc++.h>
#define int long long
using namespace std;
template<typename P>
inline void read(P &x){
P res=0,f=1;
char ch=getchar();
while(ch<'0' || ch>'9'){
if(ch=='-') f=-1;
ch=getchar();
}
while(ch>='0' && ch<='9'){
res=res*10+ch-'0';
ch=getchar();
}
x=res*f;
}
const int mod=1e4+7;
int n,m;
int le[50020];
bool check(int x){
int tot=0,len=0;
for(int i=1;i<=n;++i){
if(len+le[i]>x) tot++,len=le[i];
else len+=le[i];
if(tot>m) return 0;
}
return tot<=m;
}
int dp[50010];
int sum[50010];
int p[50010];
int rem[50010];
signed main(){
read(n),read(m);
int mx=0;
for(int i=1;i<=n;++i) read(le[i]),p[i]=p[i-1]+le[i],mx=max(mx,le[i]);
int l=mx,r=p[n],ans=0;
while(l<=r){
int mid=(l+r)>>1;
if(check(mid)) r=mid-1,ans=mid;
else l=mid+1;
}
for(int i=1;i<=n;++i){
for(int k=0;k<i;++k){
if(p[i]-p[k]<=ans) {rem[i]=k;break;}
}
}
int res=(p[n]<=ans);
for(int i=1;i<=n;++i){
if(p[i]<=ans) dp[i]=1;
sum[i]=(sum[i-1]+dp[i])%mod;
}
for(int i=2;i<=m+1;++i){
for(int j=1;j<=n;++j){
dp[j]=sum[j-1];
if(rem[j]>=1) dp[j]=((dp[j]-sum[rem[j]-1])%mod+mod)%mod;
}
for(int j=1;j<=n;++j) sum[j]=(sum[j-1]+dp[j])%mod;
res+=dp[n];
res%=mod;
}
cout<<ans<<' '<<res<<endl;
return 0;
}
单调队列优化
前置技能:单调队列(经典的问题模型:洛谷P1886 滑动窗口)
用于优化形如 \(f_i=\min/\max_{j=l_i}^{i-1}\{g_j\}+w_i\),且满足 \(l_i\le l_{i+1}\)的转移。
人话:对于序列中的每个点,从其左侧一段决策区间内的最值进行转移,且决策区间随着序列下标的增大也不断右移(就像窗口向右滑动)。
设 \(j<k\),容易发现如果 \(g_j\) 劣于 \(g_k\) 的话,那么当决策区间移动到 \(k\) 以后,\(j\) 永远不会成为最优决策点,再也不会被转移了。
于是,我们只要维护一个队列,满足下标递增,决策性递减。我们需要当前的队首成为最优决策点,那么当队首第一次超出了区间范围(以后也就永远超出了)就把它出队。为了保证单调性,队尾新加入点之前,要先把队列中比它劣的点依次从队尾出队。
P3594 [POI2015] WIL
这道题其实不算是DP,用单调队列模板改一改就ok了。
#include<bits/stdc++.h>
#define int long long
template<typename P>
inline void read(P &x){
P res=0,f=1;
char ch=getchar();
while(ch<'0' || ch>'9'){
if(ch=='-') f=-1;
ch=getchar();
}
while(ch>='0' && ch<='9'){
res=res*10+ch-'0';
ch=getchar();
}
x=res*f;
}
using namespace std;
int n,p,d;
int a[2000010];
int q[2000010];
int sum[2000010];
int w[200010];
signed main(){
read(n),read(p),read(d);
for(int i=1;i<=n;++i) read(a[i]),sum[i]=sum[i-1]+a[i];
int h=0,t=0,len=1;
q[t]=d;
int ans=d;
for(int i=d+1;i<=n;++i){
while(h<=t && sum[q[t]]-sum[q[t]-d]<sum[i]-sum[i-d]) t--;
q[++t]=i;
while(h<=t && sum[i]-sum[len-1]-sum[q[h]]+sum[q[h]-d]>p){
++len;
while(h<=t && q[h]-d+1<len) h++;
}
ans=max(ans,i-len+1);
}
cout<<ans<<endl;
return 0;
}
CF1077F2 Pictures with Kittens (hard version)
令 \(f_{i,j}\) 为考虑前 \(i\) 个数,已经选了 \(j\) 个且第 \(i\) 个数被选了的最大和。
转移是一段长度为 \(k\) 的区间中的最值。
#include<bits/stdc++.h>
#define int long long
using namespace std;
template<typename P>
inline void read(P &x){
P res=0,f=1;
char ch=getchar();
while(ch<'0' || ch>'9'){
if(ch=='-') f=-1;
ch=getchar();
}
while(ch>='0' && ch<='9'){
res=res*10+ch-'0';
ch=getchar();
}
x=res*f;
}
int n,m,k;
int a[5020];
int dp[5020][5020];
int q[5020];
int h,t;
signed main(){
read(n),read(m),read(k);
for(int i=1;i<=n;++i) read(a[i]);
memset(dp,0xcf,sizeof(dp));
if(n/m>k){
puts("-1");
return;
}
dp[0][0]=0;
for(int j=1;j<=k;++j){
h=1,t=0;
q[++t]=0;
for(int i=1;i<=n;++i){
while(h<=t && i-q[h]>m) h++;
dp[i][j]=dp[q[h]][j-1]+a[i];
while(h<=t && dp[i][j-1]>=dp[q[t]][j-1]) t--;
q[++t]=i;
}
}
int maxx=0;
for(int i=n-m+1;i<=n;++i) maxx=max(maxx,dp[i][k]);
cout<<maxx<<endl;
return 0;
}