首页 > 其他分享 >第九节 动态规划 - 1

第九节 动态规划 - 1

时间:2023-07-19 16:12:51浏览次数:36  
标签:status 10 Accepted 第九节 points memory 动态 规划 dp

简介

动态规划(Dynamic Programming, DP)及其解决的问题、根据其设计的算法及优化。

动态规划是一种通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。

由于动态规划并不是某种具体的算法,而是一种解决特定问题的方法,因此它会出现在各式各样的数据结构中,与之相关的题目种类也更为繁杂。

在 \(OI\) 中,计数等非最优化问题的递推解法也常被不规范地称作 DP,因此本章将它们一并列出。事实上,动态规划与其它类型的递推的确有很多相似之处,学习时可以注意它们之间的异同。

背景

\(DP\) 是信奥中十分重要的一部分,基本上就是 \(csp−j\) 考 \(DP\),\(csp−s\) 考 \(DP\), \(NOIP\) 考 \(DP\), \(NOI\)还是考 \(DP\)。并且,万物皆可 \(DP\)。

所以学好 \(DP\) 是学好信息学奥赛,打好各类比赛的基础!

而背包作为 \(DP\) 的基础,自然是重中之重。

这篇文章主要讲的是背包的思想和各种背包的实现

01背包

首先,我们来看二维的 \(01\) 背包。

这是最基础的背包问题,特点是:每种物品仅有一件,可以选择放或不放。

题目描述:
有 \(N\) 件物品和一个容量为 \(V\) 的背包。第 \(i\) 件物品的费用是 \(v_i\),价值是 \(w_i\)。求解将哪些物品装入背包可使价值总和最大。

数组定义: \(dp[maxn][maxV]\)

初始化:\(dp\) 数组全部赋值成 \(0\)

状态转移方程:

用子问题定义状态:即 \(dp[i][j]\) 表示前 \(i\) 件物品恰放入一个容量为 \(j\) 的背包可以获得的最大价值。则其状态转移方程便是:

\(dp[i][j] = max(dp[i − 1][j], dp[i − 1][j − w[i]]+v[i])\)

输出: \(printf("\%d", dp[n][V])\)

代码:

for(int i=1;i<=n;++i)
   for(int j=0;j<=V;++j)
       if (v[i]>j) dp[i][j]=dp[i-1][j];
       else dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);

接着我们进行优化

以上方法的时间和空间复杂度均为 \(O(Vn)\),其中时间复杂度应该已经不能再优化了,但空间复杂度却可以优化到 \(O(V)\)。

先考虑上面讲的基本思路如何实现。

肯定是有一个主循环 \(i = 1..N\),每次算出来二维数组 \(dp[i][0..V]\) 的所有值。

那么,如果只用一个数组 \(dp[0..V]\),能不能保证第 \(i\) 次循环结束后 \(dp[v]\) 中表示的就是我们定义的状态 \(dp[i][j]\) 呢?

\(dp[i][j]\) 是由 \(dp[i − 1][j]\) 和 \(dp[i − 1][j − w[i]]\) 两个子问题递推而来,能否保证在推 \(dp[i][j]\) 时(即在第 \(i\) 次主循环中推 \(dp[j]\) 时)能够得到 \(dp[i − 1][j]\) 和 \(dp[i − 1][j − w[i]]\) 的值呢?

事实上,这要求在每次主循环中我们以 \(j = V..0\) 的顺序推 \(dp[j]\),这样才能保证推 \(dp[j]\) 时 \(f[j − v[i]]\) 保存的是状态 \(f[i − 1][j − w[i]]\) 的值。

因此,\(dp\) 数组就可以从两维变为一维,变成 \(dp[maxV]\);

状态转移方程:

\(dp[j] = max(dp[j], dp[j − w[i]] + v[i])\)

这样,核心代码就变成了

for(int i=1;i<=n;++i)
    for(j=V;j>=v[i];--j)
        dp[j]=max(dp[j],dp[j-v[i]]+w[i]);

动态规划原理

能用动态规划解决的问题,需要满足三个条件:最优子结构,无后效性和子问题重叠。

最优子结构

具有最优子结构也可能是适合用贪心的方法求解。

