进击高手【第六期】线性动态规划

定义

将一个问题分解为子问题递归求解,并且将中间结果保存以避免重复计算的办法,就叫做“动态规划”

术语

  1. 决策:以前状态通过决策,前进到了当前状态.可见决策其实就是状态之间的桥梁。而以前状态也就决定了当前状态的值。

  2. 阶段:把求解问题的过程恰当地分成若干个相互联系的阶段,以便于求解,过程不同,所处的阶段就不同。描述阶段的变量称为阶段变量。

  3. 状态:在动态规划解题策略中,把描述问题的一组变量称为一个“状态”。而某个状态下的值,就是这个状态所对应的子问题的解。

请添加图片描述

适用条件:

用动态规划求解问题,我们主要利用了问题的两个性质:

  • 最优子结构
  • 重叠子问题

最优子结构指问题的最优解包含了子问题的最优解,它是动态规划方法可行的理论基础。
一个问题具有子问题重叠性质是指用递归算法自顶向下解这个问题时,用来解原问题的递归算法可以反复的解同样的子问题,而不是总在产生新的子问题。

基本步骤

  1. 分析最优解的性质,并刻划其结构特征,即科学合理地表示状态。
  2. 递归地定义最优值,即写出状态转移方程。
  3. 以自底向上的方式(递推)或自顶向下的方式(记忆化搜索或称为备忘录法)计算最优值。
  4. 根据计算最优值时记录的信息,构造一个最优解。

例题

  1. 数字金字塔
    读题后可以知道 找子问题dp[i][j]:a[i][j]到第n层的最大值。自顶向下分析,从底向上递推出最后一行外,每一行的每个点的最大值等于自身加上下面一行对应左右两个点的最大值,从下往上递推,最顶部的即所求。
#include<bits/stdc++.h>
using namespace std;
const int Max = 1e3 + 5;

int a[Max][Max], dp[Max][Max];
int main(){
	int n;
	scanf("%d", &n); 
	//初始化a数组
	for(int i = 1; i <= n; i++){
		for(int j = 1; j <= i; j++){
			cin >> a[i][j];
		}
	} 

	dp[1][1] = a[1][1];//先把第一个值拷给dp
	//做dp数组递推
	for(int i = 2;i <= n; i++){
		for(int j = 1; j <= i; j++){
			dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - 1]) + a[i][j];
		} 
	} 
	int maxv = 0;
	for(int i = 1; i <= n; i++){
		maxv = max(maxv, dp[n][i]);
	}
	printf("%d", maxv);
    return 0;
}
  1. 最长上升子序列
    这道题我们可以d(i) 就是找以A[ i ]结尾的,在A[ i ]之前的最长上升子序列+1,即前 i 个数的 LIS 长度 + 1。当A[ i ]之前没有比A[ i ]更小的数时,d(i) = 1。所有的d(i)里面最大的那个就是最长上升子序列。 其实说的通俗点,就是每次都向前找比它小的数和比它大的数的位置,将第一个比它大的替换掉,这样操作虽然LIS序列的具体数字可能会变,但是很明显LIS长度还是不变的,因为只是把数替换掉了,并没有改变增加或者减少长度。

状态设计:F [ i ] 代表以 A [ i ] 结尾的 LIS 的长度

状态转移:F [ i ] = max { F [ j ] + 1 ,F [ i ] } (1 <= j < i,A[ j ] < A[ i ])

边界处理:F [ i ] = 1 (1 <= i <= n)

void init(){
	int i;
	scanf("%d", &n);
	for (i=0; i<n; i++)
		scanf("%d", &a[i]);
}

int lis() {
	int i, j, max = 0;
	for ( i = 0; i < n; i++) {
		dp[i] = 1; prev[i] = i;
	}
 	for ( i = 1; i < n; i++ )
      for ( j = 0; j < i; j++ )
         if ( a[i] > a[j] && dp[i] < dp[j] + 1 ) {
		  	dp[i] = dp[j] + 1;
	    	prev[i] = j;       // prev[]用于构造最优解
	       }
	for ( i = 0; i < n; i++ )
      if ( max < dp[i] ) {
         max = dp[i];   ans = i;  //ans用于构造最优解
      }
	return max;
}

void print(int i) {      //递归输出一组最优解 
   if (prev[i] == i) {	//边界
     printf(%d”, a[i]);
     return; 	
   }
   print(prev[i]);      //输出当前元素之前的序列
   printf(%d”, a[i]); //输出当前元素
}
 
  1. 俄罗斯套娃信封问题

1.首先,在套娃前进行准备工作,对信封按照第一维 w 进行升序排列,先得到一个 w 的递增序列;

