简单 dp

dp

我是谁?
我从哪里来?
我要到哪去?

声明

本文对于 dp 的定义较为广泛,包含某些图论算法,递推算法,计数类问题等。
附题库连接:

概念

dp 的思想是高效存储信息,可以视作对暴力搜索的优化。
即将具有相同特征的信息进行整合统一处理,并通过空间的存储优化时间的效率。
同时,我们处理的问题基本相同,在每一步的子问题也相同,让我们可以通过 dp 高效处理。

最优子结构

对于原问题可以通过求解一定的子问题将答案合并得到解。
同时能够证明原问题最优解可以通过子问题最优解得到。

无后效性

已经求解的子问题不会改变,不受后续操作影响。

子问题重叠

对于大量的子问题,即使他们在来源和位置上不同,但对于答案的影响相同,我们就可以考虑将其合并共同处理。

状态

我们在合并子问题时关注的信息。

转移

对于子问题的决策,求解当前状态时对于其他状态的选择与利用。
一般通过寻找题目中的等量关系进行求解。

阶段(转移顺序)

在进行状态之间的转移时,多数情况应当满足之前的状态已完成求解。
转移顺序本质上则是一种对顺序的构造,使其满足这一要求。
基本的有填表法和刷表法。

  • 填表法:用之前的信息更新现在的信息。
  • 刷表法:使用当前的信息对以后的信息记录贡献。

其他的转移顺序,如区间 dp 的按区间长度转移,树上图上 dp 的转移顺序较为灵活,需自行考虑。

优化

对于时间空间的节省。

基础模型

简单 dp I

这一部分的 dp 题目大部分状态设计较为简单。
一般无需优化即可通过。
以下是洛谷【动态规划1】题单。
也包含一些较为简单的背包 dp 和树形 dp。
通过这部分题目你可以基本的建立对 dp 的认识,
并掌握基本的分析题目方法。
所以请认真思考,不要抱着“刷水题”的心态通过这一部分。

T1 纸币问题一

考虑相同的子问题中我们关心什么。
显然只有当前价值对我们有用。
不难想到设 \(f(i)\) 为凑出 \(i\) 元钱最少所需纸币数,
显然有转移 \(f(i)=\min f(i-h_i)+1\)

考虑转移顺序。
显然可以枚举当前的钱数和纸币种类进行转移。

然而还有一种写法:先枚举纸币再枚举钱数,
对于未完全处理的信息进行预先转移。
这不符合我们对阶段的定义,
但由于我们不关心路径,
不妨对于最优解的取法按编号设序,转化成该等价问题。

实现上注意显然有 \(f(0)=0\)
其他则应赋值为 \(\inf\) 因为要取最小值。

//Solution 01
#include<bits/stdc++.h>
using namespace std;
int dp[10009],cash[1009];
int main(){
    int n,w; scanf("%d%d",&n,&w),dp[0]=0; 
    for(int i=1;i<=n;i++) scanf("%d",&cash[i]);
    for(int i=1;i<=w;i++) dp[i]=10086;
    for(int i=1;i<=w;i++){
        for(int j=1;j<=n;j++){
            if(cash[j]>i) continue;
            dp[i]=min(dp[i],dp[i-cash[j]]+1);
        }
    }
    printf("%d\n",dp[w]);
    return 0;
}

//Solution 02
#include<bits/stdc++.h>
using namespace std;
const int N=10009;
int dp[N],h[N];
int main(){
    int n,w; scanf("%d%d",&n,&w);
  	for(int i=1;i<=n;i++) scanf("%d",&h[i]);
    for(int i=1;i<=w;i++) dp[i]=N;
    for(int i=1;i<=n;i++) for(int j=h[i];j<=w;j++) dp[j]=min(dp[j-h[i]]+1,dp[j]);
    printf("%d\n",dp[w]);
    return 0;
}
T2 数字三角形

\(f(i,j)\) 表示第 \(i\) 行第 \(j\) 列的最大值。
我们的最优子结构是存在的,而位置是我们唯一关心的事情。
转移显然。
最终在最后一行统计答案即可。

#include<bits/stdc++.h>
using namespace std;
const int N=1008;
int dp[N][N];
int main(){
    int n,ans=0; scanf("%d",&n);
    for(int i=1;i<=n;i++) for(int j=1;j<=i;j++) scanf("%d",&dp[i][j]),dp[i][j]+=max(dp[i-1][j],dp[i-1][j-1]);
    for(int i=1;i<=n;i++) ans=max(ans,dp[n][i]);
    printf("%d\n",ans);
    return 0;
}
T3 纸币问题二

思路同 T1。
本题由于所求等价于 排列 故仅能使用第一种方法。
现在我们关心方案,设序显然不对。
显然 \(f(0)=1\),注意要取模。

#include<bits/stdc++.h>
using namespace std;
int n,w,cash[1065];
const int Mod=1e9+7;
unsigned long long dp[10006];
int main(){
    scanf("%d%d",&n,&w); dp[0]=1;
    for(int i=1;i<=n;i++) scanf("%d",&cash[i]);
    for(int i=1;i<=w;i++) for(int j=1;j<=n;j++) if(cash[j]<=i) dp[i]+=dp[i-cash[j]],dp[i]%=Mod;
    printf("%llu\n",dp[w]);
    return 0;
}
T4 纸币问题三

T2 组合版本,强制设序。
用上述第二种写法。

#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int n,w,cash[1009],dp[10009];
int main(){
    scanf("%d%d",&n,&w),dp[0]=1; for(int i=1;i<=n;i++) scanf("%d",&cash[i]);
    for(int i=1;i<=n;i++) for(int j=cash[i];j<=w;j++) dp[j]=(dp[j]+dp[j-cash[i]])%mod;
    printf("%d\n",dp[w]);
    return 0;
}
T5 采药/【模板】01背包

01 背包问题一般题意:
\(n\) 个物品和一个容量为 \(W\) 的背包,每个物品有重量 \(w_{i}\) 和价值 \(v_{i}\) 两种属性。
要求选若干物品放入背包,使背包中物品的总价值最大,且背包中物品的总重量不超过背包的容量。

本文以下均采用 weight 和 value 的解释。

考虑我们只关心所得价值和已用容量/剩余容量。
显然有设 \(f(i)\) 表示用 \(i\) 单位容量获得的最大收益。
转移呢?显然,我们当前的决策有从之前取一个或不取两种选择。
然而发现我们的转移顺序复杂,可能会取到以前取过的元素。
于是考虑设序,和 T1 一样先枚举物品。

然而问题仍在。
例如如果 \(f(5)\) 想从 \(f(4)\) 转移时会发现 \(f(4)\) 已经被修改过。
换言之,\(f(4)\) 可能已经取过当前物品。
于是思考切换转移顺序,在枚举容量时倒序枚举,这样就可以保证不会使用被当前物品更新过的信息而导致错误。

初始全零即可,注意答案可能并非刚好取满每一个元素,需要取序列最大值。

#include<bits/stdc++.h>
using namespace std;
const int N=109,M=1009;
int w[N],v[N],dp[M];
int main(){
    int T,n,ans=-1; scanf("%d%d",&T,&n);
    for(int i=1;i<=n;i++) scanf("%d%d",&w[i],&v[i]);
    for(int i=1;i<=n;i++) for(int j=T;j>=w[i];j--) dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
    for(int i=1;i<=T;i++) ans=max(ans,dp[i]);
    printf("%d\n",ans);
    return 0;
}
T6 挖地雷

思路是容易的。
\(f(i)\) 表示到了第 \(i\) 个地窖最多获得地雷数。
任意起点就在初始状态里设一个 \(f(i)=h_i\)
转移是显然的。

现在的问题是输出路径。
由于我们具有最优子结构所以我们每一步应当也由一个最优的选择转移而来。
我们在 dp 的过程中维护这个转移位置即可。

最后的答案由于不存在负的地雷数,可以直接输出最大值。
如果其还有后继节点,显然其后继节点至少不会劣于它。
故考虑保留下标最大的最优解。

#include<bits/stdc++.h>
using namespace std;
const int N=25;
vector<int> pre[N];
int lst[N],dp[N],num[N],n;
int main(){
    int tmp,best=0;
    scanf("%d",&n); for(int i=1;i<=n;i++) scanf("%d",&num[i]),dp[i]=num[i];
    for(int i=1;i<n;i++) for(int j=i+1;j<=n;j++) {scanf("%d",&tmp); if(tmp) pre[j].push_back(i);}
    for(int i=1;i<=n;i++) for(int j:pre[i]) if(dp[j]+num[i]>dp[i]) lst[i]=j,dp[i]=dp[j]+num[i];
    for(int i=1;i<=n;i++) if(dp[i]>=dp[best]) best=i;
    tmp=best; while(tmp) pre[0].push_back(tmp),tmp=lst[tmp];
    for(auto i=pre[0].rbegin();i!=pre[0].rend();i++) printf("%d ",*i);
    printf("\n%d\n",dp[best]);
}
T7 滑雪/【模板】记搜

按照套路可以设 \(f(i,j)\) 为在 \((i,j)\) 点能够获得的最大长度。
我们发现转移很困难。
显然,我们可以枚举每一个点并试着从四周转移。
由于每次长度至少加一最劣只需遍历 \(RC\) 轮就可以得到答案。
时间复杂度 \(O(4n^4)\),对于 \(100\) 的数据范围很勉强。

#include<bits/stdc++.h>
using namespace std;
const int N=105,dx[4]={1,0,0,-1},dy[4]={0,1,-1,0};
int h[105][105],dp[105][105];
int main(){
    int r,c,ans=INT_MIN; scanf("%d%d",&r,&c);
    for(int i=1;i<=r;i++) for(int j=1;j<=c;j++) scanf("%d",&h[i][j]),dp[i][j]=1;
    for(int i=0;i<=r+1;i++) dp[i][0]=dp[i][n+1]=INT_MIN;
    for(int i=0;i<=c+1;i++) dp[0][i]=dp[n+1][i]=INT_MIN;
    for(int cnt=1;cnt<=r*c;cnt++) for(int i=1;i<=r;i++) for(int j=1;j<=c;j++) for(int it=0;it<4;it++) 
        if(h[i+dx[i]][j+dx[j]]>h[i][j]) dp[i][j]=max(dp[i][j],dp[i+dx[i]][j+dy[i]]+1);
    for(int i=1;i<=r;i++) for(int j=1;j<=c;j++) ans=max(ans,dp[i][j]);
   	printf("%d\n",ans);
    return 0;
}

我们交上去会 TLE 90
发现最后一次转移时有许多非法情况根本无需反复判断,可以直接对其记录。
我们可以对每个位置维护一个转移数组,直接判断 \(it\) 为多少时合法。
这显然是可以的,然而只有常数优化。
我们发现我们在中间干了大量的无效转移尝试,究其原因还是循环的转移太过死板。
对于这种转移式不固定的题目,我们可以考虑用搜索进行 \(dp\),在搜索的同时记录最优子结构。
即所谓“记忆化搜索”是也。

现在考虑搜索怎么做。
我们先不用管时间复杂度的事情,显然可以:

  1. 枚举每一个点。
  2. 对于当前点递归操作,在四周找高度比自己高的点,递归操作。
  3. 如果找不到就退出。

