引入:记忆化搜索
记忆化的方法
没了解过搜索的可以看这个
记忆化,就是将搜索时的状态记录下来,在再次调用时直接拿出来使用,能剪少很多的重复搜索。
例题:
题意就是找最长不断递减的一段数。
朴素做法
思考当以(x,y)这个位置为终点时的最长最长不断递减。
可以打出朴素算法:从每一个点打一个暴力DFS,每一个点向四周扩散,更新答案,每一个点的最佳答案进行比较。
分析时间复杂度:每一点有四个选择,即最多有4^n ,每个点运行一次即要×上nm ,总时间为O(nm*(4^n))
过不了一点。
考虑记忆化
朴素做法中提到过以(x,y)这个位置为终点最长的不断递减的长度,我们用一个f数组 来记录(x,y)这个结果
f[x][y]该如和转移?
它可以在它上下左右四个中选一个合法(小于a[x][y];x,y不越界)且优于(更长)的f,
即
cpp
int dfs(int x,int y){
if(f[x][y]){
return f[x][y];
}
int sum=0;
if(x-1>0){
if(a[x-1][y]>a[x][y])
sum=max(sum,dfs(x-1,y));
}
if(x+1>0)
if(a[x+1][y]>a[x][y])
sum=max(sum,dfs(x+1,y));
if(y-1>0)
if(a[x][y-1]>a[x][y])
sum=max(sum,dfs(x,y-1));
if(y+1>0)
if(a[x][y+1]>a[x][y])
sum=max(sum,dfs(x,y+1));
f[x][y]=sum+1;
return f[x][y];
}
逐一枚举每一个点
cpp
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
if(!f[i][j]){
f[i][j]=dfs(i,j);
ans=max(ans,f[i][j]);
}
}
}
标程
c++
#include<bits/stdc++.h>
using namespace std;
int f[101][101];
int a[101][101];
int n,m,ans;
int dfs(int x,int y){
if(f[x][y]){
return f[x][y];
}
int sum=0;
if(x-1>0){
if(a[x-1][y]>a[x][y])
sum=max(sum,dfs(x-1,y));
}
if(x+1>0)
if(a[x+1][y]>a[x][y])
sum=max(sum,dfs(x+1,y));
if(y-1>0)
if(a[x][y-1]>a[x][y])
sum=max(sum,dfs(x,y-1));
if(y+1>0)
if(a[x][y+1]>a[x][y])
sum=max(sum,dfs(x,y+1));
f[x][y]=sum+1;
return f[x][y];
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
cin>>a[i][j];
}
}
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
if(!f[i][j]){
f[i][j]=dfs(i,j);
ans=max(ans,f[i][j]);
}
}
}
cout<<ans;
}
DP
什么是DP
DP是一种通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。
也就是DFS,不过要把每一个子问题的答案记录下来。(记忆化就是一种DP)
背包DP
01背包
模版
这题就是给定我们一个范围m(t),n(m)个物品,每个物品都有一个价值与一个代价,每个物品可以选或不选,求最大可得的代价不过范围的最大价值。
第一眼,可以考虑一个以当前是第几个以及已花费代价的DFS
cpp
void dfs(int x/*第x个物品*/,int sum/*当前已花价值*/,int ans){
if(sum>m){
return ;
}
if(x>n){
maxx=max(ans,maxx);
return ;
}
dfs(x+1,sum+a[x].v/*v是代价*/,ans+a[x].w/*w是价值*/);//选它
dfs(x+1,sum,ans);//不选
}
但这肯定过不了,考虑DP。
由我们的DFS可得一个状态f[i][j] ,它的意义是当走到第i个物品时花费j代价得到的的最大价值
DFS中分两种情况选与不选 ,因为它的定义是最大价值,所以是取两者的最大值。
先考虑不选,不选就是上一个物品已经到达j 的最大价值,即f[i-1][j] .
再考虑选的情况,选就是上一个物品在加上i的代价到达j的状态的最大价值加上i的价值 ,即f[i-1][j-w[i]]+v[i]
注:v代表价值,w代表代价
得到上面两个,可以推出f[i][j] 的转移方程:f[i][j]=max{f[i-1][j],f[i-1][j-w[i]]+v[i]}
得没有优化空间后的码:
cpp
#include<bits/stdc++.h>
using namespace std;
int t,n,f[10001][10001],w[100001],s[100001];
int main(){
cin>>t>>n;
for(int i=1;i<=n;i++){
cin>>w[i]>>s[i];
}
for(int i=1;i<=n;i++){
for(int j=0;j<=t;j--){
if(j>=w[i])
f[i][j]+=max(f[i-1][j-w[i]]+s[i],f[i-1][j]);
else
f[i][j]+=f[i-1][j];
}
}
cout<<f[n][t];
}
这个时间复杂度是O(nm),空间复杂度是O(nm)
想一下怎么优化空间。
我们发现0/1的状态只关于上一层 ,那么可以第一维滚掉,但又有一个问题正序or逆序 。
正序时,j-w[i]会更新到前面已经更新的状态,会重复更新 就不符合0/1背包。
逆序可以避免这种结果。so,我们应倒序枚举。
标程:
cpp
#include<bits/stdc++.h>
using namespace std;
int t,n,f[10001],w[100001],s[100001];
int main(){
cin>>t>>n;
for(int i=1;i<=n;i++){
cin>>w[i]>>s[i];
}
for(int i=1;i<=n;i++){
for(int j=t;j>=w[i];j--){
f[j]=max(f[j],f[j-w[i]]+s[i]);
}
}
cout<<f[t];
}
完全背包
与上题差不多,但是一个物品可以重复选,那就重上一个的改。
在0/1时说过:正序枚举会导致重复更新
所以也就将枚举顺序改一下就过了
cpp
#include<bits/stdc++.h>
using namespace std;
int t,m;
long long f[10000001];
int T[10000001];
int w[10000001];
int main(){
cin>>t>>m;
for(int i=1;i<=m;i++){
cin>>w[i]>>T[i];
}
for(int i=1;i<=m;i++){
for(int j=w[i];j<=t;j++){
f[j]=max(f[j-w[i]]+T[i],f[j]);
}
}
cout<<f[t];
}
多重背包
每一个物品有价值和花费,还有一个可以购买的数量,求最多价值。
朴素做法
暴力枚举每一个物品的可以有的次数,跑0/1背包。
二进制拆分
考虑对枚举次数进行优化。
任一个数都能拆为多个2的多次方的和
因为每一个数都能转为二进制,而一位1对应着2的那一位的次方,所以任一个数都能拆为多个2的多次方的和。
正常将01转移成多重背包时,我们从小到大枚举放j第i类物品会更优,但这样其实是会有重复枚举的。
我们可以将多次加入同一类物品变为一次加多个 ,就是将一个物品的多个数量用二进制合成一个物品 。这样就能将枚举次数从本来物品个数优化到原来的log。
题目:https://www.luogu.com.cn/problem/P1776
模版,可以这么做,代码:
cpp
#include<bits/stdc++.h>
using namespace std;
int n,q;
int c[1000001],w[1000001];
int tempc[1000001],temp2[1000001];
int s[1000001],f[1000001];
int main(){
cin>>n>>q;
for(int i=1;i<=n;i++){
cin>>tempc[i]>>temp2[i]>>s[i];
}
int k=0;
for(int i=1;i<=n;i++){
for(int j=1;j<=s[i];j*=2){
k++;
c[k]=tempc[i]*j;
w[k]=temp2[i]*j;
s[i]-=j;
}
if(s[i]>0){
k++;
c[k]=tempc[i]*s[i];
w[k]=temp2[i]*s[i];
}
}
for(int i=1;i<=k;i++){
for(int j=q;j>=w[i];j--){
f[j]=max(f[j],f[j-w[i]]+c[i]);
}
}
cout<<f[q];
}
区间DP
例题
P1880 [NOI1995] 石子合并
题目描述
在一个圆形操场的四周摆放 N N N 堆石子,现要将石子有次序地合并成一堆,规定每次只能选相邻的 2 2 2 堆合并成新的一堆,并将新的一堆的石子数,记为该次合并的得分。
试设计出一个算法,计算出将 N N N 堆石子合并成 1 1 1 堆的最小得分和最大得分。
输入格式
数据的第 1 1 1 行是正整数 N N N,表示有 N N N 堆石子。
第 2 2 2 行有 N N N 个整数,第 i i i 个整数 a i a_i ai 表示第 i i i 堆石子的个数。
输出格式
输出共 2 2 2 行,第 1 1 1 行为最小得分,第 2 2 2 行为最大得分。
输入输出样例 #1
输入 #1
4
4 5 9 4
输出 #1
43
54
说明/提示
1 ≤ N ≤ 100 1\leq N\leq 100 1≤N≤100, 0 ≤ a i ≤ 20 0\leq a_i\leq 20 0≤ai≤20。
经思考,我们可以通过题目中的合并操作想到将区间分开处理,这时就可以理所当然地想到DP(将大问题分解成多个子问题解决),再去考虑状态,既然是合并区间,那么肯定就以区间为状态。
设 F[l][r]为l~r之间合并的最小值 ,那么就在l~r之间找一个点拼接两个区间使代价最小的转移
即
l < k < r , f [ l ] [ r ] = m a x ( f [ l ] [ k ] + f [ k ] [ r ] , f [ l ] [ r ] ) l< k < r,f[l][r]=max(f[l][k]+f[k][r],f[l][r]) l<k<r,f[l][r]=max(f[l][k]+f[k][r],f[l][r])
再考虑如何处理环,破环成链,复制两遍,跑DP,代价最大一样
代码:
cpp
#include<bits/stdc++.h>
using namespace std;
long long n,sum[301];
long long m[301];
long long f[301][301][2];
int main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>m[i];
sum[i]=m[i];
sum[n+i]=m[i];
}
for(int i=1;i<=2*n;i++){
sum[i]=sum[i]+sum[i-1];
f[i][i][0]=0;
f[i][i][1]=0;
for(int j=1;j<=2*n;j++){
if(i!=j)f[i][j][0]=0x3f3f3f3f3f3f3;
}
}
for(int len=2;len<=n;len++){
for(int l=1;l+len-1<=2*n;l++){
int r=l+len-1;
int w=sum[r]-sum[l-1];
for(int k=l;k<r;k++){
f[l][r][0]=min(f[l][r][0],f[l][k][0]+f[k+1][r][0]+w);
f[l][r][1]=max(f[l][r][1],f[l][k][1]+f[k+1][r][1]+w);
}
}
}
long long minn=f[1][n][0],maxx=f[1][n][1];
for(int i=1;i<=n;i++){
minn=min(minn,f[i][i+n-1][0]);
maxx=max(maxx,f[i][i+n-1][1]);
}
cout<<minn<<endl<<maxx;
}