注意要确保我们考察了最优解中用到的所有子问题。

  1. 证明问题最优解的第一个组成部分是做出一个选择;
  2. 对于一个给定问题,在其可能的第一步选择中,假定你已经知道哪种选择才会得到最优解。你现在并不关心这种选择具体是如何得到的,只是假定已经知道了这种选择;
  3. 给定可获得的最优解的选择后,确定这次选择会产生哪些子问题,以及如何最好地刻画子问题空间;
  4. 证明作为构成原问题最优解的组成部分,每个子问题的解就是它本身的最优解。方法是反证法,考虑加入某个子问题的解不是其自身的最优解,那么就可以从原问题的解中该子问题的最优解替换掉当前的非最优解,从而得到原问题的一个更优的解,从而与原问题最优解的假设矛盾。

要保持子问题空间尽量简单,只在必要时扩展。

最优子结构的不同体现在两个方面:

  1. 原问题的最优解中涉及多少个子问题;
  2. 确定最优解使用哪些子问题时,需要考察多少种选择。

子问题图中每个定点对应一个子问题,而需要考察的选择对应关联至子问题顶点的边。

无后效性

已经求解的子问题,不会再受到后续决策的影响。

子问题重叠

如果有大量的重叠子问题,我们可以用空间将这些子问题的解存储下来,避免重复求解相同的子问题,从而提升效率。

基本思路

对于一个能用动态规划解决的问题,一般采用如下思路解决:

  1. 将原问题划分为若干 阶段,每个阶段对应若干个子问题,提取这些子问题的特征(称之为 状态);
  2. 寻找每一个状态的可能 决策,或者说是各状态间的相互转移方式(用数学的语言描述就是 状态转移方程)。
  3. 按顺序求解每一个阶段的问题。

A. 我的眼睛就是尺

时间:1s 空间:256M

题目描述

“就这速度我也能滑出来” “他是指定进不了下一轮的” “我的眼睛就是尺” 这是王濛在解说冬奥会时的精彩语录。今天你也来展示一下什么叫做眼睛就是尺,你有 \(n\) 个东西,你一眼就能看出来他们有多重(因为上面写了重多少斤),让你选几个东西,使得他们的总重量在不小于t的情况下,尽可能地接近 \(t\),求出这个最小的总重量。如果不存在则输出 \(-1\)。

输入格式

第一行两个整数 \(n,t\)

第二行 \(n\) 个整数,第 \(i\) 个整数代表第 \(i\) 件物品的重量

输出格式

一个整数.

样例输入

3 30
25
10
23

样例输出

33

约定
\(1 \le n \le 50\), \(1 \le t \le 10000\), \(1 \le a_i \le 10000\)

点击查看代码
#include<iostream>
#include<string.h>
using namespace std;

int n, t, a[60];
long long sum = 0, ans = 0x3f3f3f3f;

void dfs(int now, long long res) {
    if(now > n) return;
    if(res >= t) {
        ans = min(ans, res);
        return;
    }
    dfs(now + 1, res);
    dfs(now + 1, res + a[now]);
    return;
}

int main() {
    cin >> n >> t;
    for(int i = 1; i <= n; i ++) {
        cin >> a[i];
        sum += a[i];
    }
    if(sum < t) {
        cout << "-1\n";
        return 0;
    }
    dfs(1, 0);
    cout << ans << endl;
    return 0;
}
编译结果
compiled successfully
time: 9ms, memory: 3500kb, score: 100, status: Accepted
> test 1: time: 1ms, memory: 3392kb, points: 10, status: Accepted
> test 2: time: 1ms, memory: 3428kb, points: 10, status: Accepted
> test 3: time: 0ms, memory: 3500kb, points: 10, status: Accepted
> test 4: time: 1ms, memory: 3340kb, points: 10, status: Accepted
> test 5: time: 1ms, memory: 3404kb, points: 10, status: Accepted
> test 6: time: 1ms, memory: 3352kb, points: 10, status: Accepted
> test 7: time: 1ms, memory: 3388kb, points: 10, status: Accepted
> test 8: time: 1ms, memory: 3444kb, points: 10, status: Accepted
> test 9: time: 1ms, memory: 3352kb, points: 10, status: Accepted
> test 10: time: 1ms, memory: 3384kb, points: 10, status: Accepted

不同点:

这道题我们需要求出体积至少为 \(j\) 的最小价值

初始化: \(dp[0][0] = 0\), \(dp[0][1…t] = inf\)

状态转移方程: \(dp[i][j] = min(dp[i − 1][j], dp[i − 1][max(0, j − w[i])] + v[i])\)

