王道机试 DP篇

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 网友:两个二维状态转移方程本来就不一样,利用滚动数组后状态一样了,完全是巧合

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值