这样能够保证正确性。
然而,我们可能会重复访问一个点,在这时如果存过当前点的答案就可以直接返回。
最终复杂度 \(O(n^2)\)

#include<bits/stdc++.h>
using namespace std;
const int N=105,dx[4]={1,0,0,-1},dy[4]={0,1,-1,0};
int h[105][105],dp[105][105],r,c;
int dfs(int i,int j){
	if(~dp[i][j]) return dp[i][j];
	dp[i][j]=1;
	for(int o=0;o<4;o++) if(i+dx[o]>=1&&i+dx[o]<=r&&j+dy[o]>=1&&j+dy[o]<=c&&h[i][j]<h[i+dx[o]][j+dy[o]]) dp[i][j]=max(dp[i][j],dfs(i+dx[o],j+dy[o])+1);
	return dp[i][j];
}
int main(){
    int ans=INT_MIN; scanf("%d%d",&r,&c);
    for(int i=1;i<=r;i++) for(int j=1;j<=c;j++) scanf("%d",&h[i][j]),dp[i][j]=-1;
    for(int i=1;i<=r;i++) for(int j=1;j<=c;j++) ans=max(ans,dfs(i,j));
   	printf("%d\n",ans);
    return 0;
}

如果实在想用裸 dp 实现的话也可以考虑排序再按顺序转移。
考虑到快排的实力,\(\log_2100\le7\) 和递归的常数,具体谁快也不一定。

我们回顾一下这个思想,对于一个转移较为“灵活”的式子,
可以考虑记忆化搜索,
因为每个点是我们唯一关注的信息,
所以我们可以将其存储下来进行分析。

T8 最大食物链计数

转化题意:对于一个 DAG,求其中源点到汇点的路径条数。
DAG 保证了无环,也就是转移无后效性。
考虑我们只关心一个点和他与源点的关系。
想设 \(f(i)\) 表示源点到达 \(i\) 的方案数,转移直接加即可。

#include<bits/stdc++.h>
using namespace std;
const int N=5009,Mod=80112002;
vector<int> k[N]; int In[N],dp[N],Out[N];
queue<int> q;
int main(){
    int n,m,l,r,ans=0; scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++) scanf("%d%d",&l,&r),k[l].push_back(r),In[r]++,Out[l]++;
    for(int i=1;i<=n;i++) if(In[i]==0) q.push(i),dp[i]=1;
    while(!q.empty()){
        l=q.front(),q.pop();
        for(int i:k[l]){
        	dp[i]=(dp[i]+dp[l])%Mod;
        	if(--In[i]==0) q.push(i);
		}
    }
    for(int i=1;i<=n;i++) if(Out[i]==0) ans=(ans+dp[i])%Mod;
    printf("%d\n",ans);
    return 0;
}
T9 最大子段和

这个严格来说不太算 dp。
最大子段和=最大前缀和-最小前缀和(满足其位置在最大前缀和前即可)
这其实是个线性递推。

#include<bits/stdc++.h>
using namespace std;
int a[200009];
int main(){
    int n,Min=0,ans=INT_MIN; scanf("%d",&n);
    for(int i=1;i<=n;i++){
        scanf("%d",&a[i]),a[i]+=a[i-1];
        ans=max(ans,a[i]-Min),Min=min(Min,a[i]);
    }
    printf("%d\n",ans);
    return 0;
}
T10 5 倍经验日

01 背包变形,注意一下对于每个 \(dp\) 值的处理需要先加上 \(lose\)
同时存在花费为零,故失败情况需要后考虑。

#include<bits/stdc++.h>
using namespace std;
const int N=1006;
int lose[N],win[N],need[N],dp[N];
int main(){
    int n,x; scanf("%d%d",&n,&x);
    for(int i=1;i<=n;i++) scanf("%d%d%d",&lose[i],&win[i],&need[i]);
    for(int i=1;i<=n;i++){
        for(int j=x,tmp;j>=0;j--){
            tmp=dp[j];
            if(j>=need[i]) dp[j]=max(dp[j],dp[j-need[i]]+win[i]);
            dp[j]=max(dp[j],tmp+lose[i]);
        }
    }
    printf("%lld\n",5ll*dp[x]);
    return 0;
}
T11 过河卒

就是简单的表格问题。
直接设 \(f(i,j)\) 表示该点方案,转移显然。

#include<bits/stdc++.h>
using namespace std;
typedef unsigned long long ll;
bool vis[55][55];
ll dp[55][55];
int n,m;
inline ll dpp(int a,int b){return (a>=0&&b>=0)?dp[a][b]:0;}
void mark(int a,int b){if(a>=0&&b>=0&&a<=n&&b<=m) vis[a][b]=1;}
int main(){
    int a,b; scanf("%d%d%d%d",&n,&m,&a,&b);
    mark(a+2,b+1),mark(a+2,b-1);
    mark(a-2,b+1),mark(a-2,b-1);
    mark(a+1,b+2),mark(a+1,b-2);
    mark(a-1,b+2),mark(a-1,b-2);
    mark(a,b),dp[0][0]=1;
    for(int i=0;i<=n;i++) for(int j=0;j<=m;j++) if(!vis[i][j]) dp[i][j]+=dpp(i-1,j)+dpp(i,j-1);
    printf("%llu",dp[n][m]);
    return 0;
}
T12 装箱问题

\(f(i)\) 表示是否可能取到,转移易得。
思考可以用 bitset 优化,确实不难。

#include<bits/stdc++.h>
using namespace std;
bitset<20000> mp;
int main(){
    int V,n,k; scanf("%d%d",&V,&n),mp[0]=1;
    for(int i=1;i<=n;i++) scanf("%d",&k),mp|=(mp<<k);
    for(int i=0;i<=V;i++) if(mp[V-i]) printf("%d",i),exit(0); 
    return 0;
}
T13 疯狂的采药/【模板】完全背包

完全背包就是每种物品可以无限取。
所以我们发现 01 背包特意倒序枚举避免使用修改过的数据,
然而对于完全背包想要的就是修改过的。
那就很简单了。
\(O(nV)\) 可过

#include<bits/stdc++.h>
using namespace std;
const int N=10009,M=10000009;
int w[N],v[N];
long long dp[M];
int main(){
    int T,n; long long ans=-1; scanf("%d%d",&T,&n);
    for(int i=1;i<=n;i++) scanf("%d%d",&w[i],&v[i]);
    for(int i=1;i<=n;i++) for(int j=w[i];j<=T;j++) dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
    for(int i=1;i<=T;i++) ans=max(ans,dp[i]);
    printf("%lld\n",ans);
    return 0;
}
T14 小 A 点菜

仍然是没有任何新意的状态设计。
\(f(i)\) 表示正好 \(i\) 元的方案数。
初始状态设 \(f(0)=1\)

#include<bits/stdc++.h>
using namespace std;
int a[105],dp[10086];
int main(){
    int m,n; scanf("%d%d",&n,&m),dp[0]=1;
    for(int i=1;i<=n;i++) scanf("%d",&a[i]);
    for(int i=1;i<=n;i++) for(int j=m;j>=a[i];j--) dp[j]+=dp[j-a[i]];
    printf("%d\n",dp[m]);
    return 0;
}
T15 摆花

显然设 \(f(i)\) 表示放 \(i\) 盆合法的方案。
思考转移。
类似多重背包显然先枚举每一个物品,再枚举 \(i\),最后枚举取几个。
显然 \(O(n^3)\) 可过。

#include<bits/stdc++.h>
using namespace std;
const int N=109,mod=1e6+7;
int dp[N],num[N],n,m;
int main(){
    scanf("%d%d",&n,&m),dp[0]=1; for(int i=1;i<=n;i++) scanf("%d",&num[i]);
    for(int i=1;i<=n;i++) for(int j=m;j>=1;j--) for(int k=1;k<=j&&k<=num[i];k++) 
        dp[j]=(dp[j]+dp[j-k])%mod;
    printf("%d",dp[m]);
    return 0;
}
T16 线段

首先每行之间是独立的。
第一条线段等价于 \((1,r_1)\),最后一条等价于 \((l_n,n)\)
那么理论上每一行最终只对应两个 \(x\) 值。
一通分类乱搞就能过。

#include<bits/stdc++.h>
using namespace std;
int l[20009],r[20009];
inline int lans(int lim,int rim,int pos){
    if(pos>rim) return pos-lim;
    else return rim-pos+rim-lim;
}
inline int rans(int lim,int rim,int pos){
    if(pos<lim) return rim-pos;
    else return pos-lim+rim-lim;
}
int main(){
    int n,ll,rr,dpl,dpr,tmpl,tmpr; scanf("%d",&n);
    for(int i=1;i<=n;i++) scanf("%d%d",&l[i],&r[i]);
    l[1]=ll=rr=1,r[n]=n,dpl=dpr=-1;
    for(int i=1;i<=n;i++){
        ++dpl,++dpr;
        tmpl=min(dpl+lans(l[i],r[i],ll),dpr+lans(l[i],r[i],rr)),tmpr=min(dpl+rans(l[i],r[i],ll),dpr+rans(l[i],r[i],rr));
        dpl=tmpl,dpr=tmpr;
        ll=l[i],rr=r[i];
    }
    printf("%d\n",dpr);
    return 0;
}
T17 金明的预算方案/【模板】分组背包

显然我们需要简化题意:
一共有 \(m(m\le 60)\) 个物品和 \(n(n<32000)\) 的容量,选物品 \(j\) 的价值是 \(v_i*\Delta_i\),体积是 \(v_i(v_i=10k,k<=1000)\)
然而,这个问题是有依赖的,每个物品至多依赖一个物品,每个物品至多被两个物品依赖,不存在嵌套依赖。

首先思考除掉一个 \(10\) 把数据范围变成 \(1000\)\(3200\)
依赖怎么办?
考虑我们对于每一个 主-副 依赖转化为一个分组背包。
我们至多也就是枚举一下每个选或不选,复杂度不会很高,极限情况三变四也就是 \(80\) 个物品分组背包,每组选一个。

分组背包怎么做?
还是我们设 \(f(i)\) 为使用 \(i\) 容量获得最大价值。
我们先枚举每一组,再枚举每一个 \(i\),最后枚举每一个物品。
他就和普通的 \(01\) 背包非常相似。

