寒假集训专题六:动态规划
ESAY1:最大子段和
P1115 最大子段和
题目描述
给出一个长度为 \(n\) 的序列 \(a\),选出其中连续且非空的一段使得这段和最大。
输入格式
第一行是一个整数,表示序列的长度 \(n\)。
第二行有 \(n\) 个整数,第 \(i\) 个整数表示序列的第 \(i\) 个数字 \(a_i\)。
输出格式
输出一行一个整数表示答案。
输入输出样例 #1
输入 #1
7
2 -4 3 -1 2 -4 3
输出 #1
4
说明/提示
样例 1 解释
选取 \([3, 5]\) 子段 \(\{3, -1, 2\}\),其和为 \(4\)。
数据规模与约定
- 对于 \(40\%\) 的数据,保证 \(n \leq 2 \times 10^3\)。
- 对于 \(100\%\) 的数据,保证 \(1 \leq n \leq 2 \times 10^5\),\(-10^4 \leq a_i \leq 10^4\)。
解题思路:
通常使用一个数组来存储状态,基本上思路是用数组来存储前i个数字里的最大子段和,最后返回数组下标n上储存的值。
但是这样做会产生额外的空间,是可以进行优化的,就是直接选用sum=nums[1],利用sum来记录目前记录子段和,如果目前字段和对于提升这个最大值没有贡献比如负数啥的,那么这一段在我们想法中应当是被舍弃的,另起字段便是。那么这个时候判断就是当前下标储存数和当前储存的子段和加上目前选中数做大小比较,如果小于,便以当前数为开端另起子段,没有就加入字段。接着更新我们存储的答案。
#include<bits/stdc++.h>
using namespace std;
//时间复杂度O(n),空间复杂度O(n)
int main()
{
int n;
int ans = INT_MIN;
int cur_sum = 0;
vector<int> nums;
cin >> n;
nums.resize( n+1, 0 );
for( int i = 1; i <= n; i++ )
{
cin >> nums[i];
}
for( int i = 1; i <= n; i++ )
{
//判断当前和是否对最大子段和有贡献,比如为负那么就应该舍弃
cur_sum = max( nums[i], cur_sum+nums[i] );
ans = max( ans, cur_sum );
}
cout << ans << endl;
}
ESAY2:采药
P1048 [NOIP 2005 普及组] 采药
题目描述
辰辰是个天资聪颖的孩子,他的梦想是成为世界上最伟大的医师。为此,他想拜附近最有威望的医师为师。医师为了判断他的资质,给他出了一个难题。医师把他带到一个到处都是草药的山洞里对他说:“孩子,这个山洞里有一些不同的草药,采每一株都需要一些时间,每一株也有它自身的价值。我会给你一段时间,在这段时间里,你可以采到一些草药。如果你是一个聪明的孩子,你应该可以让采到的草药的总价值最大。”
如果你是辰辰,你能完成这个任务吗?
输入格式
第一行有 \(2\) 个整数 \(T\)(\(1 \le T \le 1000\))和 \(M\)(\(1 \le M \le 100\)),用一个空格隔开,\(T\) 代表总共能够用来采药的时间,\(M\) 代表山洞里的草药的数目。
接下来的 \(M\) 行每行包括两个在 \(1\) 到 \(100\) 之间(包括 \(1\) 和 \(100\))的整数,分别表示采摘某株草药的时间和这株草药的价值。
输出格式
输出在规定的时间内可以采到的草药的最大总价值。
输入输出样例 #1
输入 #1
70 3
71 100
69 1
1 2
输出 #1
3
说明/提示
【数据范围】
- 对于 \(30\%\) 的数据,\(M \le 10\);
- 对于全部的数据,\(M \le 100\)。
【题目来源】
NOIP 2005 普及组第三题
解题思路:
本题为很经典的背包dp问题,选用一维数组来存储状态,遍历时遍历每个下标,并且从时间上限开始逆向遍历状态数组,给出状态转移方程dp[ti] = max( dp[ti], dp[ti - 消耗的t] + 消耗的t带来的价值 ).
此处选用逆向遍历是为了避免重复计算,以保证答案的正确性。
#include<bits/stdc++.h>
using namespace std;
int main()
{
int T, M;
cin >> T >> M;
vector<int> w(M+1);
vector<int> t(M+1);
vector<int> dp(T+1);
for( int i = 1; i <= M; i++ )
{
cin >> t[i] >> w[i];
}
for( int i = 1; i <= M; i++ )
{
for( int j = T; j >= t[i]; j-- )
{
dp[j] = max( dp[j] , dp[j - t[i]] + w[i] );
}
}
cout << dp[T] << endl;
}
MEDIUM1:宝物筛选
P1776 宝物筛选
题目描述
终于,破解了千年的难题。小 FF 找到了王室的宝物室,里面堆满了无数价值连城的宝物。
这下小 FF 可发财了,嘎嘎。但是这里的宝物实在是太多了,小 FF 的采集车似乎装不下那么多宝物。看来小 FF 只能含泪舍弃其中的一部分宝物了。
小 FF 对洞穴里的宝物进行了整理,他发现每样宝物都有一件或者多件。他粗略估算了下每样宝物的价值,之后开始了宝物筛选工作:小 FF 有一个最大载重为 \(W\) 的采集车,洞穴里总共有 \(n\) 种宝物,每种宝物的价值为 \(v_i\),重量为 \(w_i\),每种宝物有 \(m_i\) 件。小 FF 希望在采集车不超载的前提下,选择一些宝物装进采集车,使得它们的价值和最大。
输入格式
第一行为一个整数 \(n\) 和 \(W\),分别表示宝物种数和采集车的最大载重。
接下来 \(n\) 行每行三个整数 \(v_i,w_i,m_i\)。
输出格式
输出仅一个整数,表示在采集车不超载的情况下收集的宝物的最大价值。
输入输出样例 #1
输入 #1
4 20
3 9 3
5 9 1
9 4 2
8 1 3
输出 #1
47
说明/提示
对于 \(30\%\) 的数据,\(n\leq \sum m_i\leq 10^4\),\(0\le W\leq 10^3\)。
对于 \(100\%\) 的数据,\(n\leq \sum m_i \leq 10^5\),\(0\le W\leq 4\times 10^4\),\(1\leq n\le 100\)。
解题思路:
这题其实也是一种背包问题,只是需要考虑这多重背包在朴素动态规划模板下可能会出现的问题。
如果我们用常用的背包模板,在我们更新状态时我们要考虑样本数目的问题,然后我们会对样本数量再进行一层循环的遍历,这样在最坏情况下时间复杂度可能会来到n的3次方。
再看题目的数据范围,很显然,时间上会爆。
那么我们就需要优化这个更新过程,我们像二分那样把一个需要遍历的区间不断的分割,然后在区间内进行更新,是不是就行了。
我们就用到了二进制分解的方法,将样本数量分为2的0次方,2的1次方......以此类推,这样我们在考虑样本的时候考虑的就是次数而不是样本总量,将这个过程变成了logn的复杂度。
但是我们会遇到并不是2的次方结果的情况。那么这里我们就需要额外写剩余部分的更新方式了。
#include<bits/stdc++.h>
using namespace std;
int main()
{
int n, W;
cin >> n >> W;
vector<int> v(n+1);
vector<int> w(n+1);
vector<int> m(n+1);
vector<int> dp(W+1);
for( int i = 1; i <= n; i++ )
{
cin >> v[i] >> w[i] >> m[i];
}
for( int i = 1; i <= n; i++ )
{
for( int j = 1; j <= m[i]; j *= 2 )
{
for( int k = W; k >= j * w[i]; k-- )
{
dp[k] = max( dp[k], dp[k-(j*w[i])]+(j*v[i]));
}
m[i] -= j;
}
if( m[i] > 0 )
{
for( int j = W; j >= m[i] * w[i]; j-- )
{
dp[j] = max( dp[j], dp[j-m[i]*w[i]] + m[i] * v[i] );
}
}
}
// for (int i = 1; i <= n; i++) {
// for (int k = W; k >= w[i]; k--) { // 逆序更新
// for (int j = 1; j <= m[i] && j * w[i] <= k; j++) {
// dp[k] = max(dp[k], dp[k - j * w[i]] + j * v[i]);
// }
// }
// }
cout << dp[W] << endl;
}
MEDIUM2:最长公共子序列
P1439 【模板】最长公共子序列
题目描述
给出 \(1,2,\ldots,n\) 的两个排列 \(P_1\) 和 \(P_2\) ,求它们的最长公共子序列。
输入格式
第一行是一个数 \(n\)。
接下来两行,每行为 \(n\) 个数,为自然数 \(1,2,\ldots,n\) 的一个排列。
输出格式
一个数,即最长公共子序列的长度。
输入输出样例 #1
输入 #1
5
3 2 1 4 5
1 2 3 4 5
输出 #1
3
说明/提示
- 对于 \(50\%\) 的数据, \(n \le 10^3\);
- 对于 \(100\%\) 的数据, \(n \le 10^5\)。
解题思路:
这道题如果用正常考虑我们可能会用二位数组存储以两排序坐标为下标的状态,但是同样如上面要考虑的,这样的开销是不有点大了,数据范围到1e5,二维数组做法时间复杂度O(n^2),只能说必爆,有没有更优的做法。
有的就是用lis,我们考虑到本身给出的是两个排序,说明里面的数一定是一样出现一次的,如果一个数在第二个排列里的位置越靠前,那是不是意味着它形成的子序列就越可能越长,所以我们就这样去找每一个数在第二个排列里的下标,然后用二分查找,寻找比它大的下标是否存在在动态数组中,如果不存在,那就可以把这个数推入动态数组中,作为我们已选取子序列的后继,如果存在,将它替换,保证始终是一个递增序列。
采用映射来读取第一排列中数在第二排列的坐标,利用stl的upper_bound()来查找更大下标位置,最后返回动态数组的大小就是我们要的答案,将时间复杂度降到了nlogn。
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e5+5;
int mp[maxn], p1[maxn], p2[maxn];
int main()
{
int n;
cin >> n;
vector<int> dp;
for( int i = 0; i < n; i++ )
{
cin >> p1[i];
}
for( int i = 0; i < n; i++ )
{
cin >> p2[i];
mp[p2[i]] = i;
}
for( int i = 0; i < n; i++ )
{
int index = mp[p1[i]];
auto it = upper_bound( dp.begin(), dp.end(), index );
if( it == dp.end() )
{
dp.push_back( index );
}
else
{
dp[ it - dp.begin() ] = index;
}
}
cout << dp.size() << endl;;
}
学习总结:
学习了背包dp,和最长上升子序列一类的动态规划问题,动态规划问题最重要的将问题分解成若干个子问题,寻求每一个子问题可能的决策,也就是状态与状态转移方程。然后求解这些问题。一般需要三个条件:最优子结构、无后效性和子问题重叠。
浙公网安备 33010602011771号