summary
递推求解
N级楼梯上楼问题:一次可以走一级或两级,问有多少种上楼方式。
思路:首先设有n级台阶上有f(n)种方式;倒推,从最后一步开始想:因为在最上面一级可以退1步或2步,也就是说,f(n)可以由f(n-1)和f(n-2)相加得到,也就是到达n-2级的方式数 加上 到达n-1级的方式数。
so,状态转移方程get:f(n)=f(n-1)+f(n-2)。其实也就是斐波那契数列啦!
#include<iostream>
using namespace std;
unsigned long long a[100]= {1,2};//注意这里要long long
int main()
{
int n;
while(cin>>n)
{
for(int i=2; i<n; i++)
a[i]=a[i-1]+a[i-2];
cout<<a[n-1]<<endl;
}
return 0;
}
LIS
刚开始看讲解感觉理解困难,写了代码仿佛秒懂了,但是怎么构建这么个dp【用来存以ai结尾的LIS的最长长度,最后在for一遍找到全局最长的LIS】,还需要深入思考+总结
case1:拦截导弹,这就是LIS的镜像版本LDS?就是每次往前找>=当前位置的即可
#include<iostream>
#include<cstring>
using namespace std;
const int N=30;
int a[N];
int dp[N];//存每个以ai结果的最长递增子序列的元素数
int main() {
int n;
while(cin>>n, n!=0) {
memset(dp,0,sizeof(dp));
for(int i=0; i<n; i++)
scanf("%d", &a[i]);
for(int i=0; i<n; i++) {
int tmax=1;//因为最短序列就是本身这个元素,so,1
for(int j=0; j<i; j++) {//这层循环的目的就是找到以ai结尾的LIS的元素数
if(a[j]>=a[i]) {
if(dp[j]+1>tmax)
tmax=dp[j]+1;//这就是基于前面已经找到了的lIS,看能否在这基础上更“长”
}
}
dp[i]=tmax;
}
int ans=-1;
for(int i=0; i<n; i++)
if(dp[i]>ans)
ans=dp[i];
printf("%d\n", ans);
}
return 0;
}
case2:合唱队形,问一个序列要满足先增后减序列,最少要去掉几个数
- 我刚开始以为先找出一个max的,然后以此为标,左边LIS,右边LDS即可,然而发现eg 2323这种,其实是以第一个3位标才对,
- 所以应该要全体LIS一遍,然后也是关键:反向LIS(我开始由想当然的正向LDS,从左往右这个顺序显然是错误的,因为真正需要的是从右往左的LIS)
#include<iostream>
using namespace std;
const int N=105;
int a[N];
int dp1[N],dp2[N];//正向上升,反向上升!
int main() {//17:41->s
int n,ma;
while(scanf("%d",&n)!=EOF, n) {
for(int i=0; i<n; i++) {
scanf("%d", &a[i]);
}
for(int i=0; i<n; i++) {
ma=1;
for(int j=0; j<i; j++) {
if(a[j]<a[i])//上升
ma=max(ma,dp1[j]+1);
}
dp1[i]=ma;
}
for(int i=n-1; i>=0; i--) {
ma=1;
for(int j=n-1; j>i; j--) {
if(a[j]<a[i])//反向上升
ma=max(ma,dp2[j]+1);
}
dp2[i]=ma;
}
ma=-1;
for(int i=0; i<n; i++) {
if(dp1[i]+dp2[i]>ma)
ma=dp1[i]+dp2[i];
}
printf("%d\n", n-ma+1);
}
return 0;
}
LCS
case1:Coincidence 裸的LCS,以下为标准版子,几个细节
- init的时候从0一直到length
- 而dp递推的时候是从1开始,意思是从1开始往后数,每次比较的是前面的i-1位置,递推的是i位置
#include<iostream>
using namespace std;
const int N=105;
string a,b;
int dp[N][N];//dp[i][j]表示a和b在前i和j个字符时的LCS长度
int main() {
while(cin>>a>>b) {
int la=a.length();
int lb=b.length();
for(int i=0; i<=la; i++) dp[i][0]=0; //共la+1个数要init
for(int j=0; j<=lb; j++) dp[0][j]=0;
for(int i=1; i<=la; i++) {//1~la
for(int j=1; j<=lb; j++) {
if(a[i-1]==b[j-1])//把i-1当做上次位置,每次递推当前的i位置
dp[i][j]= dp[i-1][j-1]+1;
else
dp[i][j]=max(dp[i][j-1], dp[i-1][j]);
}
}
printf("%d\n", dp[la][lb]);
}
return 0;
}
背包
01背包:每个物品选1或不选。二维的情况好理解多了,dp[i][j]为选到前i个物品时剩余容量j时的最大收益,就是从头开始填表。注意
- 初始化是没有选物品时的dp为0:dp[i][0]为0
- 物品要从0到n!因为两个维度的0分别表示:“没选物品”以及“容量为0”的含义,在递推时要用到,不能赋值了,so赋值是从1到n
#include<iostream>
using namespace std;
const int N=1005;
int w[N];//重量
int v[N];//收益
int dp[N][N];//dpij表示体积<=j时,前i个物品达到的最大收益
int W,n;//容量,物品数
void show_dp() {
for(int i=1; i<=n; i++) {
for(int j=0; j<=W; j++)
cout<<dp[i][j]<<' ';
cout<<"\n";
}
cout<<"\n";
}
int main() {
while(cin>>W>>n) {
if(!W && !n) break;
for(int i=1; i<=n; i++)//必须从1开始,因为0有“没选物品”的含义,不能缺少!
cin>>w[i]>>v[i];
for(int j=0; j<=W; j++) dp[0][j]=0;//没选的话,都是0
for(int i=1; i<=n; i++) {
for(int j=w[i]; j<=W; j++)//够装
dp[i][j]=max(dp[i-1][j], dp[i-1][j-w[i]]+v[i]);
for(int j=w[i]-1; j>=0; j--)//不够装
dp[i][j]=dp[i-1][j];
// show_dp();
}
cout<<dp[n][W]<<endl;
}
return 0;
}
使用“滚动数组”压缩空间,下面这段话写的非常好,细细品味
完全背包,与01背包区别 仅在于每个物品可以选多次。一维数组的情况,和01背包完全一样,只是变成了正序遍历j
#include<iostream>
using namespace std;
const int N=1005;
int w[N];//重量
int v[N];//收益
int dp[N];//体积<=j时,能达到的最大收益
int W,n;//时间,物品数
void show_dp() {
for(int j=0; j<=W; j++)
cout<<dp[j]<<' ';
cout<<"\n\n";
}
int main() {
while(cin>>W>>n) {
if(!W && !n) break;
for(int i=1; i<=n; i++)//必须从1开始,因为0有“没选物品”的含义,不能缺少!
cin>>w[i]>>v[i];
for(int j=0; j<=W; j++) dp[j]=0; //也要都init=0。理解为,虽把时间维度去了,
//但是其实刚开始是要全部赋为0,表示了时间维度的意义,即“没选”时,收益都是0
for(int i=1; i<=n; i++) {
for(int j=w[i]; j<=W; j++)
dp[j]=max(dp[j], dp[j-w[i]]+v[i]);
show_dp();
}
cout<<dp[W]<<endl;
}
return 0;
}
而一维数组的正序倒序的区别在于,有多种理解方式:
- 《算法笔记》(用二维数组考虑)
- 完全背包对j的每次遍历(就是对一行的遍历)就是用的正左边的值,压缩到一维后不受影响,从左往右遍历刚好
- 01背包用的是左上方的值,现在压缩到一维后,相当于每次更新都不能动左边的值,必须从右边开始向左更新
- 《王道》另一种理解:
- 01背包逆序,因为只能放一次,逆序可以保证更新dp[j]时,dp[j-w[i]]是没放物品i时的dp[i-1][j-w[i]]
- 完全背包正序,因为可放多次,dp[i][j]可以由可能放入物品i的状态dp[i][j-w[i]]转移而来;
- 还有一种理解从小的开始遍历,就是允许它被重复装入直到装不下
- from 网友:两个二维状态转移方程本来就不一样,利用滚动数组后状态一样了,完全是巧合