#include<bits/stdc++.h>
using namespace std;
const int N=75,M=3209;
int weight[N],value[N],fa[N],n,dp[M];
vector<int> k[N];
int main(){
    int m,a,b; scanf("%d%d",&n,&m),n/=10;
    for(int i=1;i<=m;i++) scanf("%d%d%d",&a,&b,&fa[i]),k[fa[i]].push_back(i),a/=10,weight[i]=a,value[i]=a*b;
    for(int i=1;i<=m;i++){
        if(fa[i]) continue;
        if(k[i].empty()){
            for(int j=n;j>=weight[i];j--) dp[j]=max(dp[j],dp[j-weight[i]]+value[i]);
        }else if(k[i].size()==1){
            a=k[i][0];
            for(int j=n;j>=weight[i];j--){
                dp[j]=max(dp[j],dp[j-weight[i]]+value[i]);
                if(j>=weight[i]+weight[a]) 
                    dp[j]=max(dp[j],dp[j-weight[i]-weight[a]]+value[i]+value[a]);
            }
        }else{
            a=k[i][0],b=k[i][1];
            for(int j=n;j>=weight[i];j--){
                dp[j]=max(dp[j],dp[j-weight[i]]+value[i]);
                if(j>=weight[i]+weight[a]) 
                    dp[j]=max(dp[j],dp[j-weight[i]-weight[a]]+value[i]+value[a]);
                if(j>=weight[i]+weight[b]) 
                    dp[j]=max(dp[j],dp[j-weight[i]-weight[b]]+value[i]+value[b]);
                if(j>=weight[i]+weight[a]+weight[b]) 
                    dp[j]=max(dp[j],dp[j-weight[i]-weight[a]-weight[b]]+value[i]+value[a]+value[b]);
            }
        }
    }
    printf("%d\n",dp[n]*10);
    return 0;
}
T18 kkksc03考前临时抱佛脚

显然每科之间独立。
问题等价转化成对于每科求最优解并加和。
然而我不会啊。
贪心?
比如说逆序排序,每次加入两个最大值。
试试。

#include<bits/stdc++.h>
using namespace std;
int k[70];
bool cmp(int a,int b){return a>b;}
int Ans(int n){
    if(n==1) return k[1];
    if(n==2) return max(k[1],k[2]);
    sort(k+1,k+n+1,cmp); int l=k[1],r=k[2],ans=0;
   	for(int i=3;i<=n;i++){
        if(l<=r) r-=l,ans+=l,l=k[i];
        else l-=r,ans+=r,r=k[i];
    }
    return ans+max(l,r);
}
int main(){
    int a,b,c,d,ans=0; scanf("%d%d%d%d",&a,&b,&c,&d);
    for(int i=1;i<=a;i++) scanf("%d",&k[i]); ans+=Ans(a);
    for(int i=1;i<=b;i++) scanf("%d",&k[i]); ans+=Ans(b);
    for(int i=1;i<=c;i++) scanf("%d",&k[i]); ans+=Ans(c);
    for(int i=1;i<=d;i++) scanf("%d",&k[i]); ans+=Ans(d);
    printf("%d",ans);
    return 0;
}

WA,0pt.
我们发现以前我们都是设序,但这个貌似不好设序。
等等。
原问题再次等价于,把原序列分成两组且差值最小。
这个一开始仍想不到什么好方法,直到你想到 this
我们只需要来一个相同的,对于每组维护一下最接近 \(n/2\) 的即可。
所以这题真的是橙?

#include<bits/stdc++.h>
using namespace std;
int k[70];
bitset<800> mp;
bool cmp(int a,int b){return a>b;}
int Ans(int n){
    int sum=0; mp.reset(),mp[0]=1;
    for(int i=1;i<=n;i++) sum+=k[i],mp|=(mp<<k[i]);
    for(int i=sum/2;i>=0;i--) if(mp[i]) return sum-i;
}
int main(){
    int a,b,c,d,ans=0; scanf("%d%d%d%d",&a,&b,&c,&d);
    for(int i=1;i<=a;i++) scanf("%d",&k[i]); ans+=Ans(a);
    for(int i=1;i<=b;i++) scanf("%d",&k[i]); ans+=Ans(b);
    for(int i=1;i<=c;i++) scanf("%d",&k[i]); ans+=Ans(c);
    for(int i=1;i<=d;i++) scanf("%d",&k[i]); ans+=Ans(d);
    printf("%d",ans);
    return 0;
}
简单 dp II

这一部分对应洛谷题单【动态规划 2】。
线性 dp 其实就是朴素 dp。
其余注意事项同上。

T1 导弹拦截

考虑设 \(f(i)\) 表示以 \(i\) 结尾的最长不升子序列长度。
显然 \(f(i)=\max\limits^{i-1}_{h(j)\ge h(i)}\{f(j)+1\}\)
暴力复杂度 \(O(n^2)\),无法通过。

简单直白的思路是优化找最小值的过程,
比如我们把 \(h\) 作为键值存入,
查询时查询 \([h,\inf)\) 范围内 \(dp\) 的最大值。
这个可以线段树,
但更常用也更方便的是树状数组。
本题唯一的难点是树状数组无法倒序遍历,而 \(\max\) 操作也不具有可减性。

显然可以直接倒过来找最长不降子序列。
时间复杂度 \(O(n\log n)\)

同时还可以考虑优化状态设计。
我们考虑这个思路没问题,但 \(f(i)\) 性质比较少,不容易优化。
而根据以前的思想,我们之所以不够快,还是因为我们没有将等价的东西充分合并。
由以上两点,考虑设 \(f(i)\) 为长度为 \(i\) 的序列最大结尾元素。

显然,我们对于本题只有两个限制:下标递增,高度不升。
用限制当状态的想法亦显然。

我们发现这个东西具有单调性,可以二分转移。

考虑第二问。
对于每一个新加入的元素,我们有两种选择:新建或是加入。
显然能加入的话新建并不优秀。
我们考虑加入哪一组,显然贪心的想加入”浪费空间最小的一组“最优秀。
而显然只当”大于最大值“才会加入,显然最终序列单调,考虑二分。

综上,时间复杂度 \(O(n\log n)\)

//Solution 01
#include<bits/stdc++.h>
using namespace std;
const int N=5e4,M=1e5;
int tree[N+9],k[M+9],h[M+9],cnt;
void modify(int i,int d){while(i<=N) tree[i]=max(tree[i],d),i+=(i&-i);}
int query(int i){int ans=0; while(i) ans=max(ans,tree[i]),i-=(i&-i); return ans;}
int main(){
	int n=1,ans=-1; while(scanf("%d",&k[n])!=EOF) n++; --n;
	for(int i=n,tmp;i>=1;i--) tmp=query(k[i]),ans=max(ans,tmp+1),modify(k[i],tmp+1); printf("%d\n",ans);
	for(int i=1;i<=n;i++) 
        if(cnt==0||k[i]>h[cnt]) h[++cnt]=k[i]; 
        else h[lower_bound(h+1,h+cnt+1,k[i])-h]=k[i];
	printf("%d\n",cnt);
	return 0;
} 

//Solution 02
#include<bits/stdc++.h>
using namespace std;
const int M=1e5;
int dp[M+9],k[M+9],h[M+9],cnt;
int main(){
	int n=1,ans=1,l,r,mid; while(scanf("%d",&k[n])!=EOF) n++; --n,dp[1]=k[1];
    for(int i=2;i<=n;i++){
        if(k[i]<=dp[ans]) dp[++ans]=k[i];
        else{
            l=1,r=ans;
            while(l<r){
                mid=(l+r)>>1;
                if(dp[mid]<k[i]) r=mid; else l=mid+1;
            }
            dp[l]=k[i];
        }
    }
    printf("%d\n",ans);
	for(int i=1;i<=n;i++) 
        if(cnt==0||k[i]>h[cnt]) h[++cnt]=k[i]; 
        else h[lower_bound(h+1,h+cnt+1,k[i])-h]=k[i];
	printf("%d\n",cnt);
	return 0;
} 
T2 打鼹鼠

考虑最暴力的想法:设 \(f(i,j)\) 为当前位置收益最大值。
思考每次枚举到一个有鼹鼠的时刻,就先暴力处理 \(dp\) 扩展并修改原数组。

我们如果从这个出发,发现可能唯一可能的解是矩乘快速幂+矩乘新定义。
然而数据并不支持我们强行拆点这么做。
我们发现此时需要优化状态设计。

发现很多节点都是无效节点,
换言之,我们存储了巨量的无效状态。
显然,我们只有每次获得收益后,这个信息才是有效的。

不难想到,可以设 \(dp_i\) 表示得到了第 \(i\) 只鼹鼠的收益时,总共能获得的最大收益。
这个转移就很方便了,用曼哈顿距离随便搞搞就行。

时间复杂度 \(O(m^2)\) 可以通过。

#include<bits/stdc++.h>
using namespace std;
const int N=1e4+7;
int dp[N],x[N],y[N],t[N];
int main(){
    int n,m,ans=0; scanf("%d%d",&n,&m);
    for(int i=1,tmp;i<=m;i++){
        scanf("%d%d%d",&t[i],&x[i],&y[i]),tmp=0;
        for(int j=1;j<i;j++) if(t[i]-t[j]>=abs(x[i]-x[j])+abs(y[i]-y[j])) tmp=max(tmp,dp[j]);
        dp[i]=tmp+1,ans=max(ans,dp[i]);
    }
    printf("%d\n",ans);
    return 0;
}
T3 琪露诺

显然设 \(f(i)\) 为当前格最大收益,转移显然。
暴力时间复杂度极限 \(O(n^2)\) 无法通过,思考优化。
显然可以线段树敲个板子水过,然而码量稍大。
考虑对于当前节点有一个神奇的限制,很像单调队列的样子。
可以实时进行滑动窗口存储答案,线性可过。

#include<bits/stdc++.h>
using namespace std;
const int N=2e5+7;
int q[N],ff=0,tt=-1,Max[N],dp[N],n,l,r,k;
int _Max(int i){
    if(i==-l) return 0;
    if(i>=0) return Max[i];
    return -1e9;
}
int main(){
    int ans=INT_MIN;
    scanf("%d%d%d",&n,&l,&r),k=r-l+1;
    for(int i=0;i<=n;i++){
        scanf("%d",&dp[i]);
        if(ff<=tt&&q[ff]+k<=i) ++ff; //pop illegal node
        dp[i]+=_Max(i-l);
        while(ff<=tt&&dp[q[tt]]<=dp[i]) --tt;
        q[++tt]=i;
        Max[i]=dp[q[ff]];
        // printf("%d %d\n",dp[i],Max[i]);
    } 
    for(int i=n-r+1;i<=n;i++) ans=max(ans,dp[i]);
    printf("%d\n",ans);
    return 0;
}
T4 大师

显然,暴力设 \(f(i,j)\) 表示以第 \(i\) 项为结尾公差为 \(j\) 的当前方案数。
转移就枚举前一个数,然后暴力转移。
综上可过。
为了便于处理内存和负数,我们采用 umap 存储。

#include<bits/stdc++.h>
using namespace std;
const int N=1009,p=998244353;
unordered_map<int,int> dp[N];
int n,h[N],ans;
int main(){
    scanf("%d",&n); for(int i=1;i<=n;i++) scanf("%d",&h[i]);
    for(int i=1;i<=n;i++){
        ans=(ans+1)%p;
        for(int j=1,delta;j<i;j++){
            delta=h[j]-h[i];
            if(!dp[i].count(delta)) dp[i][delta]=1;
            if(!dp[j].count(delta)) dp[i][delta]=(dp[i][delta]+1)%p;
            else dp[i][delta]=(dp[i][delta]+dp[j][delta])%p;
        }
        for(auto j:dp[i]) ans=(ans+j.second-1)%p;
    }
    printf("%d\n",ans);
    return 0;
}
T5 快速求和

