动态规划
最长不下降子序列
感觉真的忘得差不多了。
#include<iostream>
using namespace std;
#define N 1000010
int n, num, a[N], f[N], len1, len2;
int main()
{
while(cin >> num)
a[++n] = num;
for(int i = 1; i <= n; i ++)
{
f[i] = 1;
for(int j = 1; j < i; j ++)
{
if(a[j] >= a[i] && f[j] + 1 > f[i])
f[i] = f[j] + 1;
len1 = max(len1, f[i]);
}
}
int f1[N];
for(int i = 1; i <= n; i ++)
{
f1[i] = 1;
for(int j = 1; j < i; j ++)
{
if(a[j] < a[i] && f1[j] + 1 > f1[i])
f1[i] = f1[j] + 1;
len2 = max(len2, f1[i]);
}
}
cout << len1 << endl << len2 << endl;
return 0;
}
简析
动态规划,就我现在的理解就是:把一个问题分为若干个子问题,保证每个问题的解决方式相同或相似。也就是阶段与状态的关系,这个阶段解决出来的问题,必须是能够保证让,后续的解决过程,及结果最优。
就例如这个问题,可以看做是求 i 前面的比自己小的最长单调序列。判断条件是if(a[j] < a[i] && f1[j] + 1 > f1[i]),循环一遍 i 之前的数字,因为f [ j ] ,相当于是之前判断过的,所以能保证它记录的是他之前的比自己小的最长单调序列。这就有一种递归的思想。
最长上升子序列的问题解决了,我又遇到了最长不下降子序列的问题。
for(int i = n - 1; i >= 1; i --)
{
for(int j = i + 1; j <= n; j ++)
{
if(a[j] < a[i] && smaller[j] + 1 >= smaller[i])
smaller[i] = smaller[j] + 1;
}
}
for(int i = 1; i <= n; i ++)
{
smaller[i] = 1;
for(int j = 1; j < i; j ++)
{
if(a[j] > a[i] && smaller[j] + 1 >= smaller[i])
smaller[i] = smaller[j] + 1;
}
}
正着求只有60分。找了组数据:
输入
100
157 165 138 130 141 206 160 164 216 216 145 227 180 147 170 216 154 144 171 230 205 137 169 181 146 133 220 138 175 207 173 155 136 167 144 166 140 191 145 162 214 213 151 200 166 131 221 154 161 229 136 194 215 202 137 202 157 132 166 215 218 230 168 217 131 189 203 131 207 176 172 211 187 158 165 156 179 194 200 145 130 183 174 143 148 218 213 187 204 221 160 169 168 224 163 132 226 135 201 217
输出
78
正着时的smaller
1 1 2 3 2 1 2 2 1 1 3 1 2 3 3 2 4 5 3 1 3 6 4 4 5 7 2 6 5 3 6 7 8 7 8 8 9 4 9 9 3 4 10 5 8 11 2 10 10 2 11 6 3 5 11 5 11 12 8 3 3 1 7 4 13 7 5 13 5 8 9 5 8 11 10 12 9 6 6 13 14 9 10 14 13 3 5 8 6 3 11 11 12 3 13 15 3 15 7 4
77
逆着
9 10 6 1 6 14 9 9 14 14 7 15 12 8 11 14 8 6 11 14 13 5 10 12 7 4 13 5 11 11 10 7 4 9 6 8 5 9 5 7 12 11 5 10 7 2 11 5 6 11 4 9 10 9 4 9 5 3 6 9 10 10 6 9 2 8 8 2 8 7 6 8 7 5 5 4 6 7 7 3 1 6 5 2 2 7 6 5 5 5 2 4 3 3 2 1 2 1 1 1
78
写了一堆的我终于意识到了:正着求的是左边比自己小的,逆着是求右边比自己小的 -_- 。对比一下数据就能发现……
有时候想明白以后,总是觉得想明白以前的自己好脑瘫。
哦对了,上面求右边比自己小的序列的题是合唱队形,就是要求队列中能使先单调上升,再单调下降的最长序列。思路就是把每个点左边比自己小的和右边比自己小的(这里都指最长单调序列)给加起来,找最大值。
优化
用线段树/树状数组,保存以一个数为结尾的最长上升子序列。那么枚举到一个数,可以用线段树/树状数组查找比他小的数中最长上升子序列。
举个例子:1 4 5 2 6 8 5 3
第一次
- 线段树中:1:1, 2:0, 3:0, 4:0 ,5:0 ,6:0 ,7:0, 8:0
第二次
- 线段树中:1:1, 2:0, 3:0, 4:2 ,5:0 ,6:0 ,7:0, 8:0
第三次
- 线段树中:1:1, 2:0, 3:0, 4:2 ,5:3 ,6:0 ,7:0, 8:0
第四次
- 线段树中:1:1, 2:2, 3:0, 4:2 ,5:3 ,6:0 ,7:0, 8:0
第五次
- 线段树中:1:1, 2:2, 3:0, 4:2 ,5:3 ,6:4,7:0, 8:0
第六次
- 线段树中:1:1, 2:2, 3:0, 4:2 ,5:3 ,6:4,7:0, 8:5
第七次
- 线段树中:1:1, 2:2, 3:0, 4:2 ,5:3 ,6:4,7:0, 8:5
第八次
- 线段树中:1:1, 2:2, 3:3, 4:2 ,5:3 ,6:4,7:0, 8:5
背包问题
01背包
采药问题就是一个经典题型,就是关于要不要取的问题。
[P1048 NOIP2005 普及组] 采药
边看代码跟容易理解
#include<iostream>
using namespace std;
#define N 1010
int T, n, t[N], w[N], ans[N];
int main()
{
cin >> T >> n;
for(int i = 1; i <= n; i ++)
cin >> t[i] >> w[i];
for(int i = 1; i <= n; i ++)
{
for(int j = T; j >= t[i]; j--)
{
ans[j] = max(ans[j], ans[j - t[i]] + w[i]);
}
}
cout << ans[T];
return 0;
}
简析
直接看 j 这个循环,为了防止越界,从 T(容量) 开始,向t[ i ],也就是这个药的体积递减,那么为什么要从后面开始向前遍历呢?
我们先放着这个问题,先考虑如何求最优方案。ans[ i ],这里的 i 表示这个容量下的最优放法。那么ans[ j - t[ i ] ],就是去掉这个药的情况下的最优解,这是我们之前已经求出来了的。dp我感觉有时候不用考虑那么多,只要先考虑普遍的情况,假设前面的已经求完了,再去想后面的怎么解决就好了。max( ans[ j ], ans[ j - t [ i ] ] + w[ i ] ) 那就是考虑加上 i 药品价值高还是不加高。前面说了ans[ j - t[ i ] ] 是不加情况下的最优解,那再加 i 肯定是加上 i 的最优解,与ans[ j ]比较,得出答案。(注意这里的ans[ j ]可以是前面求过的)
这时候再去考虑为什么倒序:假如是正序,ans[ j - t [ i ] ]有可能已经被改变过了,就有可能不是只放一遍(这是完全背包的做法)。可以看着代码再考虑一遍。
完全背包
//一维
#include<iostream>
using namespace std;
#define N 10000010
#define int long long
int T, n, t[N], w[N];
int dp[N];
signed main()
{
scanf("%lld%lld", &T, &n);
for(int i = 1; i <= n; i ++)
scanf("%lld%lld", &t[i], &w[i]);
for(int i = 1; i <= n; i ++)
{
for(int j = t[i]; j <= T; j ++)
{
dp[j] = max(dp[j], dp[j - t[i]] + w[i]);
}
}
printf("%lld", dp[T]);
return 0;
}
#include<iostream>
using namespace std;
#define N 10010
#define int long long
int T, n, t[N], w[N];
int dp[N][N];
signed main()
{
scanf("%lld%lld", &T, &n);
for(int i = 1; i <= n; i ++)
scanf("%lld%lld", &t[i], &w[i]);
for(int i = 1; i <= n; i ++)
{
for(int j = t[i]; j <= T; j ++)
{
dp[i][j] = max(dp[i - 1][j], dp[i][j - t[i]] + w[i]);
}
}
printf("%lld", dp[n][T]);
return 0;
}
当T太大的时候就只能用一维的了喔qwq
完全背包和01的区别就在于正序还是逆序,以及二维数组时,状态转移方程中的第一维数组。
顺序上面讲01已近解释过,至于为什么01是 $$dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - t[i]] + w[i]);$$
而完全是 $$dp[i][j] = max(dp[i - 1][j], dp[i][j - t[i]] + w[i]);$$ 这是因为完全背包 i 物品能无限选,那么第二维数组就应该从第 $$i - 2w[ i ]$$ 更新到 $$i - w[ i ]$$ ,更新的物品并没有改变。
分组背包
物品大致可分为 k 组,每组中的物品相互冲突,现在,他想知道最大的利用价值是多少。
两个数 m,n,表示一共有 n 件物品,总重量为 m。接下来 n 行,每行 3 个数 $$a_i,b_i,c_i$$ 表示物品的重量,利用价值,所属组数。
思路
可以把它当做01背包做,只不过枚举的是每一组。先决定这一组中是不是要选,然后再在这一组里找最优解。
本文来自博客园,作者:huaziqi 转载请注明原文链接:https://www.cnblogs.com/huaziqi/p/16521304.html

浙公网安备 33010602011771号