文章目录
- DP补题记录(1)
- 1、Maximum sum(2020.3.29)
- *2、Post Office(2020.3.30)
- *3、最大子矩阵(2020.3.31)
- 4、采药(2020.4.1)
- 5、公共子序列(2020.4.1)
- 6、吃糖果(2020.4.1)
- *7、最长公共子上升序列(LCIS)(2020.4.1)
- 8、登山(2020.4.4)
- 9、Exchange Rates(2020.4.4)
- 10、移动路线(2020.4.4)
- 11、摘花生(2020.4.4)
- 12、数字组合(2020.4.4)
- *13、糖果(2020.4.4)
- 14、计算字符串距离(2020.4.5)
- 15、判断整除(2020.4.5)
- 16、最大上升子序列和(2020.4.5)
- *17、怪盗基德的滑翔翼(2020.4.5)
- 18、买书(2020.4.6)
- *19、宠物小精灵之收服(2020.4.6)
- 20、开餐馆(2020.4.6)
- *21、数据包的调度机制(2020.4.7)
DP补题记录(1)
深感dp苦手,于是从今天(2020.3.29)开始每天若干道dp。
先把OpenJudge上的dp+递推题扒完。
打星号的题比较重要。
1、Maximum sum(2020.3.29)
http://noi.openjudge.cn/ch0206/1481/
这种题还是想了好久…太差了
一直在想怎么用状态去表示这两个和 结果想不出来
然后乍一看这东西像子段和 灵光一闪就做出来了
为以 为结尾的最大子段和,则
这样子我们正着跑一遍,求出以 为结尾的最大子段和
然后再倒过来跑一边,求出以 为开头的最大子段和
之后再扫一遍,一边扫一边更新当前的最大值和答案
AC代码:
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <queue>
#include <vector>
using namespace std;
typedef long long ll;
const int MAXN=5e4+10;
const int INF=0x3f3f3f3f;
int main()
{
int t;cin>>t;
while (t--)
{
int n;cin>>n;
static int a[MAXN];
for (int i=1;i<=n;++i) scanf("%d",&a[i]);
static int s1[MAXN],s2[MAXN];
s1[1]=a[1];
for (int i=2;i<=n;++i) s1[i]=max(s1[i-1]+a[i],a[i]);
s2[n]=a[n];
for (int i=n-1;i>=1;--i) s2[i]=max(s2[i+1]+a[i],a[i]);
int maxback=a[n],ans=-INF;//注意这里初始化 赋值成-1WA了一次
for (int i=n;i>=2;--i)//两段不能交叉
{
ans=max(s1[i-1]+maxback,ans);
maxback=max(maxback,s2[i]);
}
printf("%d\n",ans);
}
return 0;
}
*2、Post Office(2020.3.30)
http://noi.openjudge.cn/ch0206/162/
难。不会做。卡了两个小时。想了半天去网上翻的题解。
自己想的时候想到用 表示前 个村庄里放 个邮局,然后不会转移,纠结 怎么更新。
之后感觉少状态,又用 表示前 个村庄里放 个邮局,而且最后一个邮局在 位置,然后还是不会转移。
然后又想到区间dp,用 表示 村庄里放 个邮局,然后又不会转移。
心态崩了。
之后看题解,用 表示前 个村庄里放 个邮局,借助了个 数组,这玩意儿感觉有点像是找规律找出来的。然后网上的题解有些感觉讲得不是很明白,下面会注释。本质上来说还是个区间dp。
回归到dp的原本思想,考虑降低问题规模。先看一下只有 个邮局时候的最短距离。
感觉应该是放在中间,如果数量有偶数个就最中间两个比一比。
Q1:为什么?
看了一下网上没有证明。下面考虑证明。
设位置为 ,邮局放在第 个位置
那么距离和
然后看到 是个常数,我们构造个 ,那么 与 有相同的单调性
然后
然后找到单调性拐点,
之后把带 的放缩掉就好了。
这样我们就找到了 时的解
然后对于 ,我们就去枚举断点 ,把 分成两段,前面一段造 个,后面一段造 个,然后就可以把问题规模缩小了。
,其中 是预处理出来的放一个的最小值。
Q2:为什么前面一段造 个而不是 个?
首先如果再加一个循环肯定超时了。
第二,以 为例,那么这个转移方程就会变成 在 中造两个的最小值,一个是后面那一项没办法表示,另一个就是如果后面两个的第一个记作 ,那么在计算 的时候就会计算到。
然后计算 数组。这个东西应该是可以优化的,但是懒得去想了。 没有超,直接暴力之。
注:今天开始规范代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int INF = 0x3f3f3f3f;
int main()
{
int v, p, a[310]; cin >> v >> p;
for (int i = 1; i <= v; ++i) scanf("%d", &a[i]);
int sum[310][310]; memset(sum, 0, sizeof(sum));
for (int i = 1; i <= v; ++i)//计算[l,r]的最小花费
for (int j = i; j <= v; ++j)
{
int mid = (i + j) / 2, ans = 0;
for (int k = i;k <= j; ++k)
ans += fabs(a[k] - a[mid]);
sum[i][j] = ans;
if ((i + j) % 2 != 0)
{
mid++; ans = 0;
for (int k = i;k <= j; ++k)
ans += fabs(a[k] - a[mid]);
sum[i][j] = min(sum[i][j], ans);
}
}
int dp[310][31]; memset(dp, 0, sizeof(dp));//dp
for (int i = 1; i <= v; ++i) dp[i][1] = sum[1][i];//注意边界条件
for (int i = 1; i <= v; ++i)//注意循环边界
for (int j = 2; j <= p; ++j)
{
int ans = INF;
for (int k = 1; k < i; ++k) ans = min(ans, dp[k][j-1] + sum[k+1][i]);
dp[i][j] = ans;
}
printf("%d\n", dp[v][p]);
return 0;
}
*3、最大子矩阵(2020.3.31)
http://noi.openjudge.cn/ch0206/1768/
水平太差了。这题想了好久都没想出来。考虑是用 表示以 为右下角元素的矩阵最大值,然后没办法转移。考虑过用子段和,然后想不出具体的算法…最后还是网上翻的题解。
这题好像是14年NOIP初赛最后一题。那年我第一次参加初赛,这题根本不会做,最后还是找的规律填上去的…没想到六年之后还是不会做。
也算是补完了一个遗憾了。
正解还是考虑子段和。我们枚举第 列和第 列作为初始与结束端点,然后新开一个 的数组,存第 列中第 行到第 行的和。这个过程可以用前缀和优化。然后对这个数组跑子段和。
然后 取遍 ,不断更新最大值就好了。
不过这题做不出来可能是因为14年这题没做出来而搞出来的心理阴影罢 看完题解发现其实如果仔细想想还是可以做出来的
最后居然1A了。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 110;
const int INF = 0x3f3f3f3f;
int main()
{
int n, a[MAXN][MAXN] = {0}, sum[MAXN][MAXN] = {0};
cin >> n;
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
scanf("%d", &a[i][j]);
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
sum[j][i] = sum[j-1][i] + a[j][i];//前缀和
int ans = -INF;
for (int i = 1; i <= n; ++i)
for (int j = i; j <= n; ++j)
{
int arr[MAXN] = {0};
for (int k = 1; k <= n; ++k)
arr[k] = sum[j][k] - sum[i-1][k];
for (int k = 2; k <= n; ++k)//子段和 没高兴重开个S数组 直接在原来的数组上跑了
arr[k] = max(arr[k], arr[k-1] + arr[k]), ans=max(ans, arr[k]);
}
cout<< ans << endl;
return 0;
}
4、采药(2020.4.1)
http://noi.openjudge.cn/ch0206/1775/
裸的01背包。上代码。
这题我dp数组还开小了RE了一次…蠢了
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 110;
const int MAXT = 1010;
int main()
{
int T, M; cin >> T >> M;
int t[MAXN], v[MAXN];
for (int i = 1; i <= M; ++i)
scanf("%d%d", &t[i], &v[i]);
int dp[MAXT] = {0};
for (int i = 1; i <= M; ++i)
for (int j = T; j >= t[i]; --j)//01背包倒着写
dp[j] = max(dp[j], dp[j - t[i]] + v[i]);
cout << dp[T] << endl;
return 0;
}
5、公共子序列(2020.4.1)
http://noi.openjudge.cn/ch0206/1808/
裸的公共子序列。上代码。
题外话:这里的状态转移方程建议去https://www.bilibili.com/video/BV1X741127ZM,讲得非常清楚。
总算是把模模糊糊的“感觉应该这样”给理顺了。
另外交的时候从0开始dp数组越界还WA了一次。字符串上的dp的话dp数组还是从1开始比较好,字符串注意减位就行了。不然特判麻烦。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 210;
int main()
{
char s1[MAXN], s2[MAXN];
while (~scanf("%s%s", s1, s2))
{
int len1 = strlen(s1), len2 = strlen(s2);
int dp[MAXN][MAXN] = {0};
for (int i = 1; i <= len1; ++i)
for (int j = 1; j <= len2; ++j)
if (s1[i - 1] == s2[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1;
else dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
cout << dp[len1][len2] << endl;
}
return 0;
}
6、吃糖果(2020.4.1)
http://noi.openjudge.cn/ch0206/1944/
类斐波那契。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int main()
{
int n; cin >> n;
ll f[20];
f[1] = 1; f[2] = 2;
for (int i = 3; i <= n; ++i) f[i] = f[i - 1] + f[i- 2];
cout << f[n] << endl;
return 0;
}
*7、最长公共子上升序列(LCIS)(2020.4.1)
https://codeforces.com/contest/10/problem/D
http://acm.hdu.edu.cn/showproblem.php?pid=1423
为什么这题的网址不是OpenJudge呢?好问题。
这题本来是看到OpenJudge上有就做了一下,然后这输出没spj,把我心态弄崩了。
折腾了两个半小时把这题搞通了。
一开始的做法一直以为是对的,去HDU上交甚至都AC了…
后来修改了一下算法去CF交就被卡了…
下面以CF上的题目为例。HDU的更简单,稍微修改一下就可以过了。
那先说一下错误的方法吧:设 为 的LCIS,然后转移
这为什么错呢?因为 中的最大值不一定以 结尾啊…太蠢了
其实这个转移方程修改一点点就对了。既然这个和以 结尾有关,那就换个状态:
设 是以 为结尾,在 中的最大LCIS。(注意:这样定义答案不是 ,要自己在 中找个最大值出来)
那么转移就很方便了:
于是上代码。
要注意的是记录下某个节点的父节点,方便输出答案。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 510;
int main()
{
int m, n, a[MAXN], b[MAXN];
cin >> m;
for (int i = 1; i <= m; ++i) scanf("%d", &a[i]);
cin >> n;
for (int i = 1; i <= n; ++i) scanf("%d", &b[i]);
int dp[MAXN][MAXN] = {0};
static int trans[MAXN][MAXN];//记录父节点。一定要开static!不然爆栈!
memset(trans, 0, sizeof(trans));
for (int i = 1; i <= m; ++i)
for (int j = 1; j <= n; ++j)
if (a[i] != b[j])
dp[i][j] = dp[i - 1][j], trans[i][j] = j;
else
{
int ans = 0, pos = 0;
for (int k = 1; k < j; ++k)
if (b[j] > b[k] && dp[i - 1][k] > ans) ans = dp[i - 1][k], pos = k;
dp[i][j] = ans + 1;
trans[i][j] = pos;
}
int pos = 1;
for (int i = 1; i <= n; ++i)
if (dp[m][i] >= dp[m][pos]) pos = i;
cout << dp[m][pos] << endl;
stack <int> pri;
int x = m, y = pos;
while (x && y)
{
int yy = trans[x--][y];
if (yy != y) pri.push(b[y]);
y = yy;
}
int cnt = 0;
while (!pri.empty())
{
if (cnt++) cout << ' ' << pri.top();
else cout << pri.top();
pri.pop();
}
cout << endl;
return 0;
}
复杂度
那么能不能优化呢?
首先,对于最里面那层 循环是可以想办法优化掉的。
我们观察一下进入 循环的条件:
于是 循环里的 条件就可以改写成
那么我们觉得 无法优化的最大原因就在于枚举的每个 ,它们的最大值对应的 都是不同的。
但是进入这个 循环是有条件的: 。也就是说对于某个确定的 ,要进入 循环去寻找最大值的情况其实已经确定了。所以并不是每个 都要去枚举。
而 循环在 循环外面,所以我们其实就提前知道了这个 的值。而在进入原先的 循环前,我们一定已经扫描过了 。那么我们就可以在前面扫描的过程中去更新当前所获得的最大值,然后当进入原先 循环所在的位置时,我们直接把 赋值成这个最大值 就可以了。
然后我们就把复杂度降到了 。
接着我们看原来的转移方程,发现每次递推只会用到第 行的内容。那么我们就可以把空间压缩到一维,然后倒着更新。
但是真的对吗?
如果没有加 的优化,这个肯定是正确的。但是我们加了优化,也就是说 必须要在更新 之前扫过一遍。那么循环的顺序就只能正着扫,而不能倒着更新。
但是就算这样,我们要利用的最多也只有两行。然后用两行的滚动数组压一下空间就可以了。
这里再说明一下,这个记录状态转移的矩阵是不能压缩的。因为对于每个节点都有成为最优解的可能,所以每个节点都要保存它们的父节点。
上优化后的代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 510;
int main()
{
int m, n, a[MAXN], b[MAXN];
cin >> m;
for (int i = 1; i <= m; ++i) scanf("%d", &a[i]);
cin >> n;
for (int i = 1; i <= n; ++i) scanf("%d", &b[i]);
int dp[2][MAXN] = {0};//滚动数组
static int trans[MAXN][MAXN];
memset(trans, 0, sizeof(trans));
for (int i = 1; i <= m; ++i)
{
int maxi = 0;//记录最优解
for (int j = 1; j <= n; ++j)
{
if (a[i] != b[j]) dp[i % 2][j] = dp[(i - 1) % 2][j], trans[i][j] = j;
else dp[i % 2][j] = dp[(i - 1) % 2][maxi] + 1, trans[i][j] = maxi;
if (a[i] > b[j] && dp[(i - 1) % 2][j] > dp[(i - 1) % 2][maxi]) maxi = j;
}//其实这里用i和i + 1比较好,减的话可能会出现负数而产生各种奇怪的错
} //不过因为i是从1开始的就没这样的讲究了。下次写滚动数组还是写i + 1好了。
int pos = 1;
for (int i = 1; i <= n; ++i)
if (dp[m % 2][i] > dp[m % 2][pos]) pos = i;
cout << dp[m % 2][pos] << endl;
stack <int> pri;
int x = m, y = pos;
while (x && y)
{
int yy = trans[x--][y];
if (yy != y) pri.push(b[y]);
y = yy;
}
int cnt = 0;
while (!pri.empty())
{
if (cnt++) cout << ' ' << pri.top();
else cout << pri.top();
pri.pop();
}
cout << endl;
return 0;
}
奇怪的是这个加了优化的去CF上交跑了46ms,而没加优化的居然只跑了31ms…
估计是模运算把时间模掉了。空间比原来省了1300K。
2020.4.2 周四晚补题 暂停一天
2020.4.3 周五晚CFDiv2 暂停一天
8、登山(2020.4.4)
http://noi.openjudge.cn/ch0206/1996/
昨天CF血崩。B居然系统测试的时候超时了。淦。
-30
现在又到1410了。太差了。
这题巨坑。没想到下山的时候看的也对答案有贡献。WA了一发。
一个裸的上升子序列。
上AC代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int main()
{
int n; cin >> n;
int a[1010];
for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);
int dp_1[1010] = {0}, dp_2[1010] = {0};
int ans = 0;
for (int i = 1; i <= n; ++i)//上升
{
int maxi = 0;
for (int j = 1; j < i; ++j)
if (a[j] < a[i]) maxi = max(maxi, dp_1[j]);
dp_1[i] = maxi + 1;
}
for (int i = n; i >= 1; --i)//下降
{
int maxi = 0;
for (int j = n; j > i; --j)
if (a[j] < a[i]) maxi = max(maxi, dp_2[j]);
dp_2[i] = maxi + 1;
}
for (int i = 1; i <= n; ++i)
ans = max(ans, dp_1[i] + dp_2[i] - 1);
cout << ans << endl;
return 0;
}
9、Exchange Rates(2020.4.4)
http://noi.openjudge.cn/ch0206/2421/
这题题意太难懂了…懂了题意之后就不难了。两个dp数组存一存就好了。
加元由美元转移过来,美元由加元转移过来。
网上查了下第一组样例那个1001.60怎么来的之后1A。
注意运算用int存以及那个0.97的手续费。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int main()
{
int d;
while (scanf("%d", &d), d)
{
double a[410];
for (int i = 1; i <= d; ++i) scanf("%lf", &a[i]);
int dp_1[410] = {0}, dp_2[410] = {0};
dp_1[0] = 100000;
for (int i = 1; i <= d; ++i)
{
dp_1[i] = max(dp_1[i - 1], (int)(dp_2[i - 1] * a[i] * 0.97));//加元
dp_2[i] = max(dp_2[i - 1], (int)(dp_1[i - 1] / a[i] * 0.97));//美元
}
printf("%.2f\n", dp_1[d] / 100.0);
}
return 0;
}
10、移动路线(2020.4.4)
http://noi.openjudge.cn/ch0206/2718/
没啥讲的必要。直接上代码。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int main()
{
int m, n; cin >> m >> n;
ll dp[30][30] = {0};
dp[0][1] = 1;
for (int i = 1; i <= m; ++i)
for (int j = 1; j <= n; ++j)
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
cout << dp[m][n] << endl;
return 0;
}
11、摘花生(2020.4.4)
和上一题差不多。也直接上代码了。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int main()
{
int t; cin >> t;
while (t--)
{
int r, c; cin >> r >> c;
int a[110][110];
for (int i = 1; i <= r; ++i)
for (int j = 1; j <= c; ++j)
scanf("%d", &a[i][j]);
ll dp[110][110] = {0};
for (int i = 1; i <= r; ++i)
for (int j = 1; j <= c; ++j)
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) + a[i][j];
cout << dp[r][c] << endl;
}
return 0;
}
12、数字组合(2020.4.4)
http://noi.openjudge.cn/ch0206/2985/
背包模型。设 为用前 个数组成 的方案数,那么考虑第 个数取与不取两种情况,得 。
注意:边界条件是 ,并且 要从1开始。
顺手加了个滚动数组。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int main()
{
int n, t; cin >> n >> t;
int dp[1010] = {0};
int a[30] = {0};
for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);
dp[0] = 1;
for (int i = 1; i <= n; ++i)
for (int j = t; j >= a[i]; --j)
dp[j] += dp[j - a[i]];
cout << dp[t] << endl;
return 0;
}
*13、糖果(2020.4.4)
http://noi.openjudge.cn/ch0206/2989/
做了一个小时终于做出来了…感天动地
最开始的时候设的是 表示从前 个数中能最多能取 的 倍的糖果,最后答案是 ,然后转移写成了 。然后发现这转移矩阵算出来不对,再回头来看这方程发现有问题。看这组数据:
这组数据标答跑出来是 ,这个方法跑出来是 。看 五个数,和为 ,当 数组填充到此时,在之后的填充里这一组数就固定下来,一定要取了。而最大值是 ,我们可以看到这里面并没有 。所以错了。真正的最大值中包含的数并不一定是前面的最大值里包含的,有后效性。
那么标准答案仍然是个背包模型。设 表示前 个数,模 余 时所能取到的最大值。那么根据第 个元素取或者不取,转移方程为 ,并且一定要注意加上条件 。
首先我们看到如果这样定义状态就不会像刚刚那样产生某个元素取了之后导致后面元素出错的情况。这样就没有后效性了。
其次,如果 小于0的话并不意味着这个状态不合法。因为我们要求的是某个最小非负整数加上 等于 ,就相当于 ,所以如果减出来是负的,通过范围我们能知道 一定不会小于 ,所以我们加上 就合法了。
再次, 一定要加。在中途填充的时候不加这个条件是完全没有问题的,因为前面求出的 值已经能保证 同余 了。但是在最开始如果我们把 数组赋值为 ,会出现 的情况。而我们知道 模任何数都同余 ,也就是说这时存在 数组里面的值其实并不满足 模 同余 ,此时再直接像刚刚那样填充就会出现问题。而如果赋值成同余 的某个负数而去掉条件一定会导致答案里面少了一块,因为被负数给减掉了。
就是这个地方我找了半天问题…盯着dp数组想了半天为什么答案这么奇怪
然后只要想清楚原因,注意这几个细节就够了。代码其实很短。下面上代码。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int main()
{
int n, k; cin >> n >> k;
int a[110];
for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);
int dp[110][110] = {0};//注意0模任何数都同余0!
for (int i = 1; i <= n; ++i)
for (int j = 0; j < k; ++j)
{
dp[i][j] = dp[i - 1][j];
if ((dp[i - 1][(k + j - a[i] % k) % k] + a[i]) % k == j)//一定要加这个条件!
dp[i][j] = max(dp[i][j], dp[i - 1][(k + j - a[i] % k) % k] + a[i]);
}
cout << dp[n][0] << endl;
return 0;
}
14、计算字符串距离(2020.4.5)
http://noi.openjudge.cn/ch0206/2988/
打完学军中学比赛,因为水平太差被踢出12队力。
这题算是之前见过类似的吧。反正做出来了。
表示字符串1前 位和字符串2前 位相同时的最短步数,那么还是个背包模型。
下面那个不等于的情况就是由三种情况转移过来:一种是修改这个字符,剩下两种就是要么删字符串1,要么删字符串2。
注意边界情况是 。这题空间不能优化。
上代码。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int main()
{
int n; cin >> n;
while (n--)
{
char s1[2020], s2[2020];
cin >> s1 >> s2;
static int dp[2020][2020] = {0};
memset(dp, 0, sizeof(dp));
int len1 = strlen(s1), len2 = strlen(s2);
for (int i = 1; i <= len1; ++i) dp[i][0] = i;
for (int i = 1; i <= len2; ++i) dp[0][i] = i;
for (int i = 1; i <= len1; ++i)
for (int j = 1; j <= len2; ++j)
if (s1[i - 1] == s2[j - 1]) dp[i][j] = dp[i - 1][j - 1];
else
dp[i][j] = min(dp[i - 1][j - 1], min(dp[i - 1][j], dp[i][j - 1])) + 1;
cout << dp[len1][len2] << endl;
}
return 0;
}
15、判断整除(2020.4.5)
居然1A了。
和上面那个糖果挺像的,但是初始化好想一点。
设 为前 为能否取到模 为 的情况,设 为 对 的余数,那么我们就有
然后初始化考虑前 个数模 的情况,那么容易得到初始条件 。
下面上代码。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int main()
{
int n, k; cin >> n >> k;
static int a[10010];
for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);
static int dp[100010][110] = {0};
dp[0][0] = 1;
for (int i = 1; i <= n; ++i)
for (int j = 0; j < k; ++j)
{
int x = (a[i] - a[i] / k * k) % k;
dp[i][j] = dp[i - 1][(k + j - x) % k] | dp[i - 1][(k + j + x) % k];
}
if (dp[n][0]) cout << "YES" << endl;
else cout << "NO" << endl;
return 0;
}
16、最大上升子序列和(2020.4.5)
水。设 为以 结尾的最大上升子序列和,那么
下面上代码。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int main()
{
int n; cin >> n;
int a[1010];
for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);
int dp[1010] = {0};
int ans = 0;
for (int i = 1; i <= n; ++i)
{
int maxi = 0;
for (int j = 1; j < i; ++j)
if (a[j] < a[i]) maxi = max(maxi, dp[j]);
dp[i] = maxi + a[i];
ans = max(ans, dp[i]);
}
cout << ans << endl;
return 0;
}
*17、怪盗基德的滑翔翼(2020.4.5)
算是个最长上升子序列的变种。讲道理这题应该用 的算法才能过,可惜不会。
但是 的算法也能过。数据太水了。
上代码。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int INF = 0x3f3f3f3f;
int main()
{
int k; cin >> k;
while (k--)
{
int n; cin >> n;
int a[110];
for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);
int ans = 0;
for (int i = 1; i <= n; ++i)
{
int dp_1[110] = {0};
for (int j = i; j >= 1; --j)
{
int maxi = 0;
for (int k = i; k > j; --k)
if (a[k] > a[j]) maxi = max(maxi, dp_1[k]);
dp_1[j] = maxi + 1;
ans = max(ans, dp_1[j]);
}
int dp_2[110] = {0};
for (int j = i; j <= n; ++j)
{
int maxi = 0;
for (int k = i; k < j; ++k)
if (a[k] > a[j]) maxi = max(maxi, dp_2[k]);
dp_2[j] = maxi + 1;
ans = max(ans, dp_2[j]);
}
}
cout << ans << endl;
}
return 0;
}
然后去学了一下 的LIS方法。顺手把这个代码也改了改,居然1A了。
原来是24ms,现在是5ms。先上代码。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int INF = 0x3f3f3f3f;
int main()
{
int k; cin >> k;
while (k--)
{
int n; cin >> n;
int a[110];
for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);
int ans = 0;
for (int i = 1; i <= n; ++i)
{
int dp_1[110] = {INF};
int len_1 = 0;
for (int j = i; j >= 1; --j)
{
if (a[j] < dp_1[len_1]) dp_1[++len_1] = a[j];
else
{
int l = 1, r = len_1, pos = 1;
while (l <= r)
{
int mid = (l + r) >> 1;
if (dp_1[mid] <= a[j]) pos = mid, r = mid - 1;
else l = mid + 1;
}
dp_1[pos] = a[j];
}
}
ans = max(ans, len_1);
int dp_2[110] = {INF};
int len_2 = 0;
for (int j = i; j <= n; ++j)
{
if (a[j] < dp_2[len_2]) dp_2[++len_2] = a[j];
else
{
int l = 1, r = len_2, pos = 1;
while (l <= r)
{
int mid = (l + r) >> 1;
if (dp_2[mid] <= a[j]) pos = mid, r = mid - 1;
else l = mid + 1;
}
dp_2[pos] = a[j];
}
}
ans = max(ans, len_2);
}
cout << ans << endl;
}
return 0;
}
然后想着去写个模板,结果模板居然疯狂出错…
之前写的这个题倒是一点错都没有 我佛啦
这个 的算法我感觉不太像dp,应该是个贪心。
以LIS为例。
我们设 为长度为 的LIS的末尾元素的最小值。
为什么是最小值呢?因为存最小值更有利于下个元素的摆放。比如我之前存了 为最小值,那么下一个 当然就可以放到后一位;而如果之前那个 的位置还可以存 ,若我们存了 则 就放不到下个位置了。一个简单的贪心。
设处理到 时当前取到的LIS最大值为 ,同时这个 也是 数组的长度。
我们就会发现一个很显然的性质,就是 数组一定是递增的。
我们假设存在这样一个 ,使 。那么我们会发现长度为 的LIS中一定包含了一个长度为 的LIS,我们设这个LIS的末尾元素为 ,那么就有 。于是我们找到了一个长度为 的LIS,其末尾元素小于 ,矛盾。
然后对于新加进来的一个数 ,如果 就说明这个数可以组成一个新的LIS,我们就 ,然后把这个数扔到 里。这时前面存在 数组里的值就不要动了。因为这个值已经比这个递增数组里的最大值大了,所以它一定不可能成为前面某项的最小值。
第二种情况是 。这时我们在这个递增的 数组里找到第一个大于等于 的数,假设就是 。然后我们把 修改成 ,其余不变。
我们来看一下这么做的正确性。首先我们这个 数组是根据 中元素原本的顺序所填充的,所以 数组中所有存的值一定在 之前出现过。其次,因为我们是找的第一个大于等于 的值,所以 。然后 存的又是长度为 的末尾最小元素。由以上这几点,我们会发现如果把当前长度为 的LIS单独拿出来,然后在末尾添上 ,这时我们就构造了一个末尾元素比 更小的LIS。于是我们更新。
这里要注意一点,我们找的元素一定是第一个大于等于 的值。
说句实话,如果 和 相等,那么改或不改其实没有太大关系。但是写成大于等于是为了方便。
我们考虑 中存了连续的值 。然后我们添加 进来,如果我们找第一个大于 的值会变成 ,如果把 修改成 就不能保证答案的正确性了。所以不如干脆就多写个等于,多方便。
测试数据:
当所有操作完成时, 就为LIS的最大长度。
注意,这种方法没办法求出某条满足条件的LIS,只能求长度。
然后这种方法不加优化的话其实还是 的。而正是因为 数组的单调性,使我们在寻找元素的时候能够采用二分优化到 。
这种方法的初始化也非常蛋疼。因为要保证第一个值要能够被塞到数组里,我们 要赋一个比所有元素都小的数。一定要注意。
对于LIS,我们可以直接用算法库自带的lower_bound()。而如果求的是最长下降子序列,我们只能手写个二分查找。对于二分查找的注意点我可能有空会另外开一篇相关的文章。
最长下降子序列的求法大致一样,我们的 修改成长度为 的最大结尾值,然后我们每次找第一个小于等于该数的值并更新就可以了。这时初始化变成 。
模板↓
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 1e5 + 10;
const int INF = 0x3f3f3f3f;
int LIS(int *a, int n)
{
static int dp[MAXN] = {-INF};//初始化!
int len = 0;
for (int i = 1; i <= n; ++i)
{
if (dp[len] < a[i]) dp[++len] = a[i];
else
{
int pos = lower_bound(dp + 1, dp + 1 + len, a[i]) - dp;//lowerbound和查找范围!
dp[pos] = a[i];
}
}
return len;
}
int LDS(int *a, int n)
{
static int dp[MAXN] = {INF};//初始化!
int len = 0;
for (int i = 1; i <= n; ++i)
{
if (a[i] < dp[len]) dp[++len] = a[i];
else
{
int l = 1, r = len, pos = 1;//查找范围和pos的初始化!
while (l <= r) //这里pos是否初始化对结果无影响,但连续区间的二分会有影响
{ //所以干脆就全部都初始化了
int mid = (l + r) >> 1;
if (dp[mid] <= a[i]) pos = mid, r = mid - 1;
else l = mid + 1;
}
dp[pos] = a[i];
}
}
return len;
}
int main()
{
int n; cin >> n;
static int a[MAXN];
for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);
cout << LIS(a, n) << endl;
cout << LDS(a, n) << endl;
return 0;
}
18、买书(2020.4.6)
完全背包。
之前只开了一维,WA了。多算了。
上代码。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int main()
{
int n; cin >> n;
int dp[5][1010] = {0};
int a[5] = {0, 10, 20, 50, 100};
for (int i = 1; i <= 4; ++i) dp[i][0] = 1;
for (int i = 1; i <= 4; ++i)
for (int j = 1; j <= n; ++j)
{
dp[i][j] = dp[i - 1][j];
if (j - a[i] >= 0) dp[i][j] += dp[i][j - a[i]];
}
cout << dp[4][n] << endl;
return 0;
}
*19、宠物小精灵之收服(2020.4.6)
这题比较有意思,是个二维费用的背包。自己居然想了想就1A了。
普通的背包只有体积一个属性,而这里有精灵球数和体力值两个属性。那我们 加一维就好了。
表示前 个花费 个球 点血能收的最多精灵数,那么
最后 矩阵右下角的值就是答案。对于最少的耗血量我们只要对 维度从后往前扫一遍,找到第一个比答案小的值取它的血量+1就可以了。
的数据有点太大了,讲道理不一定过得了。但是标答也是这么做的。
然后这题空间刚开始粗略的算了一下感觉会炸,然后我们把第一维压掉就可以了。但其实交上去发现并没有炸…神奇
上代码。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int main()
{
int n, m, p; cin >> n >> m >> p;
int a[110], b[110];
for (int i = 1; i <= p; ++i) scanf("%d%d", &a[i], &b[i]);
static int dp[1010][510] = {0};//压缩空间
for (int i = 1; i <= p; ++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);
int ans = dp[n][m], HP;
for (int i = m; i >= 1; --i)//找最小血量耗费
if (dp[n][i] != ans)
{
HP = i + 1;
break;
}
cout << ans << ' ' << m - HP << endl;
return 0;
}
20、开餐馆(2020.4.6)
一个类似于LIS的东西。上代码。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int main()
{
int t; cin >> t;
while (t--)
{
int n, k; cin >> n >> k;
int m[110] = {0}, p[110] = {0};
for (int i = 1; i <= n; ++i) scanf("%d", &m[i]);
for (int i = 1; i <= n; ++i) scanf("%d", &p[i]);
int dp[110] = {0}, ans = 0;
for (int i = 1; i <= n; ++i)
{
int maxi = 0;
for (int j = 1; j < i; ++j)
if (m[i] - m[j] > k) maxi = max(maxi, dp[j]);
else break;//题目中保证m[]有序
dp[i] = maxi + p[i];
ans = max(ans, dp[i]);
}
cout << ans << endl;
}
return 0;
}
*21、数据包的调度机制(2020.4.7)
好题。想了一个多小时,离正确答案差一点。
错误方法:设 为从 到 的最小花费,那么
想法是找出来某一段,我们枚举断点,从前半段先出栈和后半段先出栈中找个最小值。
离正确答案真的只差一点了啊…反例是 ,用这个递推式算出来是 ,标答是 。
前半段先出栈没问题,后半段先出栈的话前半段是有出栈顺序的,不能随心所欲。
比如给的这个反例, 出栈的最小花费是 ,然后用我这个递推式算的时候会变成 ,但是如果 先出栈的话前面的出栈顺序就一定是 …然后GG。
为了解决这个毛病,标答是枚举 中最后一个出栈的元素。这样子在最后出栈元素之前和最后出栈元素之后的两段就是完全独立的,不会有顺序的影响。
本来这题可以成为第一道做出来的区间 的,差一步。唉。
下面上代码。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int main()
{
int t; cin >> t;
while (t--)
{
int n; scanf("%d", &n);
int d[110];
for (int i = 1; i <= n; ++i) scanf("%d", &d[i]);
int dp[110][110] = {0}, sum[110] = {0};
for (int i = 1; i <= n; ++i) sum[i] = sum[i - 1] + d[i];
for (int len = 2; len <= n; ++len)
for (int i = 1; i + len - 1 <= n; ++i)
{
int j = i + len - 1;
dp[i][j] = 1e8;
for (int k = i; k <= j; ++k)
dp[i][j] = min(dp[i][j], dp[i][k - 1] + dp[k + 1][j] + d[k] * (len - 1) + (sum[j] - sum[k]) * (k - i));
}
printf("%d\n", dp[1][n]);
}
return 0;
}
顺便测试了一下,输入输出如果全部换成cin会慢大概0.1s。这是输入规模大概为1e6的情况。
另外这题的规模也不太科学。 的算法就过了,要跑 次。不懂什么情况。