考虑设 \(f(i,j)\) 表示在第 \(i\) 个位置获得数字 \(j\) 的最小代价。
思考转移。
首先枚举当前位置和当前数字。
然后枚举向前最多五位进行转移。
时间复杂度 \(O(|s|n\lg n)\) 可过。

#include<bits/stdc++.h>
using namespace std;
const int N=50,M=1e5+3;
int dp[N][M],n,S;
char s[N];
int main(){
	long long base,tmp;
    scanf("%s%d",s+1,&n),S=strlen(s+1);
    for(int i=1;i<=n;i++) dp[0][i]=1e8;
    for(int i=1;i<=S;i++) for(int j=0;j<=n;j++){
        dp[i][j]=1e8,tmp=s[i]-'0',base=10;
        for(int k=i-1;tmp<=j;tmp+=(s[k--]-'0')*base,base*=10){
        	dp[i][j]=min(dp[i][j],dp[k][j-tmp]+1);
        	if(k==0) break;
		}
    }
    printf("%d\n",(dp[S][n]-1>1e5)?-1:dp[S][n]-1);
    return 0;
}
T6 编辑距离

这题不好想,思考暴力想法。
发现 \(O(n^2)\) 能过,可以猜测设 \(f(i,j)\) 为让第一个串前 \(i\) 位和第二个串前 \(j\) 位匹配成功所需最小操作次数。
思考转移。

  • 对于最后一个元素修改:\(f(i-1,j-1)+[A_i=B_j]\)
  • 插入一个新元素,我们发现等价于直接考虑在末尾插入:\(f(i,j-1)+1\)
  • 删除一个元素,同上:\(f(i-1,j)-1\)

思考边界情况,有 \(f(0,k)=f(k,0)=k\)

这题 luogu 只有黄,但是思想很重要。
这题启示我们,其实很多时候我们都可以把”任意位置操作“改成”递推末尾操作“,从而简化转移。

#include<bits/stdc++.h>
using namespace std;
char A[2009],B[2009];
int a,b,dp[2009][2009];
int main(){
    scanf("%s%s",A+1,B+1),a=strlen(A+1),b=strlen(B+1);
    for(int i=1;i<=a;i++) dp[i][0]=i;
    for(int i=1;i<=b;i++) dp[0][i]=i;
    for(int i=1;i<=a;i++) for(int j=1;j<=b;j++) 
        dp[i][j]=min(min(dp[i][j-1],dp[i-1][j])+1,dp[i-1][j-1]+1-(A[i]==B[j]));
    printf("%d",dp[a][b]);
    return 0;
}
T7 【模板】LCS

\(\text{Longest Common Subsequence,short for LCS.}\)
考虑设 \(f(i,j)\) 为这一段的 LCS 长度,则显然有转移:
\(f(i,j)=\max\{f(i-1,j),f(i,j-1),f(i-1,j-1)+[A_i=B_j]\}\)

这个是 \(O(n^2)\) 的,考虑优化。
优化有一个基础策略:空间高于时间。
我们对于一个问题,时间复杂度优化方法较多,然而空间并非,所以一般先优化空间后优化时间。

显然可以滚动数组,但我们想压缩成一维。
我们思考去掉 \(i\) 这一维,第一个数直接就是所存储的值。
后面我们发现他需要两种不同的转移顺序,于是思考如下的方式:

for(int i=1;i<=n;i++){
    for(int j=n;j>=1;j--)
        dp[j]=max(dp[j],dp[j-1]+(A[i]==B[j]));
	for(int j=1;j<=n;j++)
        dp[j]=max(dp[j],dp[j-1]);
}

怎么优化呢?
思考发现 \(A_i=B_j\) 的情况极少。
那我们其实这个循环只是一次单点修改,
我们预处理这个信息即可做到 \(O(1)\)
然而并没有什么用,
下面的区间前缀 \(\max\) 成为了时间复杂度瓶颈。
怎么办?

又是一个经典思路:平衡!
我们一个操作快一个操作慢,
那相当于快的操作等于白优化了。
我们在这时就可以采取平衡时间复杂度的思想。

前缀 \(\max\),单点修改,树状数组呼之欲出。
我们每次只会改一个点,显然求一次前缀 \(\max\) 再来一次单修即可。
这个就很简单啦~

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+7;
int t[N],A[N],B[N],pos[N],n;
inline int query(int i){int ans=0; while(i) ans=max(ans,t[i]),i-=(i&-i); return ans;}
inline void modify(int i,int d){while(i<=n) t[i]=max(t[i],d),i+=(i&-i);}
int main(){
    scanf("%d",&n);
    for(int i=1;i<=n;i++) scanf("%d",&A[i]);
    for(int i=1;i<=n;i++) scanf("%d",&B[i]),pos[B[i]]=i;
    for(int i=1;i<=n;i++) modify(pos[A[i]],query(pos[A[i]]-1)+1);
    printf("%d\n",query(n));
    return 0;
}

然而,有的题可能是其变种,比如这道:

小明学了二分图匹配的算法。

现在小明想了一个问题:他有两种类型的点—— \(n\) 个红点、\(m\) 个蓝点,红点编号为 \(1\sim n\),蓝点编号为 \(1\sim m\)
每个点都有一个属性值,用大写字母 \(A\sim Z\) 表示,每次要将属性相同蓝点和红点进行匹配操作,由于是匹配,所以每个点只能匹配一次。
另外小明希望匹配的情况尽可能简单,所以每次匹配的结点不能存在交叉的情况。
即如果出现红点 \(i_1\) 与蓝点 \(j_1\) 匹配,红点 \(i_2\) 与蓝点 \(j_2\) 匹配,且满足 \(i_1<i_2\) ,那么一定有 \(j_1<j_2\)

现在小明想知道最多有多少组匹配。

【输入格式】

第一行一个整数 \(n,m\)

接下来一行,一个长度为 \(n\) 的字符串 \(S_1\),每个字符为 \(A\sim Z\) 中一个,第 \(i\) 个字符表示红点 \(i\) 的属性。

接下来一行,一个长度为 \(m\) 的字符串 \(S_2\),每个字符为 \(A\sim Z\) 中一个,第 \(i\) 个字符表示蓝点 \(i\) 的属性。

【输出格式】

一个整数,表示最多的匹配数

【样例 \(1\) 输入】
2 5
AB
BAABB
【样例 \(1\) 输出】
2

样例解释: \(S_1\) 中的 A 可以与 \(S_2\) 中第二个字符 A 匹配,\(S_1\) 中的 B 可以与 \(S_2\) 中的第四个字符 B 匹配

【样例 \(2\)

见下发文件

【样例 \(3\)

见下发文件

【子任务】

对于\(30\%\)的数据,\(1\le n,m\le 10^3\)

对于另外 \(20\%\) 的数据,保证 \(S_1\) 中字符都相同。

对于\(100\%\)的数据,\(1\le n\le 10^3,1\le m\le 10^6\)

显然直接找 LCS。
问题是每次匹配位置不唯一。
再看一下这段代码。

for(int i=1;i<=n;i++){
    for(int j=n;j>=1;j--)
        dp[j]=max(dp[j],dp[j-1]+(A[i]==B[j]));
	for(int j=1;j<=n;j++)
        dp[j]=max(dp[j],dp[j-1]);
}

发现我们并不能复用思路,因为我们不再满足以前的一一对应关系。
显然,树状数组优化 be like:

1000 1000000
aaa...
aaaaaa...

这种毒瘤数据下,我们的时间复杂度会被卡到 \(O(nm\log n)\),无法通过。

我们有几个好一些的性质。

  1. 字符集很小。
  2. \(n\) 很小可以跑平方。
  3. \(dp\) 数组单调性。
  4. 答案不超过 \(n\)

考虑优化状态设计。

还记得我们写 LIS (\(\text{Longest Increasing Subsequence}\))的时候的思路吗?
限制是下标和值都要递增。
自然想到去维护 \(dp_i\) 维护答案为 \(i\) 时对应下标最小值。

然而发现很难进行转移。
我们的信息太少难以处理。
于是我们考虑增加状态来简化问题。

根据优化性质二 & 四,想到对于 \(f(i,j)\) 设其为答案为 \(i\),短串中下标为 \(j\) 的最小长串下标。
怎么转移?
对于 \(f(i,j)\),他可能从哪里来?
\(f(i-1,x)\quad(x<j)\)
那我们只需要找一个最小的 \(pos\),使得其满足:\(\begin{cases}pos>\min f(i-1,x)\\ A_{j}=B_{pos}\end{cases}\)

我们预处理出 \(B\) 中每个字母出现的位置存入一个 vector
然后先枚举答案后枚举短串,递推过程中记录最小值。
每次在对应 vector 中二分即可。
如果不存在满足条件的值,就报告无解。
如果 \(\forall j,f(i,j)\) 无解,就证明答案为 \(i-1\)
发现我们开两维用处不大,可以直接压成一个 \(f(i)\) 记录最小对应下标。

考虑边界。
非法情况设为 \(\inf\)
显然 \(f(i,j)=\inf(j>i)\)
对于 \(f(1,k)\) 的情况,显然直接用上一层的 \(0\) 当限制即可。
注意更改顺序。

分析一下时间复杂度。
首先枚举答案和短串,\(O(n^2)\)
其次进行二分,极限 \(O(\log m)\)
理论时间复杂度 \(O(n^2\log m)\),可以通过。

思想:
构造极限数据测试是否能过。
寻找特殊性质,如特别小的数据范围。
根据限制设状态。
卡死时空复杂度,无法通过就分析当前时间复杂度瓶颈。

#include<bits/stdc++.h>
using namespace std;
const int N=1e3+7,M=1e6+7,K=30,inf=1e9+7;
int n,m,dp[N]; char A[N],B[M];
vector<int> posB[K];
int main(){
	scanf("%d%d%s%s",&n,&m,A+1,B+1);
	for(int i=1;i<=m;i++) posB[B[i]-'A'].push_back(i);
	for(int i=1,tmp,flg;i<=n;i++){
		flg=0,tmp=dp[1];
		for(int j=1;j<i;j++) tmp=min(tmp,dp[j]),dp[j]=inf;
		for(int j=i,data,idx;j<=n;j++){
			idx=A[j]-'A';
			if(posB[idx].empty()||tmp>=posB[idx].back()) {tmp=min(tmp,dp[j]),dp[j]=inf; continue;}
			flg=1,data=*upper_bound(posB[idx].begin(),posB[idx].end(),tmp);
			tmp=min(tmp,dp[j]),dp[j]=data;
		}
		if(!flg) printf("%d\n",i-1),exit(0);
	}
	printf("%d\n",n);
	return 0;
}
T8 子串

同编辑距离一题。
考虑设 \(f(i,j,k)\) 为将 \(A\)\(i\) 位与 \(B\)\(j\) 位匹配花费为 \(k\) 的方案。
显然,\(f(i,j,k)=f(i-1,j,k)+\sum f(i-\Delta,j-\Delta,k-1)\)
发现只需倒序枚举就可将 \(k\) 压缩掉。
字符串哈希优化?

并不需要,递推过程中判断即可。

