GESP C++ 七级 DP 精选 4 题题解
本文档精选 8 道 GESP 七级。每道题包含:题目大意、状态定义、转移方程、边界条件、完整 AC 代码。
第 1 题:[GESP202506 七级] 调味平衡
题目大意
有 n 种调料,每种调料可以放在 A 组或 B 组(每组的重量累加)。求使两组总重量的差值 |x - y| 最小的方案,输出这个最小差值。
思路分析
01 背包变形 —— 差值 DP(可达性 DP)
- 核心思想:把"差值"作为状态。
- 状态定义:由于差值可能为负,加偏移量 OFFSET。
dp[j] = true表示差值为j - OFFSET可达。
- 转移:
- 放到 A 组:差值
+ w[i] - 放到 B 组:差值
- w[i]
- 放到 A 组:差值
- 答案:从 OFFSET 往两边找最近的 true。
AC 代码
#include <bits/stdc++.h>
using namespace std;
const int OFFSET = 50000;
const int MAXS = OFFSET * 2 + 10;
int n, w[105];
bool dp[MAXS];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
int sum = 0;
for (int i = 1; i <= n; i++) {
cin >> w[i];
sum += w[i];
}
// 初始差值为 0
dp[OFFSET] = true;
for (int i = 1; i <= n; i++) {
bool tmp[MAXS] = {false};
for (int j = 0; j < MAXS; j++) {
if (dp[j]) {
// 放到 A 组:差值 + w[i]
if (j + w[i] < MAXS) {
tmp[j + w[i]] = true;
}
// 放到 B 组:差值 - w[i]
if (j - w[i] >= 0) {
tmp[j - w[i]] = true;
}
}
}
memcpy(dp, tmp, sizeof(dp));
}
// 从差值 0 往两边找最近可达状态
for (int d = 0; d <= sum; d++) {
if (dp[OFFSET + d] || dp[OFFSET - d]) {
cout << d << '\n';
break;
}
}
return 0;
}
第 2 题:[GESP202509 七级] 金币收集
题目大意
在二维平面上有 n 个金币,每个金币有坐标 (x[i], y[i])。从 (0,0) 出发,每次只能向右或向上移动,路径上遇到的金币可以收集。问最多能收集多少金币?
思路分析
二维偏序 → LIS 转化 + 二分优化
- 排序:按 x 坐标升序排序,x 相同时按 y 坐标升序排序。
- 转化:排序后 x 已递增,问题转化为求 y 坐标序列的最长不下降子序列。
- 二分优化 LIS:维护 tails 数组,tails[i] 表示长度为 i 的子序列最小末尾元素,用 lower_bound 二分,复杂度 O(n log n)。
AC 代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 200005;
int n;
struct Point {
int x, y;
bool operator<(const Point& other) const {
if (x != other.x) {
return x < other.x;
}
return y < other.y;
}
} p[MAXN];
int tails[MAXN];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> p[i].x >> p[i].y;
}
sort(p + 1, p + 1 + n);
// 提取 y 数组,求 LIS
int len = 0;
for (int i = 1; i <= n; i++) {
int v = p[i].y;
int pos = lower_bound(tails + 1, tails + 1 + len, v) - tails;
tails[pos] = v;
len = max(len, pos);
}
cout << len << '\n';
return 0;
}
第 3 题:[GESP202412 七级] 燃烧
题目大意
给定一棵 n 个节点的树,每条边有长度。在某个节点点火,火会沿着边蔓延,燃烧一条边所需时间等于边长。求从哪个节点点火使整棵树全部被点燃的时间最短,输出这个最短时间。
思路分析
树形 DP + 换根法(两遍 DFS)
-
第一遍 DFS(子树信息):
down[u]:从 u 向下点燃整棵子树的最长时间。- 同时记录每个节点子树中的最大值、次大值,以及最大值来自哪个子节点。
-
第二遍 DFS(换根):
up[u]:从 u 通过父节点 p 向上点燃其他部分的最长时间。- 换根时:若 u 是 p 的最大子节点,则取次大值;否则取最大值。
up[v] = max(up[u], best_from_siblings) + w(u,v)
-
答案:
min over all u of max(down[u], up[u])
AC 代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 200005;
int n;
struct Edge {
int to;
long long w;
};
vector<Edge> g[MAXN];
long long down[MAXN], up[MAXN];
int fmax_child[MAXN];
long long fmax[MAXN], smax[MAXN];
// 第一遍:计算 down、最大值、次大值
void dfs1(int u, int fa) {
fmax[u] = 0;
smax[u] = 0;
fmax_child[u] = -1;
for (auto& e : g[u]) {
int v = e.to;
if (v == fa) {
continue;
}
dfs1(v, u);
long long val = down[v] + e.w;
if (val > fmax[u]) {
smax[u] = fmax[u];
fmax[u] = val;
fmax_child[u] = v;
} else if (val > smax[u]) {
smax[u] = val;
}
}
down[u] = fmax[u];
}
// 第二遍:换根计算 up
void dfs2(int u, int fa) {
for (auto& e : g[u]) {
int v = e.to;
if (v == fa) {
continue;
}
// v 通过 u 能到达的最长路径
long long best = up[u];
if (v == fmax_child[u]) {
best = max(best, smax[u]);
} else {
best = max(best, fmax[u]);
}
up[v] = best + e.w;
dfs2(v, u);
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 1; i < n; i++) {
int a, b;
long long c;
cin >> a >> b >> c;
g[a].push_back({b, c});
g[b].push_back({a, c});
}
dfs1(1, -1);
up[1] = 0;
dfs2(1, -1);
long long ans = 1e18;
for (int i = 1; i <= n; i++) {
ans = min(ans, max(down[i], up[i]));
}
cout << ans << '\n';
return 0;
}
第 4 题:[GESP202606 七级] 消消乐
题目大意
给定一个字符串 s,每次操作可以选择一段相邻的、字符完全相同的连续子串将其消除,消除后两侧拼接。消除长度为 k 的子串得分为 k²。求消除整个字符串的最大得分。
思路分析
区间 DP(消除模型)
- 状态定义:
dp[l][r]表示消除 s[l..r] 能获得的最大得分。 - 转移方程:
- 直接切分:枚举分割点 k,
dp[l][r] = max(dp[l][r], dp[l][k] + dp[k+1][r]) - 首尾合并:若
s[l] == s[m],可以在消除中间后合并,dp[l][r] = max(dp[l][r], dp[l+1][m-1] + dp[m][r])
- 直接切分:枚举分割点 k,
- 边界:
dp[i][i] = 1(单个字符得 1 分)。 - 计算顺序:先枚举区间长度 len = 1 → n,再枚举左端点 l。
AC 代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 205;
int n;
string s;
int dp[MAXN][MAXN];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> s;
n = s.size();
s = " " + s;
// 初始化:单个字符得 1 分
for (int i = 1; i <= n; i++) {
dp[i][i] = 1;
}
// 枚举区间长度
for (int len = 2; len <= n; len++) {
for (int l = 1; l + len - 1 <= n; l++) {
int r = l + len - 1;
// 最坏情况:一个个消除
dp[l][r] = len;
// 情况 1:切分两半
for (int k = l; k < r; k++) {
dp[l][r] = min(dp[l][r], dp[l][k] + dp[k + 1][r]);
}
// 情况 2:首尾字符相同,可以合并消除
if (s[l] == s[r]) {
dp[l][r] = min(dp[l][r], dp[l + 1][r - 1] + 1);
}
// 情况 3:中间有字符与左端相同,可合并
for (int k = l + 1; k <= r; k++) {
if (s[k] == s[l]) {
dp[l][r] = min(dp[l][r],
dp[l + 1][k - 1] + dp[k][r]);
}
}
}
}
cout << dp[1][n] << '\n';
return 0;
}
三、八大 DP 模型速查表
| 模型 | 核心状态 | 典型转移 | 代表题 |
|---|---|---|---|
| 差值背包 | dp[diff] |
可达性转移 | 调味平衡 |
| LIS 优化 | tails[] |
二分查找 O(n log n) | 金币收集 |
| 换根树 DP | down/up |
两遍 DFS | 燃烧 |
| 区间 DP | dp[l][r] |
枚举分割点 + 合并 | 消消乐 |

浙公网安备 33010602011771号