动态规划练习 普及-

附题单

链接

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

分析

  1. 最大公约数(GCD)计算:初始角度的GCD决定了可以生成的角度范围。所有可能的线性组合的模360的结果必须为该GCD与360的GCD的倍数。

  2. 模运算处理:将目标角度对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 山东] 勇敢的津津

分析

  1. 贪心策略:每次跳跃时,选择当前能够到达的最远的石墩。这样可以尽量减少跳跃次数。

  2. 二分查找:使用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 题即可)

以上就是第五周练习总结。

posted @ 2025-03-31 21:39  Easoncalm  阅读(78)  评论(0)    收藏  举报