时间复杂度 \(O(m^3n)\),并不能通过。
考虑优化状态设计。
我们还是那题思路:任意位置 \(\to\) 递推末尾。
\(f(i,j,0/1,k)\) 为原状态(0/1 代表取不取最后一个),显然:
\(\begin{cases}f(i,j,0,k)=f(i-1,j,0,k)+f(i-1,j,1,k)\\ f(i,j,1,k)=[A_i=B_j](f(i-1,j-1,1,k)+f(i-1,j-1,1,k-1)+f(i-1,j-1,0,k-1))\end{cases}\)
倒序枚举,压缩掉第一维。

#include<bits/stdc++.h>
#define f(i,a,b) for(register int i=a;i<=b;i++)
#define F(i,a,b) for(register int i=a;i>=b;i--)
using namespace std;
const int N=1e3+7,M=2e2+7,Mod=1e9+7;
int n,m,t,dp[M][M][2];
char A[N],B[M];
int main(){
    scanf("%d%d%d%s%s",&n,&m,&t,A+1,B+1);
    f(i,1,n) F(j,m,1) f(k,1,t){
        dp[j][k][0]=(dp[j-1][k][0]+dp[j-1][k][1])%Mod;
        dp[j][k][1]=(A[i]==B[j])?((dp[j-1][k][1]+dp[j-1][k-1][1])%Mod+dp[j-1][j-1][0])%Mod:0;
    }
    printf("%d\n",(dp[j][k][0]+dp[j][k][1])%Mod);
    return 0;
}
T9 三倍经验

显然设 \(f(i,j,k)\) 设在 \((i,j)\) 时已经用过 \(k\) 次机会。
转移显然。

#include<bits/stdc++.h>
#define int long long
using namespace std;
typedef long long ll;
const int N=109;
ll mp[N][N],dp[N][N][N];
signed main(){
    int n,k; ll ans=LLONG_MIN; scanf("%d%d",&n,&k),k=min(n,k);
    for(int i=1;i<=n;i++) for(int j=1;j<=i;j++) scanf("%lld",&mp[i][j]);
    for(int i=1;i<=n;i++) for(int j=0;j<=i+1;j++) for(int l=0;l<=k;l++) dp[i][j][l]=-1e15;
    for(int i=1;i<=n;i++) for(int j=1;j<=i;j++) for(int l=0;l<=k&&l<=i;l++) {
        dp[i][j][l]=max(dp[i-1][j][l],dp[i-1][j-1][l])+mp[i][j];
        if(l>0) dp[i][j][l]=max(dp[i][j][l],max(dp[i-1][j][l-1]+mp[i][j]*3,dp[i-1][j-1][l-1]+mp[i][j]*3));
    }
    for(int x=1;x<=n;x++) for(int i=0;i<=k;i++) ans=max(ans,dp[n][x][i]);
    printf("%lld\n",ans);
    return 0;
}
T10 方格取数

贪心的想就是跑两次最优解,合并后依然是最优解。
跑最优解显然 dp 可过。
只不过需要记录路径,这个是简单的。

#include<bits/stdc++.h>
#define f(i,a,b) for(int i=a;i<=b;i++)
using namespace std;
typedef long long ll;
const int N=10098;
ll dp[N][N],mp[N][N];
int pre[N][N];
int main(){
   	int n,x,y; ll d,ans; scanf("%d",&n);
    while(scanf("%d%d%lld",&x,&y,&d)!=EOF) mp[x][y]=d;
    for(int i=2;i<=n;i++) dp[0][i]=dp[i][0]=-1e10;
    f(i,1,n) f(j,1,n){
        dp[i][j]=mp[i][j]+max(dp[i-1][j],dp[i][j-1]);
        pre[i][j]=(dp[i-1][j]>dp[i][j-1]);
    }
    ans=dp[n][n],x=n,y=n;
    while(x>0&&y>0){
        mp[x][y]=0;
        if(pre[x][y]==0) --y; else --x;
    }
    f(i,1,n) f(j,1,n) dp[i][j]=mp[i][j]+max(dp[i-1][j],dp[i][j-1]);
    printf("%lld\n",ans+dp[n][n]);
    return 0;
}

然而这并不正确。
如图:

\[\begin{bmatrix} \color{red}10^9 & \color{red}10^9 & \color{red}10^9 & 0 & 0\\ \color{green}1 & \color{green}1 & \color{red}10^9 & \color{red}1 & \color{red}1\\ 0 & \color{green}1 & \color{green}10^9 & 0 & \color{red}1\\ 0 & 0 & \color{green}10^9 & 0 & \color{red}1\\ 0 & 0 & \color{green}10^9 & \color{green}10^9 & \color{red}10^9\\ \end{bmatrix} \]

显然,贪心的想法是走中间的 \(10^9\) 路径,
然而我们可以证明,不存在一条路径,使得一次取光所有的 \(1\)
然而显然有如图所示的构造使得所有的数字均被取。

那怎么办?
注意到 \(n\le8\) 可以乱搞。
然而 \(2^{32}\) 暴力仍然过不了。

考虑对全局进行 dp。
思考把所有东西都加入状态,
\(f(a,b,c,d)\) 表示两个点分别为 \((a,b)\)\((c,d)\) 的方案数。
显然重合时一定在同一状态里可以转移。
综上得解。

#include<bits/stdc++.h>
using namespace std;
int dp[10][10][10][10],mp[10][10];
inline int f(int a,int b,int c,int d){
    return max(dp[a-1][b][c-1][d],max(dp[a-1][b][c][d-1],max(dp[a][b-1][c-1][d],dp[a][b-1][c][d-1])));
}
int main(){
    int n,a,b,c,d; scanf("%d",&n);
    while(scanf("%d%d%d",&a,&b,&c)!=EOF) mp[a][b]=c;
    dp[1][1][1][1]=mp[1][1];
    for(int sum=2;sum<=n+n;sum++) for(a=1;a<=min(sum,n);a++) for(c=1;c<=min(sum,n);c++){
        b=sum-a,d=sum-c;
        if(a==c) dp[a][b][c][d]=mp[a][b]+f(a,b,c,d);
        else dp[a][b][c][d]=mp[a][b]+mp[c][d]+f(a,b,c,d);
    }
    printf("%d\n",dp[n][n][n][n]);
    return 0;
}
T11 合唱队形

考虑对于左右分别维护 LIS。
枚举中间即可。

#include<bits/stdc++.h>
using namespace std;
const int N=109;
int h[N],fdp[N],rdp[N],n;
int main(){
    int ans=INT_MIN;
    scanf("%d",&n),h[0]=h[n+1]=INT_MIN;
    for(int i=1;i<=n;i++) scanf("%d",&h[i]);
    for(int i=1;i<=n;i++) for(int j=0;j<i;j++) if(h[j]<h[i]) fdp[i]=max(fdp[i],fdp[j]+1);
    for(int i=n;i>=1;i--) for(int j=n+1;j>i;j--) if(h[j]<h[i]) rdp[i]=max(rdp[i],rdp[j]+1);
    for(int i=1;i<=n;i++) ans=max(ans,fdp[i]+rdp[i]-1);
    printf("%d\n",n-ans);
    return 0;
}
T12 回文字串

看到这题没什么好思路。
如果不想硬写字符串哈希的话,枚举回文中心是一个很好的方法。
注意到如果回文中心是后来插入的话严格劣于不插入,
于是考虑枚举中心。
然后就变成了一个简单的 dp。

思考转移:
相等的话考虑不加贡献直接插入,
否则在左右分别考虑补一个插入,
如果一边已经没有了就也可以考虑一边读进来一个另一边进行插入。

这个显然可过,基本不等式可得一次运算次数 \(500\times500=2.5\times10^5\)
那总运算次数就小于 \(2.5\times10^8\) 远远跑不满显然能过。
只不过奇偶分别处理有点麻烦,然而这个不能插虚点否则必然 TLE。

细节较多。

#include<bits/stdc++.h>
using namespace std;
const int N=1009;
char S[N];
int n,dp[N][N];
int main(){
    int ans=INT_MAX;
    scanf("%s",S+1),n=strlen(S+1);
    //real middle
    for(int i=1,lim,rim;i<=n;i++){
        lim=i-1,rim=n-i;
        for(int j=1;j<=lim;j++) dp[j][0]=j;
        for(int j=1;j<=rim;j++) dp[0][j]=j;
        for(int j=1;j<=lim;j++) for(int k=1;k<=rim;k++){
            dp[j][k]=min(dp[j-1][k],dp[j][k-1])+1;
            if(i-j>=1&&i+k<=n&&S[i-j]==S[i+k]) dp[j][k]=min(dp[j][k],dp[j-1][k-1]);
            else if(i-j<1||i+k>n) dp[j][k]=min(dp[j][k],dp[j-1][k-1]+1);
        }
        ans=min(ans,dp[lim][rim]);
    }
    //imagine middle(before)
    for(int i=1,lim,rim;i<=n+1;i++){
        lim=i-1,rim=n-i+1;
        for(int j=1;j<=lim;j++) dp[j][0]=j;
        for(int j=1;j<=rim;j++) dp[0][j]=j;
        for(int j=1;j<=lim;j++) for(int k=1;k<=rim;k++){
            dp[j][k]=min(dp[j-1][k],dp[j][k-1])+1;
            if(i-j>=1&&i+k-1<=n&&S[i-j]==S[i+k-1]) dp[j][k]=min(dp[j][k],dp[j-1][k-1]);
            else if(i-j<1||i+k-1>n) dp[j][k]=min(dp[j][k],dp[j-1][k-1]+1);
        }
        ans=min(ans,dp[lim][rim]);
    }
    printf("%d\n",ans);
    return 0;
}

然而,这个并非正解。
原问题等价于求最长回文子串,从而相当于对于正序字串和倒序字串求 LCS。
数据范围唐式平方可过。

#include<bits/stdc++.h>
using namespace std;
const int N=1009;
char a[N],b[N]; int n,dp[N][N];
int main(){
	scanf("%s",a+1),n=strlen(a+1);
	for(int i=1;i<=n;i++) b[i]=a[n+1-i];
	for(int i=1;i<=n;i++) for(int j=1;j<=n;j++){
		dp[i][j]=max(dp[i][j-1],max(dp[i-1][j],dp[i-1][j-1]+(a[i]==b[j])));
	}
	printf("%d\n",n-dp[n][n]);
	return 0;
}

正如 LCS 中所写的,我们可以压掉一维并采取正反两种方式进行枚举。

#include<bits/stdc++.h>
using namespace std;
const int N=1009;
char a[N],b[N]; int n,dp[N];
int main(){
	scanf("%s",a+1),n=strlen(a+1);
	for(int i=1;i<=n;i++) b[i]=a[n+1-i];
	for(int i=1;i<=n;i++){
		for(int j=n;j>=1;j--)
			dp[j]=max(dp[j],dp[j-1]+(a[i]==b[j]));
		for(int j=1;j<=n;j++)
			dp[j]=max(dp[j],dp[j-1]);
	}
	printf("%d\n",n-dp[n]);
	return 0;
}

这个思路从实现难度和时间上都更优秀。
这启示我们可以在原问题复杂的时候,思考反向操作,将符合条件的抽出来,从而得到答案。