拓展:

需要求体积至少为 \(j\) 的方案数

初始化: \(dp[0][0] = 1\), \(dp[0][1…t] = 0\)

状态转移方程: \(dp[i][j] = dp[i − 1][j] + dp[i − 1][max(0, j − w[i])]\)

恰好为 j

\((1)\) 恰好为 \(j\) 的最小价值;

\((2)\) 恰好为 \(j\) 的最大价值;

\((3)\) 恰好为 \(j\) 的方案数;

解法: 与原本的 \(01\) 背包相同,初始化不同

初始化: \(dp[0][0] = 0\), \(dp[0][1…t] = −1 / inf\)

B. we are 伐木累

时间:1s 空间:256M

题目描述

光头强强是个伐木工人,有天他趁着熊三和熊四不在,又来砍树啦!

他现在把一棵高为n的树砍倒了,但因为树太大了实在不好搬,所以他打算把这棵树砍成若干段。而他的老板是有要求的,只收长度为a或者长度为b或者长度为c的三种型号的木段,现在他希望最后得到的木段的数量尽可能的多。光头强强是个节俭的人,所以他希望树恰好被分完,不能有剩余边角料,如果不能分完则输出0。

输入格式

一行四个整数 \(n,a,b,c\) \((1 \le n,a,b,c \le 1000000)\)

输出格式

一个整数代表木段数量的最大值。

样例输入

5 5 3 2

样例输出

2
点击查看代码
#include<iostream>
#include<string.h>
using namespace std;

int n, a, b, c;
int dp[1000005];

int main() {
    cin >> n >> a >> b >> c;
    dp[0] = 0;
    for (int i = 1; i <= n; i++) {
        dp[i] = -1;
        if (i >= a && dp[i - a] != -1) dp[i] = max(dp[i], dp[i - a] + 1);
        if (i >= b && dp[i - b] != -1) dp[i] = max(dp[i], dp[i - b] + 1);
        if (i >= c && dp[i - c] != -1) dp[i] = max(dp[i], dp[i - c] + 1);
    }
    if(dp[n] < 0) cout << 0 << endl;
    else cout << dp[n] << endl;
    return 0;
}
编译结果
compiled successfully
time: 17ms, memory: 7280kb, score: 100, status: Accepted
> test 1: time: 0ms, memory: 3432kb, points: 10, status: Accepted
> test 2: time: 1ms, memory: 3468kb, points: 10, status: Accepted
> test 3: time: 1ms, memory: 3348kb, points: 10, status: Accepted
> test 4: time: 0ms, memory: 3436kb, points: 10, status: Accepted
> test 5: time: 0ms, memory: 3448kb, points: 10, status: Accepted
> test 6: time: 1ms, memory: 3400kb, points: 10, status: Accepted
> test 7: time: 0ms, memory: 3252kb, points: 10, status: Accepted
> test 8: time: 5ms, memory: 6444kb, points: 10, status: Accepted
> test 9: time: 3ms, memory: 6848kb, points: 10, status: Accepted
> test 10: time: 6ms, memory: 7280kb, points: 10, status: Accepted

C. 宝物筛选

题目描述:

终于,破解了千年的难题。小 \(F\) 找到了王室的宝物室,里面堆满了无数价值连城的宝物。

这下小 \(F\) 可发财了,嘎嘎。但是这里的宝物实在是太多了,小 \(F\) 的采集车似乎装不下那么多宝物。看来小 \(F\) 只能含泪舍弃其中的一部分宝物了。

小 \(F\) 对洞穴里的宝物进行了整理,他发现每样宝物都有一件或者多件。他粗略估算了下每样宝物的价值,之后开始了宝物筛选工作:小 \(F\) 有一个最大载重为 \(W\) 的采集车,洞穴里总共有 \(n\) 种宝物,每种宝物的价值为 \(v_i\),重量为 \(w_i\),每种宝物有 \(m_i\)​ 件。小 \(F\) 希望在采集车不超载的前提下,选择一些宝物装进采集车,使得它们的价值和最大。

输入格式:

第一行为一个整数 \(n\) 和 \(W\),分别表示宝物种数和采集车的最大载重。

接下来 \(n\) 行每行三个整数 \(v_i\), \(w_i\), \(m_i\)。

输出格式:

输出仅一个整数,表示在采集车不超载的情况下收集的宝物的最大价值。

