思路(套模型最重要)
1.先判断(最优子结构,重叠子结构)
2.考虑暴力怎么写(用决策树表示一下或许能为后面找状态有帮助)
3.找出最优子结构,确定dp数组的含义(记忆化的技巧,维度由小到大考虑)
4.确定dp数组边界,写出状态转移方程
5.遍历注意点,可能会在这里出现一些问题
6.AC啦
1线性dp
1.1第一个dp(递推)
/*
dp第一个问题:数塔问题
先看题目:如下图(图片来自百度图片)(此处没有百度一下就知)是一个数塔,从顶部出发在每一个节点可以选择向左或者向右走,一直走到底层,要求找出一条路径,使得路径上的数字之和最大.
记忆化(以空间换时间),贪不了心的,从2的n次方优化成n的 ,最优子结构,重叠子结构
做法:(第一道题看出来的)
1.先判断(最优子结构,重叠子结构)
2.找出最优子结构,确定dp数组的含义(记忆化的技巧)
3.确定dp数组边界,写出状态转移方程
4.AC啦
*/
#include<stdio.h>
#include<iostream>
#include<algorithm>
using namespace std;
int maxn=1000;
int f[maxn][maxn],dp[maxn][maxn];
int main()
{
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
for(int j=1;j<=i;j++) scanf("%d",&f[i][j]);
}
for(int j=1;j<=n;j++) dp[n][j]=f[n][j];//这里是边界(为什么这里是正常过程的末尾dp却是从这里开始呢?因为这里是最优子结构的开始) (此处初始化不要忘记了)
for(int i=n-1;i>=1;i--)
{
for(int j=1;j<=i;j++) dp[i][j]=maxn(dp[i+1][j],dp[i+1][j+1])+f[i][j];//此处状态转移方程
}
printf("%d\n",dp[1][1]);//最上面的答案
return 0;
}
1.2大连续子序列
/*
最大连续子序列和
这里要注意设计的状态是无后效性(下一个状态不被前一个状态唯一确定)
这里的状态是指以这个为基准之前的选了多少个
*/
#include<stdio.h>
#include<iostream>
#include<algorithm>
using namespace std;
const int maxn=10010;
int a[maxn],dp[maxn];
int main()
{
int n;
scanf("%d",&n);
for(int i=0;i<n;i++) scanf("%d",&a[i]);
dp[0]=a[0];//边界
for(int i=1;i<n;i++) dp[i]=max(a[i],dp[i-1]+a[i]);//状态转移方程(选还是不选)
int k=0;
for(int i=1;i<n;i++)
{
if(dp[i]>dp[k]) k=i;
}
printf("%d\n",dp[k]);
return 0;
}
1.3最长不下降子序列
/*
最长不下降子序列
状态是以此结尾的最长不下降子序列 (难道线性的都是以此结尾的答案/手动滑稽)
时间复杂度变成n方,也就是说不一定是n
而且状态不一定是仅和上一个状态有关
*/
#include<stdio.h>
#include<iostream>
#include<algorithm>
using namespace std;
const int N=100;
int a[N],dp[N];
int main()
{
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
int ans=-1;//记录最大的dp[i]
for(int i=1;i<=n;i++)
{
dp[i]=1;//边界初始条件(假设每个元素自成一个子序列)
for(int j=1;j<i;j++)
{
if(a[i]>=a[j]&&(dp[j]+1>dp[i])) dp[i]=dp[j]+1//条件
}
ans=max(ans,dp[i]);
}
printf("%d",ans);
return 0;
}
1.4最长公共子序列
/*
子序列形式化定义:
给定一个序列X=<x1,x2,x3,x4...,xm>,另一个序列Z=<z1,z2,z3,z4...,zk>,若存在一个严格递增的X的下标序列<i1,i2,i3,...,ik>对所有的1,2,3,...,k,都满足x(ik)=zk,则称Z是X的子序列
比如Z=<B,C,D,B>是X=<A,B,C,B,D,A,B>的子序列
公共子序列定义:
如果Z既是X的子序列,又是Y的子序列,则称Z为X和Y的公共子序列
最长公共子序列(以下简称LCS):
2个序列的子序列中长度最长的那个
*/
#include<iostream>
#include<stdio.h>
#include<algorithm>
#include<string.h>
using namespace std;
const int n=100;
char a[n],b[n];
int dp[n][n];
int main()
{
int n;
gets(a+1);//此处用string比较方便(虽说这样做很奇怪但却是必须,因为要确定边界)
gets(b+1);
int lena=strlen(a+1);
int lenb=strlen(b+1);
for(int i=0;i<=lena;i++) dp[i][0]=0;
for(int j=0;j<=lenb;j++) dp[0][j]=0;
for(int i=1;i<=lena;i++)
{
for(int j=1;j<=lenb;j++)//状态转移方程
{
if(a[i]==b[j]) dp[i][j]=dp[i-1][j-1]+1;
else dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
}
}
printf("%d\n",dp[lena][lenb]);
}
1.5最长回文子串
#include<iostream>
#include<stdio.h>
#include<string.h>
using namespace std;
const int maxn=100;
char s[maxn];
int dp[maxn][maxn];
int main()
{
gets(s);
int len=strlen(s),ans=1;
memset(dp,0,sizeof(dp));
//边界
for(int i=0;i<len;i++)
{
dp[i][i]=1;
if(i<len-1)
{
if(s[i]==s[i+1])
{
dp[i][i+1]=1;
ans=2;
}
}
}
for(int l=3;l<=len;l++)//枚举子串的长度
{
for(int i=0;i+l-1<len;i++)
{
int j=i+l-1;
if(s[i]==s[j]&&dp[i+1][j-1]==1)
{
dp[i][j]=1;
ans=l;//更新最长
}
}
}
printf("%d\n",ans);
return 0;
}
1.6DAG最长(最短)路
//DAG最长路1.求解整个图中的最长路2.固定终点,求DAG的最长路径
//这个dp的边界是将所有出度为0的点标记成0
int DP(int i)//dp数组表示从i顶点出发能获得的最长路径长度
{
if(dp[i]>0) return dp[i];//因为是逆拓扑排序出来的动态规划递归所以只要计算出来即为最终答案
for(int j=0;j<n;j++)
{
if(g[i][j]!=inf)//此处和下面第二问有些区别
{
dp[i]=max(dp[i],DP(j)+g[i][j]);//这里出来的即为字典序最小的,因为此处出答案的过程是从后往前出,但是遍历的顺序却是从前往后
}
}
return dp[i];
}
//如果标记路径呢?
int DP(int i)
{
if(dp[i]>0) return dp[i];
for(int j=0;j<n;j++)
{
if(g[i][j]!=inf)
{
int temp=DP(j)+g[i][j];//防止重复计算
if(temp>dp[i])
{
dp[i]=temp;//覆盖dp【i】
choice[i]=j;//i的顶点后继顶点是j(有点像链式前向星)
}
}
}
return dp[i];
}
void printpath(int i)
{
printf("%d",i);
while(choice[i]!=-1)
{
i=choice[i];
printf("->%d",i);
}
}
//第二个问题(dp数组定义为从i号顶点出发到达终点T能获得的最长路径长度)
//初始化将dp[点]都变为-inf(不用+inf是因为最长路中max中+inf会覆盖一切)
vis[T]=true;
dp[T]=0;
int DP(int i)
{
if(vis[i]) return dp[i];
vis[i]=true;
for(int j=0;j<n;j++)
{
if(g[i][j]!=inf)
{
dp[i]=max(dp[i],DP[j]+g[i][j]);
}
}
return dp[i];
}
1.7 .1 01背包,完全背包
//为什么说采药这道题不能用上升子序列呢?因为这里状态不再想上升子序列一样的那两个了(这里设错状态了)对于dp来说模型越适合越好吧
/*#include<iostream>
#include<stdio.h>
#include<algorithm>
using namespace std;
struct node
{
int tim,val;
}med[110];
int main()
{
int dp[110];
int dpt[110]={0};
int t,m;
scanf("%d %d",&t,&m);
for(int i=1;i<=m;i++) scanf("%d %d",&med[i].tim,&med[i].val);
int ans=-1;
for(int i=1;i<=m;i++)
{
if(med[i].tim>t) dp[i]=0;
else
{
dp[i]=med[i].val;
dpt[i]=med[i].tim;
}
for(int j=1;j<i;j++)
{
if(dpt[j]+med[i].tim<=t&&dp[j]+med[i].val>dp[i])
{
dpt[i]=dpt[j]+med[i].tim;
dp[i]=dp[j]+med[i].val;
}
}
ans=max(ans,dp[i]);
}
printf("%d",ans);
return 0;
}
*/
//求解01背包
#include<stdio.h>
#include<iostream>
using namespace std;
const int maxn=100;
const int maxv=1000;
int w[maxn],c[maxn],dp[maxv];
int main()
{
int n,v;
scanf("%d%d",&n,&v);
for(int i=1;i<=n;i++) scanf("%d",&w[i]);
for(int i=1;i<=n;i++) scanf("%d",&c[i]);
for(int v=0;v<=V;v++) dp[v]=0;//一维滚动数组连这个边界的初始化都改了,就是从0开始的
for(int i=1;i<=n;i++)
{
for(int v=V;v>=w[i];v--) dp[v]=max(dp[v],dp[v-w[i]]+c[i]);//动态转移方程(因为是滚动数组所以一定是从后往前遍历)
}
int max=0;
for(int v=0;v<=V;v++)
{
if(dp[v]>max) max=dp[v];
}
printf("%d\n",maxn);
return 0;
}
//完全背包
for(int i=1;i<=n;i++)
{
for(int v=w[i];v<=V;v++) dp[v]=max(dp[v],dp[v-w[i]]+c[i]);//此处必须正向枚举,不能理解滚动数组法就用表格
}
1.7.2多重背包(二进制优化)
阎氏dp分析法
1.找集合
2.找属性
3.找状态转移方程
4.写代码
/*
有N种物品和一个容量为T的背包,第i种物品最多有M[i]件可用,价值为P[i],体积为V[i],求解:选哪些物品放入背包,可以使得这些物品的价值最大,并且体积总和不超过背包容量。
*/
//阎氏dp分析法
#include<iostream>
#include<stdio.h>
#include<algorithm>
using namespace std;
const int N=25000,m=2010;
int n,m;
int v[N],m[N];//这里N是依照二进制拆分的最大数目决定的
int f[N];
int main()
{
cin>>n>>m;
int cnt=0;
for(int i=1;i<=n;i++)//此处二进制拆分
{
int a,b,s;
cin>>a>>b>>s;
int k=1;//起始拆分
while(k<=s)
{
cnt++;
v[cnt]=a*k;
w[cnt]=b*k;
s-=k;
k*=2;
}
if(k<s)
{
cnt++;
v[cnt]=a*s;
w[cnt]=b*s;
}
}
n=cnt;
for(int i=1;i<=n;i++)//拆分以后的01背包
{
for(int j=m;j>=v[i];j++) f[j]=max(f[j],f[j-v[i]]+w[i]);
}
cout<<f[m]<<endl;
return 0;
}
2记忆化搜索
2.1采药(记忆化搜索实现的)
https://oi-wiki.org/dp/memo/这篇文章不论是思路还是实现方法都写的很好值得借鉴
//采药那道题目
#include<iostream>
#include<stdio.h>
using namespace std;
const int INF=1e6;
int n,t;
int tcost[103],mget[103];
int mem[101][1003];//dp数组
int dfs(int pos,int tleft)//其实这也算是一个数组
{
if(mem[pos][tleft]!=-1) return mem[pos][tleft];
if(pos==n+1) return mem[pos][tleft]=0;
int dfs1,dfs2=-INF;
dfs1=dfs(pos+1,tleft);//不选
if(tleft>=tcost[pos]) dfs2=dfs(pos+1,tleft=tcost[pos])+mget[pos];
return mem[pos][left]=max(dfs1,dfs2);
}
int main()
{
memset(mem,-1,sizeof(mem));//这个数组有标记作用也有记录答案的作用
cin>>t>>n;
for(int i=1;i<=n;i++) cin>>tcost[i]>>mget[i];
cout<<dfs(1,t)<<endl;
return 0;
}