T13 花店橱窗布置

显然设 \(f(i,j)\) 表示将前 \(i\) 朵花插进前 \(j\) 个花瓶的最大收益。
显然应从 \(f(i-1,j')\) 转移而来。
暴力扫一次取最大值即可。
时间复杂度 \(O(n^3)\) 可以通过。

#include<bits/stdc++.h>
using namespace std;
int n,v,dp[105][105],mp[105][105];
struct node{int x,y; node(){x=y=0;} node(int _x,int _y){x=_x,y=_y;}};
node pre[106][106];
void dfs(int a,int b){
    if(a==0) return;
    dfs(pre[a][b].x,pre[a][b].y);
    if(a!=pre[a][b].x) printf("%d ",b);
}
int main(){
    scanf("%d%d",&n,&v); 
    for(int i=1;i<=n;i++) for(int j=1;j<=v;j++) scanf("%d",&mp[i][j]);
    for(int i=1;i<=v;i++) for(int j=0;j<=n;j++) dp[i][j]=-1e9;
    for(int i=1;i<=n;i++) for(int j=1;j<=v;j++){
        dp[i][j]=dp[i][j-1],pre[i][j]=node(i,j-1);
        for(int k=j-1;k>=0;k--){
            if(dp[i][j]<dp[i-1][k]+mp[i][j]) dp[i][j]=dp[i-1][k]+mp[i][j],pre[i][j]=node(i-1,k);
        }
    } 
	printf("%d\n",dp[n][v]);
    dfs(n,v);
    return 0;
}
T14 樱花

题意简述:给定背包容量 \(V\) 和若干种物品,每个物品有若干个,求最大收益。
其实就是裸题多重背包。
对于完全背包情况可以特判优化。

时间复杂度 \(O(nV\sum cnt)\),理论的极限是接近 \(10^{14}\) 的运算次数,并不能通过。
虽然数据水不开 O2 也过了。

#include<bits/stdc++.h>
using namespace std;
const int N=1086;
int n,V,dp[N];
int main(){
	int a,b,c,d;
	scanf("%d:%d %d:%d %d",&a,&b,&c,&d,&n),V=(c-a)*60+(d-b);
	for(int i=1,w,v,c;i<=n;i++){
		scanf("%d%d%d",&w,&v,&c),c=(c==0)?V/w:c;
		for(int j=V;j>=w;j--) for(int k=1;k*w<=j&&k<=c;k++)
			dp[j]=max(dp[j],dp[j-k*w]+k*v);
	}
	printf("%d\n",dp[V]);
	return 0;
}

可以加一个二进制拆分优化。

#include<bits/stdc++.h>
using namespace std;
const int N=1086;
int n,V,dp[N];
int main(){
	int a,b,c,d;
	scanf("%d:%d %d:%d %d",&a,&b,&c,&d,&n),V=(c-a)*60+(d-b);
	for(int i=1,w,v,c;i<=n;i++){
		scanf("%d%d%d",&w,&v,&c),c=(c==0)?V/w:c;
		for(int j=1;j<=c;j<<=1){
			c-=j;
			for(int k=V;k>=j*w;k--) dp[k]=max(dp[k],dp[k-j*w]+j*v);
		}
		if(c) for(int k=V;k>=c*w;k--) dp[k]=max(dp[k],dp[k-c*w]+c*v);
	}
	printf("%d\n",dp[V]);
	return 0;
}

接近线性。

T15 Cow Exhibition G

三条限制:智商和非负,情商和非负,最大化总和。
注意到,如果一头牛智商和情商均非正一定不选,均非负则一定选。

状态设什么?
考虑有几种构造:

  • \(f(i),g(i)\) 表示答案为 \(i\) 时能取到的最大智商和情商。
    显然错误,我们难以转移。
  • \(f(i,j)\) 表示两维的情况是否存在。
    复杂度不可接受。
  • \(f(i)\) 表示每种智商和对应的最大情商和。
    这个合理很多,满足分批要求。
    转移考虑直接暴力转移,时间可以接受。

考虑细节。
需要记录所有的状态,考虑哈希表。

#include<bits/stdc++.h>
using namespace std;
unordered_map<int,int> dp,tmp;
int main(){
	int n,ans=-1; scanf("%d",&n),dp[0]=0;
	for(int i=1,s,f;i<=n;i++){
		scanf("%d%d",&s,&f);
		if(s+f<0) continue;
		tmp=dp;
		for(auto j:dp)
			if(!tmp.count(j.first+s)) tmp[j.first+s]=j.second+f;
			else tmp[j.first+s]=max(tmp[j.first+s],j.second+f);
		dp=tmp;
	}
	for(auto j:dp) if(j.first>=0&&j.second>=0) ans=max(ans,j.first+j.second);
	printf("%d",ans);
	return 0;
}
T16 乌龟棋

考虑设 \(dp(a,b,c,d)\) 表示在四种卡片各剩余对应张数时对应的最大分数。
转移显然。

#include<bits/stdc++.h>
using namespace std;
const int N=42,M=350;
int dp[N][N][N][N],mp[M],n,m,cnt[6];
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++) scanf("%d",&mp[i]);
	for(int i=1,j;i<=m;i++) scanf("%d",&j),cnt[j]++;
	for(int a=cnt[1];a>=0;a--)
		for(int b=cnt[2];b>=0;b--)
			for(int c=cnt[3];c>=0;c--)
				for(int d=cnt[4];d>=0;d--){
					if(a<cnt[1]) dp[a][b][c][d]=max(dp[a][b][c][d],dp[a+1][b][c][d]);
					if(b<cnt[2]) dp[a][b][c][d]=max(dp[a][b][c][d],dp[a][b+1][c][d]);
					if(c<cnt[3]) dp[a][b][c][d]=max(dp[a][b][c][d],dp[a][b][c+1][d]);
					if(d<cnt[4]) dp[a][b][c][d]=max(dp[a][b][c][d],dp[a][b][c][d+1]);
					dp[a][b][c][d]+=mp[a+b+b+c+c+c+d+d+d+d+1];
				}
	printf("%d\n",dp[0][0][0][0]);
	return 0;
}
T17 爵士好题

平方显然。
考虑一种状态设计:\(f(i,j)\) 表示答案为 \(i\) 时二进制位第 \(j\) 位有无可能为 \(1\)
转移呢?
这个东西显然单调可以二分。
或者就把他压进一个 int 里面。
转移就或上。

然而发现还需要跑前缀或。。。
瓶颈复杂度依旧平方。

#include<bits/stdc++.h>
using namespace std;
int dp[100009],n;
int main(){
	dp[0]=INT_MAX,scanf("%d",&n);
	for(int i=1,x,l,r,mid;i<=n;i++){
		scanf("%d",&x),l=0,r=i-1;
		while(l<r){
			mid=(l+r+1)>>1;
			if(dp[mid]&x) l=mid;
			else r=mid-1;
		}
		for(int j=1;j<=l+1;j++) dp[j]|=x;
	}
	for(int i=1;i<=n;i++) if(dp[i]==0) printf("%d\n",i-1),exit(0);
	return 0;
}

这个大概用线段树去搞,区间操作,单点查询是可以做到 \(O(n\log^2n)\) 的,应该可过。
然而以前的暴力可以优化。
暴力大概是:

for(int i=1;i<=n;i++)
    for(int j=0;j<i;j++)
        if(mp[j]&mp[i]) tmp=max(tmp,dp[j]+1)
	dp[i]=tmp+1;
...

考虑对于转移前缀优化。
\(b(i)\) 表示当前在二进制位第 \(i\) 位为一的情况下所能够获得的最大 \(dp\) 值,然后转移过程中维护。
综上时间复杂度 \(O(n\log V)\)

我们其实还是考虑平衡时间复杂度。
转移 \(O(n)\) 填数 \(O(1)\)
然而会发现我们每次填数后可以前缀的维护一些东西,
然后降低掉转移的时间复杂度到 \(O(\log V)\)

#include<bits/stdc++.h>
using namespace std;
const int N=100009,K=29;
int b[K+5],dp[N],n,ans=0;
int main(){
	scanf("%d",&n);
	for(int i=1,tmp;i<=n;i++){
		dp[i]=0,scanf("%d",&tmp);
		for(int j=0;j<=K;j++) if(tmp&(1<<j)) dp[i]=max(dp[i],b[j]);
		++dp[i];
		for(int j=0;j<=K;j++) if(tmp&(1<<j)) b[j]=max(b[j],dp[i]);
	}
	for(int i=1;i<=n;i++) ans=max(ans,dp[i]);
	printf("%d\n",ans);
	return 0;
}
T18 248 G & 262144 P

我们先做一下弱化版 \(N\le 248\)
这个支持立方,显然想到进行区间 dp。
然而,我们不能保证一段区间能够最终收缩成为一个单点。

考虑如果一个区间能够缩成单点,
我们就可以将其记录为 \(f(i,j)\)
这个转移是容易的,答案在转移时顺便记录就可以了。
时间复杂度 \(O(n^3)\)

这道题可能一开始由于区间不一定具有合适的性质,
使得我们难以设置状态。
然而会发现,我们并不要求所有的区间都具有某种性质(如能够缩成一个单点),
我们只对与答案相关的区间才有此要求。
这也是一个重要的思想。

#include<bits/stdc++.h>
using namespace std;
const int N=250;
int dp[N][N],mp[N],n,ans;
int main(){
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    	scanf("%d",&mp[i]),dp[i][i]=mp[i],ans=max(ans,mp[i]);
    for(int k=2;k<=n;k++)
    	for(int l=1,r;l+k-1<=n;l++){
    		r=l+k-1;
    		for(int mid=l;mid<r;mid++)
    			if(dp[l][mid]>0&&dp[l][mid]==dp[mid+1][r])
    				dp[l][r]=max(dp[l][r],dp[l][mid]+1);
    		ans=max(ans,dp[l][r]);
    	}
    printf("%d\n",ans);
    return 0;
}

考虑对于完全版数据优化状态设计。
首先,发现原来的状态中存在大量空信息。
结合答案极小,
不难想到设 \(f(i,j)\) 表示答案为 \(i\) 左端点为 \(j\) 对应的可行右端点。
考虑转移。
类似倍增算法,可以想到转移方程 \(f(i,j)=f(i-1,f(i-1,j)+1)\)

当答案很小的时候,
如果空状态数过多,
就考虑把答案存进状态,
然后枚举状态的一维直接存储另一维状态。

这是一个“收紧状态,减少冗余”的思想,简记为“系鞋带优化”。
事实上,我们写过的 LIS,LCS 都用过这一优化。

#include<bits/stdc++.h>
using namespace std;
const int N=270000,M=60;
int mp[N],dp[M][N],n,ans;
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++) scanf("%d",&mp[i]),dp[mp[i]][i]=i;
	for(int k=2;k<=58;k++) for(int i=1;i<=n;i++) if(mp[i]<k) dp[k][i]=(dp[k-1][i]!=0)?dp[k-1][dp[k-1][i]+1]:0;
	for(int k=1;k<=58;k++) for(int i=1;i<=n;i++) if(dp[k][i]) ans=k;
	printf("%d\n",ans);
	return 0;
}
区间 dp