样例输入:

4 20
3 9 3
5 9 1
9 4 2
8 1 3

样例输出:

47

数据规模:

对于 \(100\%\) 的数据,\(n \le ∑m_i \le 10 ^ 5\),\(0 \le W \le 4 × 10 ^ 4\),\(1 \le n \le 100\)。

点击查看代码
#include<iostream>
#include<cstdio>
using namespace std;

int n, m;
int num, cnt, ans, f[100005], v[1000005], w[1000005];

int main() {
	scanf("%d %d", &n, &m);
	for(int i=1, x, y, z; i <= n; i++) {
		scanf("%d %d %d", &x, &y, &z);
		num = 1;
		while(z >= num) {
			z -= num;
			cnt++;
			v[cnt] = num * x;
			w[cnt] = num * y;
			num *= 2;
		}
		cnt++;
		v[cnt] = z * x;
		w[cnt] = z * y;
	}
	for(int i = 1; i <= cnt; i++)
		for(int j = m; j >= w[i]; j--)
			f[j] = max(f[j], f[j - w[i]] + v[i]);
	for(int j = 0; j <= m; j++) ans = max(ans, f[j]);
	printf("%d", ans);
	return 0;
}
编译结果
compiled successfully
time: 69ms, memory: 5820kb, score: 100, status: Accepted
> test 1: time: 9ms, memory: 5768kb, points: 10, status: Accepted
> test 2: time: 0ms, memory: 5748kb, points: 10, status: Accepted
> test 3: time: 1ms, memory: 5652kb, points: 10, status: Accepted
> test 4: time: 16ms, memory: 5784kb, points: 10, status: Accepted
> test 5: time: 15ms, memory: 5796kb, points: 10, status: Accepted
> test 6: time: 16ms, memory: 5612kb, points: 10, status: Accepted
> test 7: time: 0ms, memory: 5628kb, points: 10, status: Accepted
> test 8: time: 3ms, memory: 5636kb, points: 10, status: Accepted
> test 9: time: 8ms, memory: 5820kb, points: 10, status: Accepted
> test 10: time: 1ms, memory: 5664kb, points: 10, status: Accepted

D. 学习king

时间:1s 空间:512M

题目描述:

众所周知,期末考试是一件非常恐怖的事情,比如 \(Bob\) 马上期末考试,而他因为玩原神过于入迷,导致他学了但又好像啥都没学。他不想摆烂,所以打算奋起直追,他还有 \(N\) 门课需要去复习(预习),但现在距离期末测试只有 \(M\) 天了。根据不同的复习策略,分配各科的复习时间,最后获得的效果也不同,问他应如何安排,使得最终能够取得最好的复习效果呢?

输入格式:

第一行两个整数 \(N,M\) ,分别代表还有 \(N\) 门课要复习和剩下 \(M\) 天。

接下来 \(N\) 行 \(M\) 列整数,第 \(i\) 行第 \(j\) 列记作 \(c[i][j]\) ,代表第 \(i\) 门课,如果 \(Bob\) 花 \(j\) 天学习,可以取得的效果。

输出格式:

一行一个整数代表 \(Bob\) 可以获得的效果最大值。

样例1输入:

2 2
1 2
1 4

样例1输出:

4

约定与提示: \(1 \le N,M \le 200\)

点击查看代码
#include<iostream>
#include<string.h>
using namespace std;

int n, m, a[205][205], dp[205][205];

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 = 0; j <= m; j ++) {
            dp[i][j] = dp[i - 1][j];
            for(int k = 1; k <= j; k ++) {
                dp[i][j] = max(dp[i][j], dp[i - 1][j - k] + a[i][k]);
            }
        }
    }
    cout << dp[n][m] << endl;
    return 0;
}
编译结果
compiled successfully
time: 22ms, memory: 3848kb, score: 100, status: Accepted
> test 1: time: 0ms, memory: 3508kb, points: 10, status: Accepted
> test 2: time: 0ms, memory: 3436kb, points: 10, status: Accepted
> test 3: time: 1ms, memory: 3420kb, points: 10, status: Accepted
> test 4: time: 1ms, memory: 3520kb, points: 10, status: Accepted
> test 5: time: 1ms, memory: 3576kb, points: 10, status: Accepted
> test 6: time: 1ms, memory: 3516kb, points: 10, status: Accepted
> test 7: time: 5ms, memory: 3680kb, points: 10, status: Accepted
> test 8: time: 2ms, memory: 3716kb, points: 10, status: Accepted
> test 9: time: 3ms, memory: 3540kb, points: 10, status: Accepted
> test 10: time: 8ms, memory: 3848kb, points: 10, status: Accepted