2.其次,为了防止后面迭代过程中出错,即保证当信封的 w 相等时,仅会在一个序列里选中其中一个信封,所以将信封的第二维 h 作为次关键字将序列按降序排列(与第1步合并为以 w 为主关键字,以 h 为次关键字的快排);

3.在排序完成后,就可以开始套娃(bushi)动态规划了,用 dp[i] 表示当序列长度为 i 时最外面一层信封的 h

4.动态规划的步骤具体可以用二分实现(易证迭代过程中dp为严格单调递增序列):

(1)若当前信封的 h i h_{i} hi > > > dp[-1] 时,则
dp[len(dp) + 1] = = = h i h_{i} hi

(2)否则就二分求出 mid 使 dp[mid] ≤ \leq h i h_{i} hi < < < dp[mid + 1],且 dp[mid] = h i = h_{i} =hi

5.最后返回 len(dp) 即可。

时间复杂度: O ( n l o g n ) O(n log n) O(nlogn)(注:快排和基于二分的动态规划均为 O ( n l o g n ) O(n log n) O(nlogn)

class Solution:

    def maxEnvelopes(self, envelopes: List[List[int]]) -> int:
        if not envelopes:  # 空列表直接返回
            return 0

        length = len(envelopes)
        envelopes.sort(key=lambda x: (x[0], -x[1]))  # 按第一维升序,第二位降序对信封进行快排
        dp = [envelopes[0][1]]
        for i in range(1, length):
            if (num := envelopes[i][1]) > dp[-1]:
                dp.append(num)
            else:  # 二分查找部分
                index = bisect.bisect_left(dp, num)
                dp[index] = num
        return len(dp)

4.宠物小精灵之收服

解析:

既然这道题是DP问题,就要用DP的方法解决:
1.明确DP类型:01背包。

2.确定状态,这需要一定的经验。这道题来说,费用有两个,即:状态应有两维。这里用 f i j f_{ij} fij 表 示 当 精 灵 球 数 为 i i i , 体 力 为 j j j 时 表 示 收 服 的 最 大 小 精 灵 数 。 f i j f_{ij} fij 表示当精灵球数为 i i i ,体力为 j j j 时表示收服的最大小精灵数。 f i j f_{ij} fij 表示当精灵球数为 i i i ,体力为 j j j 时表示收服的最大小精灵数。

至此:我们已经解决了题目的第一问。代码如下:

#include<bits/stdc++.h>
using namespace std;
const int Max = 1005;
int n, m, t;
int a[Max], b[Max], dp[Max][Max];
int main(){
	scanf("%d %d %d", &n, &m, &t);
	for(int i = 1; i <= t; i++)
		scanf("%d %d", &a[i], &b[i]);
	for(int i = 1; i <= t; i++)
		for(int j = n; j >= a[i]; j--)
			for(int k = m; k >= b[i]; k--)
				dp[j][k] = max(dp[j][k], dp[j - a[i]][k - b[i]] + 1);
	printf("%d", dp[n][m]);
	return 0;	
}

这道题有些棘手的地方在第二问:体力值!
很显然,求出最大体力值的前提是求出最优方案,而要求出最优方案必然算出了所有方案,所以,我们只要根据状态的定义,遍历所有的方案,只要满足最大收服数,就更新答案,将总体力值 - 最小的体力消耗值就 = 最大剩余体力值。所以,只要将输出改为一下代码:

	for(int i = 0;i <= m; i++){
		if(dp[n][i] == dp[n][m]){
			printf("%d %d", dp[n][m], m-i);
			break;
		}
	}
  1. 地下城游戏

解题思路

一看到这个问题首先想到的就是动态规划,其次想的就是自顶向下,但是这样想的话就会有一个问题,就是我们需要判断每次到达一个房间后,我们的血量不能是负数,这给我们编写代码造成了很大的困难。如果采用自底向上的思路去做的话,那么问题就会变得简单很多。我们只需要取右和下的最小值(我们需要计算最少的能量),然后减去 d u n g o e n [ i ] [ j ] dungoen[i][j] dungoen[i][j],并且保证到达当前房间后的血量大于等于0就行了!

#include<bits/stdc++.h>
using namespace std;
const int Max = 1e3 + 5;
int n, m, a[Max][Max], dp[Max][Max];
int main() {
	memset(dp, 0x3f, sizeof(dp));
	scanf("%d %d", &n, &m);
	for(int i = 1; i <= n; i++) 
		for(int j = 1; j <= m; j++) 
			scanf("%d", &a[i][j]);
	dp[n + 1][m] = dp[n][m + 1] = 1;
	for(int i = n; i >= 1; i--)
		for(int j = m; j >= 1; j--)
			dp[i][j] = max(min(dp[i + 1][j], dp[i][j + 1]) - a[i][j], 1);
	printf("%d", dp[1][1]);
	return 0;
}

end

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值