对应洛谷题单【动态规划 3】。
区间 dp 一般是通过合并小区间信息从而得到大区间信息。
所以在 dp 时一般搜索顺序从短到长。
大部分朴素的区间 dp 时间复杂度 \(O(n^3)\)

T1 石子合并弱化

朴素区间 dp,设 \(f(i,j)\) 表示这部分合并后的最小贡献,转移显然。
可以用前缀和求区间和

#include<bits/stdc++.h>
using namespace std;
const int N=320;
int dp[N][N],mp[N],n;
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++) scanf("%d",&mp[i]),mp[i]+=mp[i-1];
	for(int k=2;k<=n;k++)
		for(int l=1,r;l+k-1<=n;l++){
			r=l+k-1,dp[l][r]=INT_MAX;
			for(int mid=l;mid<r;mid++)
				dp[l][r]=min(dp[l][r],dp[l][mid]+dp[mid+1][r]+mp[r]-mp[l-1]);
		}
	printf("%d\n",dp[1][n]);
	return 0;
}
T2 Zuma

考虑状态设计不难,三次方可过,应当还是 \(f(i,j)\) 表示最小消除次数。

我们的关键是刻画操作。

  • 分别处理
    直接合并。

  • 对于一个次级序列在外面裹一层。
    这个看似需要分类讨论,但其实:

    • 左右已经裹了一层
      不做处理。
    • 左右拼合而成
      先处理一部分,然后递归。

    综上,直接继承即可。

#include<bits/stdc++.h>
using namespace std;
const int N=505;
int n,f[N][N],mp[N];
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++) for(int j=i;j<=n;j++) f[i][j]=1e9;
	for(int i=1;i<=n;i++) scanf("%d",&mp[i]),f[i][i]=f[i][i-1]=1;
	for(int k=2;k<=n;k++){
		for(int l=1,r;l+k-1<=n;l++){
			r=l+k-1;
			if(mp[l]==mp[r]) f[l][r]=f[l+1][r-1];
			for(int mid=l;mid<r;mid++) f[l][r]=min(f[l][r],f[l][mid]+f[mid+1][r]);
		}
	}
	printf("%d\n",f[1][n]);
	return 0;
}
T3 合唱队

考虑暴力 dp。
\(l(l,r),r(l,r)\) 表示区间 \([l,r]\) 最后一个数字(离散化)为左端点或右端点的方案。
时间复杂度 \(O(n^2)\) 可过。
注意边界。

#include<bits/stdc++.h>
using namespace std;
const int N=1009,p=19650827;
int n,mp[N],L[N][N],R[N][N];
int main(){
	scanf("%d",&n);
	if(n==1) putchar('1'),exit(0);
	else if(n==2) putchar('2'),exit(0);
	for(int i=1;i<=n;i++) scanf("%d",&mp[i]);
	for(int i=1;i<n;i++) if(mp[i]<mp[i+1]) L[i][i+1]=R[i][i+1]=1;
	for(int k=3;k<=n;k++){
		for(int l=1,r;l+k-1<=n;l++){
			r=l+k-1;
			if(mp[l]<mp[l+1]) L[l][r]+=L[l+1][r];
			if(mp[l]<mp[r]) L[l][r]+=R[l+1][r];
			if(mp[r]>mp[r-1]) R[l][r]+=R[l][r-1];
			if(mp[r]>mp[l]) R[l][r]+=L[l][r-1];
			L[l][r]%=p,R[l][r]%=p;
		}
	}
	printf("%d\n",(L[1][n]+R[1][n])%p);
	return 0;
}
T4 石子合并

处理环上问题常用破环为链法。
将环进行倍长并跑一下区间 DP。

#include<bits/stdc++.h>
using namespace std;
const int N=209;
int n,a[N],sum[N],Max[N][N],Min[N][N],Mins=INT_MAX,Maxs;
int main(){
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]),a[i+n]=a[i];
    for(int i=1;i<=n+n;i++) sum[i]=sum[i-1]+a[i];
    for(int k=2;k<=n;k++) for(int l=1,r;l+k-1<=n+n;l++){
        r=l+k-1,Min[l][r]=INT_MAX;
        for(int mid=l;mid<r;mid++){
            Min[l][r]=min(Min[l][mid]+Min[mid+1][r]+sum[r]-sum[l-1],Min[l][r]);
            Max[l][r]=max(Max[l][mid]+Max[mid+1][r]+sum[r]-sum[l-1],Max[l][r]);
        }
    }
    for(int i=1;i<=n;i++) Mins=min(Mins,Min[i][i+n-1]),Maxs=max(Maxs,Max[i][i+n-1]);
    printf("%d\n%d",Mins,Maxs);
    return 0;
}
T5 相似基因

显然设 \(f(i,j)\) 表示两基因串分别到达对应位置时候的最优解。
转移显然。

#include<bits/stdc++.h>
using namespace std;
const int N=109;
int n,m,dp[N][N];
char s[N],t[N];
inline int cmp(char a,char b){
	if(a==b) return 5;
	if(a>b) swap(a,b);
	if(a=='-') 
		switch(b){
			case 'A':return -3;
			case 'C':return -4;
			case 'G':return -2;
			case 'T':return -1;
			default:exit(0);
		}
	else if(a=='A')
		return (b=='G')?-2:-1;
	else if(a=='C')
		return (b=='G')?-3:-2;
	else return -2;
}
int main(){
	scanf("%d%s%d%s",&n,s+1,&m,t+1);
	for(int i=0;i<=n;i++) for(int j=0;j<=m;j++){
		dp[i][j]=(i==0&&j==0)?0:INT_MIN;
		if(i>0) dp[i][j]=max(dp[i][j],dp[i-1][j]+cmp('-',s[i]));
		if(j>0) dp[i][j]=max(dp[i][j],dp[i][j-1]+cmp('-',t[j]));
		if(i>0&&j>0) dp[i][j]=max(dp[i][j],dp[i-1][j-1]+cmp(s[i],t[j]));
	}
	printf("%d\n",dp[n][m]);
	return 0;
}
T6 涂色

首先思考,第一个位置一定只被涂色了一次。
然后考虑他可以选择一直往后涂,代价就是中间的段之间不再连续了。
于是考虑设 \(f(i,j)\) 表示某一段的价值。

从左向右涂色,枚举对应颜色所涂位置,然后乱搞,于是就做完了。

#include<bits/stdc++.h>
using namespace std;
const int N=55;
char s[N]; int n,dp[N][N];
int main(){
	scanf("%s",s+1),n=strlen(s+1);
	for(int i=1;i<=n;i++) dp[i][i]=1;
	for(int k=2;k<=n;k++) for(int l=1,r,lc,lst,sum;l+k-1<=n;l++){
		r=l+k-1,lc=s[l],lst=l,sum=0,dp[l][r]=INT_MAX;
		//LC is the left color,
		//and LST is the last pos which isn't LC.
		//(We put a virtual one at the beginning.)
		for(int i=l;i<=r;i++) if(s[i]==lc){
			sum+=dp[lst][i-1],lst=i+1;
			dp[l][r]=min(dp[l][r],sum+dp[i+1][r]+1);
		}
	}
	printf("%d\n",dp[1][n]);
	return 0;
}

但发现这么做是错的,比如 ABCACBA 这样的数据就会卡他。

接着想。。。

发现我们无论如何全涂上最左侧闭眼开干一定是对的。
我们定义 \(f(i,j,k)\) 为在一个 \(k\) 的背景下我们能拿到的最优解。
首先,我们可以暴力合并。
其次,我们可以选择更换背景。

#include<bits/stdc++.h>
#define f(i,a,b) for(register int i=(a);i<=(b);++i)
using namespace std;
const int N=55,Col=26;
char s[N]; int n,dp[N][N][N];
int main(){
	scanf("%s",s+1),n=strlen(s+1);
	f(i,1,n){
		f(j,0,Col) dp[i][i][j]=1;
		dp[i][i][s[i]-'A']=0;
	}
	f(k,2,n) for(int l=1,r,best;l+k-1<=n;++l){
		r=l+k-1,best=INT_MAX;
		f(col,0,Col){
			dp[l][r][col]=INT_MAX;
			f(mid,l,r-1)
                dp[l][r][col]=min(dp[l][r][col],dp[l][mid][col]+dp[mid+1][r][col]);
			best=min(best,dp[l][r][col]);
		}
		f(col,0,Col) dp[l][r][col]=min(dp[l][r][col],best+1);
	}
	printf("%d\n",dp[1][n][Col]);
	return 0;
}
T7 玩具取名

只要想到区间 DP 就毫无难度。
怎么想到?数据范围。。。

#include<bits/stdc++.h>
#define f(i,a,b) for(register int i=a;i<=b;i++)
using namespace std;
const int N=209;
inline int Hash(char a,char b){return a*4+b;}
inline int Cmp(char a){
	switch(a){
		case 'W':return 0;
		case 'I':return 1;
		case 'N':return 2;
		default: return 3;
	}
}
unordered_set<int> W[4];
char S[N]; int n; bool dp[N][N][4];
int main(){
	int a,b,c,d; char s[5];
	scanf("%d%d%d%d",&a,&b,&c,&d);
	while(a--) scanf("%s",s),W[0].insert(Hash(Cmp(s[0]),Cmp(s[1])));
	while(b--) scanf("%s",s),W[1].insert(Hash(Cmp(s[0]),Cmp(s[1])));
	while(c--) scanf("%s",s),W[2].insert(Hash(Cmp(s[0]),Cmp(s[1])));
	while(d--) scanf("%s",s),W[3].insert(Hash(Cmp(s[0]),Cmp(s[1])));
	scanf("%s",S+1),n=strlen(S+1);
	f(i,1,n) dp[i][i][Cmp(S[i])]=1;
	f(k,2,n) for(int l=1,r;l+k-1<=n;l++){
		r=l+k-1;
		f(mid,l,r-1) f(x,0,3) f(y,0,3) if(dp[l][mid][x]&&dp[mid+1][r][y])
			f(z,0,3) if(W[z].count(Hash(x,y))) dp[l][r][z]=1;
	}
    a=0;
	if(dp[1][n][0]) putchar('W'),a=1;
	if(dp[1][n][1]) putchar('I'),a=1;
	if(dp[1][n][2]) putchar('N'),a=1;
	if(dp[1][n][3]) putchar('G'),a=1;
	if(a==0) puts("The name is wrong!");
	return 0;
}
T8 能量项链

石子合并(阅读理解版)。

#include<bits/stdc++.h>
#define f(i,a,b) for(register int i=a;i<=b;++i)
using namespace std;
const int N=209;
int n,a[N],dp[N][N],ans;
int main(){
	scanf("%d",&n); f(i,1,n) scanf("%d",&a[i]),a[i+n]=a[i];
	f(k,2,n) for(int l=1,r;l+k-1<n+n;l++){
		r=l+k-1;
		f(mid,l,r-1) dp[l][r]=max(dp[l][r],dp[l][mid]+dp[mid+1][r]+a[l]*a[mid+1]*a[r+1]);
	}
	f(i,1,n) ans=max(ans,dp[i][i+n-1]);
	printf("%d\n",ans);
	return 0;
}
T9 道路游戏

