1. 项目概述
最近在刷USACO竞赛题时遇到了P5424 [USACO19OPEN] Snakes G这道动态规划经典题,题目看似简单但暗藏玄机。这道题考察的是如何用最少的操作次数将一组蛇的长度调整为相同大小,同时还要考虑操作次数的限制。作为USACO金组题目,它很好地检验了选手对动态规划状态设计和转移的理解能力。
我在实际解题过程中踩了不少坑,特别是在状态转移方程的优化上走了不少弯路。经过反复调试和优化,最终找到了一个时间复杂度为O(N^3)的解法,比暴力解法效率高出不少。下面就来详细拆解这道题的解题思路和实现细节。
2. 题目分析与建模
2.1 题目理解
题目大意是:我们有N组蛇,每组有不同数量的蛇。我们可以进行若干次操作,每次操作可以选择一个区间,将这个区间内所有组的蛇都调整为相同的数量。每次操作的代价是区间内最大值与各组蛇数量的差的平方和。我们需要在最多进行K次操作的情况下,找到最小的总代价。
举个例子,假设我们有蛇组数量为[3,6,2,1,4],K=2。最优解是第一次操作将后三个组调整为2(代价为(2-2)^2 + (2-1)^2 + (2-4)^2 = 5),第二次操作将前两个组调整为6(代价为(6-3)^2 + (6-6)^2 = 9),总代价为14。
2.2 数学模型建立
这个问题可以抽象为:给定一个长度为N的数组A和整数K,将数组分成最多K+1段(因为初始状态也算一次"操作"),每段内的元素都变为该段的最大值,求所有段的调整代价之和的最小值。
定义代价函数cost(l,r)为将区间[l,r]内的元素都变为max(A[l..r])的代价:
cost(l,r) = Σ(max(A[l..r]) - A[i])^2,其中i从l到r
我们的目标是找到一种分段方式,使得总代价最小。
3. 动态规划解法设计
3.1 基本DP状态设计
定义dp[k][i]表示前i个元素分成k段的最小总代价。则状态转移方程为:
dp[k][i] = min(dp[k-1][j] + cost(j+1,i)),其中j从0到i-1
初始条件:
dp[0][i] = ∞(不可能)
dp[k][0] = 0(前0个元素代价为0)
最终答案为min(dp[K][N], dp[K-1][N], ..., dp[0][N])
3.2 预处理优化
直接计算cost(l,r)的时间复杂度是O(N),如果每次都实时计算会导致总复杂度达到O(K*N^3),对于N=400来说不可接受。我们需要预处理cost数组。
预处理两个数组:
- max_range[l][r]:区间[l,r]的最大值
- cost[l][r]:将区间[l,r]调整为max_range[l][r]的代价
预处理max_range可以使用动态规划:
max_range[l][r] = max(max_range[l][r-1], A[r])
预处理cost可以利用前缀和优化:
先预处理平方前缀和、线性前缀和,然后:
cost[l][r] = max^2*(r-l+1) - 2maxsum + sum_sq
其中sum是A[l..r]的和,sum_sq是A[l..r]的平方和
这样预处理的时间复杂度是O(N^2),之后查询cost[l][r]就是O(1)了。
3.3 实现细节
cpp复制#include <bits/stdc++.h>
using namespace std;
const int INF = 0x3f3f3f3f;
int main() {
int N, K;
cin >> N >> K;
vector<int> A(N+1);
for(int i=1; i<=N; i++) cin >> A[i];
// 预处理前缀和
vector<int> prefix(N+1), prefix_sq(N+1);
for(int i=1; i<=N; i++) {
prefix[i] = prefix[i-1] + A[i];
prefix_sq[i] = prefix_sq[i-1] + A[i]*A[i];
}
// 预处理max_range和cost
vector<vector<int>> max_range(N+1, vector<int>(N+1));
vector<vector<int>> cost(N+1, vector<int>(N+1));
for(int l=1; l<=N; l++) {
max_range[l][l] = A[l];
for(int r=l+1; r<=N; r++) {
max_range[l][r] = max(max_range[l][r-1], A[r]);
int m = max_range[l][r];
int sum = prefix[r] - prefix[l-1];
int sum_sq = prefix_sq[r] - prefix_sq[l-1];
cost[l][r] = m*m*(r-l+1) - 2*m*sum + sum_sq;
}
}
// DP初始化
vector<vector<int>> dp(K+1, vector<int>(N+1, INF));
dp[0][0] = 0;
// DP转移
for(int k=1; k<=K+1; k++) {
for(int i=1; i<=N; i++) {
for(int j=0; j<i; j++) {
if(dp[k-1][j] != INF) {
dp[k][i] = min(dp[k][i], dp[k-1][j] + cost[j+1][i]);
}
}
}
}
// 找答案
int ans = INF;
for(int k=0; k<=K+1; k++) {
ans = min(ans, dp[k][N]);
}
cout << ans << endl;
return 0;
}
4. 优化与改进
4.1 空间优化
注意到dp[k][i]只依赖于dp[k-1][j],因此可以将空间复杂度从O(K*N)优化到O(N):
cpp复制vector<vector<int>> dp(2, vector<int>(N+1, INF));
int now = 0, pre = 1;
dp[now][0] = 0;
for(int k=1; k<=K+1; k++) {
swap(now, pre);
fill(dp[now].begin(), dp[now].end(), INF);
for(int i=1; i<=N; i++) {
for(int j=0; j<i; j++) {
if(dp[pre][j] != INF) {
dp[now][i] = min(dp[now][i], dp[pre][j] + cost[j+1][i]);
}
}
}
}
4.2 四边形不等式优化
观察cost函数满足四边形不等式性质,可以应用Knuth优化将时间复杂度降到O(K*N^2)。定义opt[k][i]为使得dp[k][i]最小的j,则有opt[k][i] <= opt[k][i+1]。
优化后的转移:
cpp复制vector<vector<int>> dp(K+2, vector<int>(N+1, INF));
vector<vector<int>> opt(K+2, vector<int>(N+2));
dp[0][0] = 0;
for(int k=1; k<=K+1; k++) {
opt[k][N+1] = N-1;
for(int i=N; i>=1; i--) {
int min_j = (k>1) ? opt[k-1][i] : 0;
int max_j = opt[k][i+1];
for(int j=min_j; j<=max_j && j<i; j++) {
if(dp[k-1][j] + cost[j+1][i] < dp[k][i]) {
dp[k][i] = dp[k-1][j] + cost[j+1][i];
opt[k][i] = j;
}
}
}
}
5. 常见问题与调试技巧
5.1 边界条件处理
- 初始状态dp[0][0]必须设为0,其他dp[0][i]设为无穷大
- K次操作实际上可以分成K+1段(包括初始状态)
- 最终答案要取所有dp[k][N]中的最小值,k从0到K+1
5.2 数值溢出问题
代价计算涉及平方操作,对于较大的A[i](题目中A[i]≤1e4)可能导致int溢出。可以:
- 使用long long类型存储代价
- 在计算过程中及时取模(如果题目要求)
5.3 调试技巧
- 先验证预处理是否正确,打印小规模的max_range和cost数组
- 对于N=3,K=1这样的简单case手动计算验证
- 使用assert检查中间结果是否合理
6. 复杂度分析
-
预处理阶段:
- max_range和cost预处理:O(N^2)
- 前缀和预处理:O(N)
-
基本DP:
- 状态数:O(K*N)
- 转移:O(N)
- 总复杂度:O(K*N^2)
-
四边形不等式优化后:
- 转移:O(1)均摊
- 总复杂度:O(K*N^2)
对于USACO金组题目,N=400,K=400时,O(K*N^2)=64,000,000,在时间限制内可以通过。
7. 个人解题心得
这道题的关键在于如何设计状态表示和优化状态转移。我最初尝试用dp[i][j][k]表示前i个元素,当前段最大值为j,已经用了k次操作的最小代价,但这样状态数太大无法处理。
后来意识到可以不必记录当前段的最大值,而是每次操作都强制将区间设为该区间的最大值。这种"分段+统一调整"的思路是这类问题的常见套路。
另一个教训是预处理的重要性。直接计算cost(l,r)会导致超时,通过前缀和优化将cost查询降到O(1)是必要的优化。这也提醒我在做DP题时要多思考哪些信息可以预处理。
最后,对于这种分段型DP,四边形不等式优化是一个强有力的工具,可以显著降低时间复杂度。虽然比赛时不一定会想到,但作为练习值得掌握。