动态规划练习 普及-
附题单
P2663 越越的组队
思路
最后答案需要求最大值思考一下也就是求一个01背包,容量是总人数折半,价值也就是每个人综合能力,体积就是也是他们的能力,因为需要求不超过这个折半值的最大值。具体01背包可以参考 P1048 [NOIP 2005 普及组] 采药 ,这里不过多赘述。
代码
#include <bits/stdc++.h>
using namespace std;
//const int one's birthday = 59;
const int MAXN = 100;
const int MAXM = 10000;
int main() {
// freopen("Robin.in","r",stdin);
// freopen("Robin.out","w",stdout);
int n, sum = 0;
cin >> n;
int a[MAXN];
for (int i = 0; i < n; ++i) {
cin >> a[i];
sum += a[i];
}
int h = sum / 2, m = n / 2;
bool dp[MAXN / 2 + 1][MAXM / 2 + 1];
memset(dp, 0, sizeof(dp));
dp[0][0] = true;
for (int i = 0; i < n; ++i)
for (int j = m; j >= 0; --j)
for (int k = h; k >= 0; --k)
if (dp[j][k] && j + 1 <= m && k + a[i] <= h)
dp[j + 1][k + a[i]] = true;
for (int k = h; k >= 0; --k)
if (dp[m][k]) {
cout << k << endl;
break;
}
return 0;
}
P2722 [USACO3.1] 总分 Score Inflation
分析
题目类似于 P1616 疯狂的采药 ,但是输入的不一样,需要交换价值和体积。
具体思路参考 P1616 ,是一个完全背包。
代码
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e4 + 5, M = 1e7 + 5;
int n, m, w[N], v[N], f[M];
signed main() {
scanf("%lld%lld", &m, &n);
for (int i = 1; i <= n; i++)
scanf("%lld%lld", &v[i], &w[i]);
for (int i = 1; i <= n; i++)
for (int j = w[i]; j <= m; j++)
f[j] = max(f[j], f[j - w[i]] + v[i]);
printf("%lld", f[m]);
return 0;
}
P2800 又上锁妖塔
分析
这道题的状态转移方程很容易就可以得到:
$ dp_i = min (dp_{i - 1}, dp_{i - 2}, dp_{i - 3}) + arr_i $
很明显,我们需要从前 1 或 2 个楼上来。
每用一次可以让小A向上跳一层或两层
要不然,小A只能爬楼了,那就是往前 3 个楼上来。也就是:
每次跳跃后小 A 都将用完力气,必须爬过至少一层才能再次跳跃
然后证明当前是需要爬楼的层数,也就是需要加上当前层的高度 $ h_i $。
代码
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const ll MAXN = 1e6 + 10;
const ll MAXX = 1e9;
ll dp[MAXN], arr[MAXN], n, i;
main() {
cin >> n;
for (i = 1; i <= n; i++) {
cin >> arr[i];
dp[i] = MAXX;
}
dp[n + 1] = MAXX;
for (i = 1; i <= n + 1; i++)
dp[i] = min({dp[i], dp[i - 1], ((i > 1) ? min(dp[i], dp[i - 2]) : dp[i]), ((i > 2) ? min(dp[i], dp[i - 3]) : dp[i])}) + arr[i];
cout << dp[n + 1];
return 0;
}
P2837 [USACO08FEB] Dining Cows B
分析
让我们来解决一下这种题如何解决。
首先,题目是需要分成两种队伍,那么我们可以试着找到这个分割点的位置。
然后我们再看看每个分割点需要修改多少个卡片编号取最小的一个,思路似乎有点暴力而且 \(O(n^2)\) 的时间复杂度下似乎也不太够。
既然如此,我们就有前缀和优化这个计算需要修改数这个部分的速度,这样理论优化到 \(O(n)\) 了。
下面我们就来看具体实现代码
代码
#include <bits/stdc++.h>
using namespace std;
//typedef long long ll;
const int MAXN = 3e4 + 10;
int main() {
// freopen("robin.in", "r", stdin);
// freopen("robin.out","w",stdout);
int n, d[MAXN];
scanf("%d", &n);
for (int i = 0; i < n; ++i)
scanf("%d", d + i);
int p1[MAXN] = {0}, p2[MAXN] = {0};
for (int i = 1; i <= n; ++i) {//这个部分既是前缀和优化,可以自己理解一下
p1[i] = p1[i - 1] + (d[i - 1] == 1);
p2[i] = p2[i - 1] + (d[i - 1] == 2);
}
int ans = n;
for (int k = 0; k <= n; ++k) {
int res = (k - p1[k]) + ((n - k) - (p2[n] - p2[k]));
ans = min(ans, res);
// printf("%d\n", ans);
}
printf("%d", ans);
return 0;
}
P2871 [USACO07DEC] Charm Bracelet S
分析
具体思路类似于 P1048 [NOIP 2005 普及组] 采药
重点在于需要把二维数组压缩成一维数组,否则会 RE ,因为 12880 的二次方就已经上亿卡常了,$ O(n^2) $。
可以参考这个代码:(70分卡常)
但是我们需要做的就是压缩,试着减去一个维度,虽然省去了维度但是方法还是类似的。
直接在第二重循环模拟背包容量,就非常简单的解决问题。
Tips:模拟背包时,应当从后往前模拟,要不然就会覆盖状态导致出错经过。试验得出的结果
代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1e7 + 10;
int main() {
// freopen("robin.in", "r", stdin);
// freopen("robin.out", "w", stdout);
int t, n, t1[3410], v1[3410], dp[MAXN];
scanf("%d%d", &n, &t);
for (int i = 1; i <= n; i++)
scanf("%d%d", t1 + i, v1 + i);
memset(dp, 0, sizeof(dp));
for (int i = 1; i <= n; i++) {
for (int j = t; j >= t1[i]; j--) {
dp[j] = max(dp[j], dp[j - t1[i]] + v1[i]);
}
}
cout << dp[t];
return 0;
}
P2925 [USACO08DEC] Hay For Sale S
分析
这个问题是一个极简单的01背包,物品的价值和容量都是同一个值。下面呈现一个bitset解决的思路:
使用bitset来记录可达的体积状态。每个位表示对应体积是否可达(可达或者不可达)。
初始化b[0] = 1,表示体积0是初始可达状态。
对每捆体积为v的稻草,通过b |= b << v这个转移方程快速更新状态。这一步将原可达体积加上当前稻草体积v,生成新的可达体积集合,并与原集合取或合并。
例如,若原状态包含体积x,则新状态会包含x + v(左移v位实现)。
处理完所有稻草后,从最大容量C开始逆序查找第一个为1的位,找到的结果即能装下的最大体积。
代码
#include <bits/stdc++.h>
using namespace std;
const int Robin = 1e5 + 10;
int h, c, v, ans;
bitset <Robin> b;
int main() {
b[0] = 1;
scanf("%d%d", &c, &h);
for (int i = 1 ; i <= h ; i++) {
scanf("%d", &v);
b |= b << v;
}
for (ans = c ; ans >= 0 ; ans--)
if (b[ans])
break;
printf("%d", ans);
return 0;
}
P3009 [USACO11JAN] Profits S
分析
解决这道题之前,我们先看看动态规划是怎么做这道题的。
主要思路是记录以当前这个数字结尾的最大值,于是得出:
$ dp_i = max( dp_{i-1} + arr_i,arr_i) $
最后取一下最大值就可以了。
但是在这个过程中我们发现可以省略掉读入和状态转移部分,因为可以直接用一个变量代替。
由此,得出一下贪心代码。
代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
ll n, a, b, i, ans = LLONG_MIN;
int main() {
scanf("%lld", &n);
for (i = 1; i <= n; i++) {
scanf("%lld", &a);
b = (i == 1) ? a : max(a, a + b);
ans = max(ans, b);
}
printf("%lld", ans);
return 0;
}
P5146 最大差值
分析
这道题需要找到最大的 \(a_j - a_i (i<j)\)。
我们需要先把思路转换一下,既然需要找到最大的差值,如果 \(a_j(j)\) 已经固定了,那么就是要找到最小的 \(a_i\) 。
所以我们可以在每次读入一个数的时候就找到前面最小的值,这个过程需要在最开始进行,所以不会干扰复杂度。
我们可以带入题目中样例试试思路是否正确:(maxx代表最大差值,minn代表最小\(a_i\))
minn = 1,maxx 初始化为 \(-9223372036854775808\)(LLONG_MIN)。
-
第2个元素 3:差值为 2,更新
maxx= 2。 -
第3个元素 4:差值为 3,更新
maxx= 3。 -
第4个元素 6:差值为 5,更新
maxx= 5。 -
第5个元素 7:差值为 6,更新
maxx= 6。 -
第6个元素 9:差值为 8,更新
maxx= 8。 -
第7个元素 10:差值为 9,更新
maxx= 9。
后续元素 1、2、9 的差值分别为 0、1、8,均不更新 maxx。
最后即是输出的样例9。
代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int main() {
ll n;
scanf("%lld", &n);
ll minn;
ll maxx = LLONG_MIN;
scanf("%lld",&minn);
for (ll i = 2, a; i <= n; ++i) {
scanf("%lld", &a);
maxx = max(maxx, a - minn);
minn = min(minn, a);
}
printf("%lld\n", maxx);
return 0;
}
P7158 「dWoi R1」Password of Shady
分析
首先简化一下题面:求没有前导 \(0\) 且有偶数个 \(k\) 的 \(n\) 位数的数量。
动态规划核心思想就是问什么设什么。
所以设 \(dp_{0,i}\) 是符合题意要求的第 \(i\) 位数
同理得 \(dp_{1,i}\) 是不符合题意的第 \(i\) 位数
得状态转移方程:
$ dp_{0,i} = (dp_{0,i-1} \times 9 + dp_{1,i-1}) $
$ dp_{1,i} = (dp_{1,i-1} \times 9 + dp_{0,i-1}) $
最后记得取模并开 long long,否则只能拿到 substack 3 了。
代码
#include <bits/stdc++.h>
using namespace std;
const int mod = 998244353;
const int MAXN = 2e5;
long long f[3][MAXN], n, t, k;
void oper() {
f[1][1] = 8, f[0][1] = 1;
for (int i = 2; i <= MAXN; i++)
f[0][i] = (f[0][i - 1] * 9 + f[1][i - 1]) % mod, f[1][i] = (f[1][i - 1] * 9 + f[0][i - 1]) % mod;
}
int main() {
oper();
scanf("%lld", &t);
while (t--) {
scanf("%lld%lld", &n, &k);
if (n == 1)
printf("9\n");
else
printf("%d\n", f[1][n]);
}
return 0;
}
P7774 [COCI 2009/2010 #2] KUTEVI
分析
-
最大公约数(GCD)计算:初始角度的GCD决定了可以生成的角度范围。所有可能的线性组合的模360的结果必须为该GCD与360的GCD的倍数。
-
模运算处理:将目标角度对360取模,检查其是否为初始角度GCD与360的GCD的倍数。
代码
#include <bits/stdc++.h>
using namespace std;
int gcd(int a, int b) {
return b == 0 ? a : gcd(b, a % b);
}
int main() {
int n, m;
cin >> n >> m;
int a[n];
for (int i = 0; i < n; ++i) {
cin >> a[i];
}
int g = a[0];
for (int i = 1; i < n; ++i) {
g = gcd(g, a[i]);
}
int d = gcd(g, 360);
while (m--) {
int b;
cin >> b;
int rem = b % 360;
if (rem % d == 0) {
cout << "YES\n";
} else {
cout << "NO\n";
}
}
return 0;
}
B4089 [CSP-X2020 山东] 勇敢的津津
分析
-
贪心策略:每次跳跃时,选择当前能够到达的最远的石墩。这样可以尽量减少跳跃次数。
-
二分查找:使用
upper_bound函数找到当前能到达的最远石墩的位置。
标程
#include <bits/stdc++.h>
using namespace std;
int main() {
int l, n, m;
cin >> l >> n >> m;
int d[505];
for (int i = 0; i < n; ++i) {
cin >> d[i];
}
int pos = 0;
int cnt = 0;
while (true) {
if (pos + m >= l) {
cnt++;
break;
}
int reach = pos + m;
int *it = upper_bound(d, d + n, reach);
if (it == d) {
break;
}
int idx = it - d - 1;
if (d[idx] <= pos) {
break;
}
pos = d[idx];
cnt++;
}
cout << cnt << endl;
return 0;
}
总结
这一周,我的主要练习方向是 模板 类型题目,主要类型有:动态规划
通过这些题目都极大的提升了我的能力(这是次要的),重要的是,模板题的适用范围很广,是非常有用的。
目前总共写了 13 篇题解(实际解决题目可能大于这个数字),换算下来,即周一到周五每天做 1 ~ 2道题,周末做2 ~ 3道题。
同时还完成了 数论 代数 的第三章节的习题。
评价一下:这个进度(效率)如何?
A.非常好
B.A
下一周的目标&任务:
数学:完成 数论 代数 的第四章习题
信奥:完成 GESP真题动态规划 习题
(由于题目难度跨度较大,所以目标不要求太多,在原来一周基础上减少 2 题即可)
以上就是第五周练习总结。

浙公网安备 33010602011771号