又是阅读理解。。。

一个 \(n(n\le1000)\) 个点的环,顺时针编号,定义连结 \([i,i+1](i<n)\) 的边编号为 \(i\),编号为 \(n\) 的边为 \([n,1]\)
我们一共有若干次操作,要求总共花费时间恰为 \(m(m\le1000)\)操作之间不能等待
定义每一次操作为从一个点顺时针移动不超过 \(p(p\le1000)\) 步,移动一步会经过一条边,同时花费一个单位时间。
操作的费用为起点点权 \(c_i(c_i>0)\),收益为经过所有边边权之和,第 \(i\) 条边第 \(j\) 秒边权会变化成为 \(v_{i,j}(v_{i,j}>0)\)
不要求两次操作起终点等相同,过程中允许总价值为负。
求最大价值。

这个三次方做法是显然的,
可以直接去模拟 dp,状态设 \(f(i)\) 表示第 \(i\) 时刻最优解。

#include<bits/stdc++.h>
#define f(i,a,b) for(register int i=a;i<=b;++i)
using namespace std;
const int N=2009;
int n,m,p,coin[N][N],cost[N],dp[N];
int main(){
	scanf("%d%d%d",&n,&m,&p);
	f(i,1,n) f(j,1,m){
		scanf("%d",&coin[i][j]);
		for(int k=i+n;k<=n+m;k+=n)
			coin[k][j]=coin[i][j];
	}
	f(i,1,n+m) f(j,1,m) coin[i][j]+=coin[i-1][j-1];
	f(i,1,n) scanf("%d",&cost[i]);
	f(i,1,m){
		dp[i]=-1000000;
		for(int j=1;j<=p&&j<=i;j++) f(k,1,n)
			dp[i]=max(dp[i],dp[i-j]-cost[k]+coin[k+j-1][i]-coin[k-1][i-j]);
	}
	printf("%d\n",dp[m]);
	return 0;
}

考虑 for(int j=1;j<=p&&j<=i;j++) 这样的转移很容易让我们想到单调队列。
但是这个形式看起来非常不友好。
如果不想斜优,那我们必须确保对应单调队列末项相同。
考虑到这个转移方式,我们可以考虑斜线单调队列。

具体怎么斜线呢?
我们把所有的以相应 \(dp\) 值为基底的东西一直走到时刻 \(m\) 的权值存进单调队列,
对于队列 \(q_i\) 表示在零时刻处于位置 \(i\),考虑大小关系始终不变,于是他是对的。

#include<bits/stdc++.h>
#define f(i,a,b) for(register int i=a;i<=b;++i)
using namespace std;
const int N=2009;
int n,m,p,coin[N][N],cost[N],dp[N],ff[N],tt[N],q[N][N],tmp;
inline int calc(int st,int key){
	return dp[key]-cost[st+key]+coin[st+n-1][n]-coin[st+key-1][key];
}
int main(){
	scanf("%d%d%d",&n,&m,&p);
	f(i,1,n) f(j,1,m){
		scanf("%d",&coin[i][j]);
		for(int k=i+n;k<=n+m;k+=n)
			coin[k][j]=coin[i][j];
	}
	f(i,1,n+m) f(j,1,m) coin[i][j]+=coin[i-1][j-1];
	f(i,1,n){
		scanf("%d",&cost[i]),ff[i]=1,tt[i]=1,q[i][1]=0;
		for(int k=i+n;k<=n+m;k+=n)
			cost[k]=cost[i];
	}
	f(i,1,m){
		dp[i]=-1000000;
		f(j,1,n){
			while(ff[j]<=tt[j]&&i-q[j][ff[j]]>p) ++ff[j];
		//The current best time to go.
			tmp=q[j][ff[j]];
			if(ff[j]<=tt[j])
			//'j+tmp' is the start position,'j+i' is the end position.
				dp[i]=max(dp[i],dp[tmp]-cost[j+tmp]+coin[j+i-1][i]-coin[j+tmp-1][tmp]);
		}
		f(j,1,n){
			while(ff[j]<=tt[j]&&calc(j,q[j][tt[j]])<=calc(j,i)) --tt[j];
			q[j][++tt[j]]=i;
		}
	}
	printf("%d\n",dp[m]);
	return 0;
}
T10 Polygon

破环为链,维护区间合并最大最小值。
立方复杂度可过。

#include<bits/stdc++.h>
#define f(i,a,b) for(register int i=a;i<=b;++i)
using namespace std;
const int N=105;
int n,g[N],Max[N][N],Min[N][N],ans=INT_MIN; char op[N],tmp[3];
int main(){
	scanf("%d",&n);
	f(i,1,n){
		scanf("%s%d",tmp,&g[i]),op[i]=op[i+n]=tmp[0];
		Max[i][i]=Min[i][i]=Max[i+n][i+n]=Min[i+n][i+n]=g[i+n]=g[i];
	}
	f(l,2,n) for(int i=1,j;i+l-1<=n+n;i++){
		j=i+l-1,Max[i][j]=INT_MIN,Min[i][j]=INT_MAX;
		f(k,i,j-1){
			if(op[k+1]=='t'){
				Max[i][j]=max(Max[i][k]+Max[k+1][j],Max[i][j]);
				Min[i][j]=min(Min[i][k]+Min[k+1][j],Min[i][j]);
			}else{
				Max[i][j]=max(Max[i][k]*Max[k+1][j],Max[i][j]);
				Max[i][j]=max(Min[i][k]*Max[k+1][j],Max[i][j]);
				Max[i][j]=max(Max[i][k]*Min[k+1][j],Max[i][j]);
				Max[i][j]=max(Min[i][k]*Min[k+1][j],Max[i][j]);
				Min[i][j]=min(Max[i][k]*Max[k+1][j],Min[i][j]);
				Min[i][j]=min(Min[i][k]*Max[k+1][j],Min[i][j]);
				Min[i][j]=min(Max[i][k]*Min[k+1][j],Min[i][j]);
				Min[i][j]=min(Min[i][k]*Min[k+1][j],Min[i][j]);
			}
		}
		// printf("%d %d %d %d\n",i,j,Min[i][j],Max[i][j]);
	}
	f(i,1,n) ans=max(ans,Max[i][i+n-1]);
	printf("%d\n",ans);
	f(i,1,n) if(Max[i][i+n-1]==ans) printf("%d ",i);
	return 0;
}
T11 关路灯

容易想到设 \(L(i,j)\) 表示区间内都已经关闭停在左端点代价,\(R(i,j)\) 为右端点。
转移显然。

#include<bits/stdc++.h>
#define f(i,a,b) for(register int i=a;i<=b;++i)
using namespace std;
const int N=55;
const long long inf=1e17;
int n,c;
long long pos[N],cost[N],L[N][N],R[N][N];
int main(){
	scanf("%d%d",&n,&c);
	f(i,1,n) scanf("%lld%lld",&pos[i],&cost[i]),cost[i]+=cost[i-1];
	f(i,2,n) for(int l=max(c-i+1,1),r;l<=c&&l+i-1>=c&&l+i-1<=n;l++){
		r=l+i-1,L[l][r]=R[l][r]=inf;
		if(l<c)
			L[l][r]=min(L[l][r],min(L[l+1][r]+(cost[n]-cost[r]+cost[l])*(pos[l+1]-pos[l]),R[l+1][r]+(cost[n]-cost[r]+cost[l])*(pos[r]-pos[l])));
		if(r>c)
			R[l][r]=min(R[l][r],min(R[l][r-1]+(cost[n]-cost[r-1]+cost[l-1])*(pos[r]-pos[r-1]),L[l][r-1]+(cost[n]-cost[r-1]+cost[l-1])*(pos[r]-pos[l])));
		// printf("%d %d %lld %lld\n",l,r,L[l][r],R[l][r]);
	}
	printf("%lld\n",min(L[1][n],R[1][n]));
	return 0;
}
树形 dp/图上 dp

啥也不说了直接看题。
对应洛谷题单【动态规划4】。

T1 没有上司的舞会

经典题目,设 \(f(i,0/1)\) 表示当前节点选或不选时最大收益即可。

#include<bits/stdc++.h>
#define f(i,a,b) for(register int i=a;i<=b;++i)
using namespace std;
const int N=6002;
int n,f[N],v[N],dp[N][2];
vector<int> s[N];
void dfs(int i){
	dp[i][1]=v[i];
	for(auto j:s[i]) dfs(j),dp[i][1]+=max(0,dp[j][0]),dp[i][0]+=max(0,max(dp[j][0],dp[j][1]));
}
int main(){
	int a,b;
	scanf("%d",&n);
	f(i,1,n) scanf("%d",&v[i]);
	f(i,1,n-1) scanf("%d%d",&a,&b),f[a]=b,s[b].push_back(a);
	b=1;
	while(f[b]) b=f[b];
	dfs(b),printf("%d\n",max(dp[b][0],dp[b][1]));
	return 0;
}
T2 二叉苹果树

考虑每个点保留枝条数量非常重要,存入状态。
其实我们正常到这一步就去爆搜枚举每个子节点的保留数了。
然而,这个 dp 数组可以重复用,他就是一个完全背包。

#include<bits/stdc++.h>
#define f(i,a,b) for(register int i=a;i<=b;++i)
#define F(i,a,b) for(register int i=b;i>=a;--i)
using namespace std;
const int N=109;
int n,q,dp[N][N];
struct node{int v,w;};
vector<node> o[N];
void dfs(int i,int f){
	for(node j:o[i]) if(j.v!=f) dfs(j.v,i);
	for(node j:o[i]) if(j.v!=f) F(k,1,q) f(x,0,k-1)
		dp[i][k]=max(dp[i][k],dp[i][k-1-x]+j.w+dp[j.v][x]);
}
int main(){
	scanf("%d%d",&n,&q);
	for(int i=1,u,v,w;i<n;i++){
		scanf("%d%d%d",&u,&v,&w);
		o[u].push_back({v,w});
		o[v].push_back({u,w});
	}
	dfs(1,1),printf("%d",dp[1][q]);
	return 0;
}
T3 选课

基本同 T2。

#include<bits/stdc++.h>
#define f(i,a,b) for(register int i=a;i<=b;++i)
#define F(i,a,b) for(register int i=b;i>=a;--i)
using namespace std;
const int N=309;
int n,q,dp[N][N],v[N];
vector<int> o[N];
void dfs(int i){
	dp[i][1]=v[i];
	for(int j:o[i]){
		dfs(j);
		F(k,2,q) f(l,1,k-1) dp[i][k]=max(dp[i][k],dp[i][k-l]+dp[j][l]);
	}
}
int main(){
	scanf("%d%d",&n,&q),q=min(n,q),++q;
	for(int i=1,j;i<=n;i++)
		scanf("%d%d",&j,&v[i]),o[j].push_back(i);
	dfs(0),printf("%d",dp[0][q]);
	return 0;
}
posted @ 2025-07-26 21:30  2025ing  阅读(23)  评论(0)    收藏  举报