标签:status,10,Accepted,第九节,points,memory,动态,规划,dp
From: https://www.cnblogs.com/So-noSlack/p/17564623.html

相关文章

  • 加载器、链接器、动态链接器概念
    动态链接器:共享库(sharedlibrary)是致力于解决静态库缺陷的一个现代创新产物。共享库是一个目标模块,在运行或加载时,可以加载到任意的内存地址,并和一个在内存中的程序链接起来。这个过程称为动态链接(dynamiclinking),是由一个叫做动态链接器(dynamiclinker)的程序来执行的。链接器:......
  • Linux的nm查看动态和静态库中的符号
    功能列出.o.a.so中的符号信息,包括诸如符号的值,符号类型及符号名称等。所谓符号,通常指定义出的函数,全局变量等等。 使用nm[option(s)][file(s)]有用的options:-A在每个符号信息的前面打印所在对象文件名称;-C输出demangle过了的符号名称;-D打印动态符号;-l使用对......
  • vue-element-admin改为从后台拿动态路由
    改为从后台拿动态路由,大概如下步骤:1、后台增加接口,返回动态路由数据2、前端增加请求动态路由接口请求3、修改src/route/index.js去掉原有的动态路由,增加组件名和组件对象映射map4、修改src/store/modules/permission.js修改当前权限判断处理方法 generateRoutes一、后......
  • jdbc-plus是一款基于JdbcTemplate增强工具包,基于JdbcTemplate已实现分页、多租户、动
    ......
  • 代码随想录算法训练营第57天 | ● 647. 回文子串 ● 516.最长回文子序列 ● 动
     第九章 动态规划part17●  647. 回文子串  ●  516.最长回文子序列●  动态规划总结篇 今天 我们就要结束动态规划章节了,大家激不激动!!!   详细布置   647. 回文子串    动态规划解决的经典题目,如果没接触过的话,别硬想 直接看题解。https:......
  • 代码随想录算法训练营第58天 | ● 739. 每日温度 ● 496.下一个更大元素 I - 第1
     第十章 单调栈part01 ●  739. 每日温度 ●  496.下一个更大元素 I    详细布置    739. 每日温度  今天正式开始单调栈,这是单调栈一篇扫盲题目,也是经典题。 大家可以读题,思考暴力的解法,然后在看单调栈的解法。 就能感受出单调栈的巧妙 ......
  • 代码随想录算法训练营第59天 | ● 503.下一个更大元素II ● 42. 接雨水 - 第10章
     第十章 单调栈part02 ●  503.下一个更大元素II ●  42. 接雨水    详细布置   503.下一个更大元素II  这道题和 739. 每日温度 几乎如出一辙,可以自己尝试做一做 https://programmercarl.com/0503.%E4%B8%8B%E4%B8%80%E4%B8%AA%E6%9B%B4%E5%......
  • 代码随想录算法训练营第60天 | ● 84.柱状图中最大的矩形 - 第10章 动态规划part03
     第十章 单调栈part03有了之前单调栈的铺垫,这道题目就不难了。  ●  84.柱状图中最大的矩形   今天是训练营最后一天,恭喜坚持两个月的录友们,接下来可以写一篇自己 代码随想录一刷的总结。好好回顾一下,这两个月自己的博客内容,以及自己的收获。  ......
  • vue3+vite4实现动态引入图片
    本来是想使用vue2时使用的require,但是在运行时却突然报错:看到上面的报错让我很懵,require为啥不能使用呢??经过我不懈的努力,终于找到原因:在Vue3和Vite4中,不再推荐使用CommonJS的require语法,而是使用ECMAScript模块(ESM)的import语法。Vite4默认支持ESM,因此在使用......
  • 动态加载页面的爬虫方法
    首先,可以直接手动拉到网页最下面,然后把F12里面的网页节点元素复制成文本,去获取目标进行下载,代码如下,用到的库BeautifulSoup:importosimporturllib.requestimportrefrombs4importBeautifulSoupasbsimportrandomasrdimporttimedefget_imgs(text):soup=bs......