题目描述
北大信息学院的同学小明毕业之后打算创业开餐馆.
现在共有 nnn 个地点可供选择。小明打算从中选择合适的位置开设一些餐馆。这 nnn 个地点排列在同一条直线上。我们用一个整数序列 m1m_1m1, m2m_2m2, ... mnm_nmn 来表示他们的相对位置。
由于地段关系,开餐馆的利润会有所不同。我们用 pip_ipi 表示在 mim_imi 处开餐馆的利润。为了避免自己的餐馆的内部竞争,餐馆之间的距离必须大于 kkk 。请你帮助小明选择一个总利润最大的方案。
输入格式
标准的输入包含若干组测试数据。输入第 111 行是整数 TTT (1<=T<=1000)(1 <= T <= 1000)(1<=T<=1000) ,表明有 TTT 组测试数据。紧接着有 TTT 组连续的测试。每组测试数据有 333 行:
第 111 行,输入地点总数 n(n<100)n (n < 100)n(n<100) , 距离限制 k(0≤k≤1000)k(0 ≤ k ≤ 1000)k(0≤k≤1000)。
第 222 行,输入 nnn 个地点的位置 m1m_1m1 , m2m_2m2 , ... mnm_nmn (0≤mi≤1∗1060 ≤ m_i ≤ 1*10^60≤mi≤1∗106), 保证 mmm 数组为整数且升序。
第 333 行,输入 nnn 个地点的餐馆利润 p1p_1p1 , p2p_2p2, ... pnp_npn (000 <<< pip_ipi <<< 100010001000),保证 ppp 数组为整数
输出格式
输出对于每组测试数据可能的最大利润
样例
输入
2
3 11
1 2 15
10 2 30
3 16
1 2 15
10 2 30
输出
40
30
解题思路
问题分析
问题本质
这是一个带约束条件的最大利润选择问题:
- 有 nnn 个有序地点(按位置升序排列)
- 每个地点有对应利润
- 限制条件:选择的任意两个餐馆之间距离必须大于 kkk
- 目标:最大化总利润
约束理解
距离限制意味着:如果选择了位置 m[i] 的地点,那么下一个可选地点的位置 m[j] 必须满足 m[j] - m[i] > k(严格大于)。
算法设计思路
1. 动态规划状态定义
定义状态:
dp[i] = 考虑前 i 个地点(不一定选择第 i 个)时能获得的最大利润
为什么这样定义?
- 我们需要记录"到某个位置为止"的最优解
- 状态转移时需要考虑是否选择当前地点
2. 状态转移分析
对于第 iii 个地点,有两种选择:
情况一:不选择第 iii 个地点
利润 = dp[i-1] // 直接继承前 i-1 个地点的最优解
情况二:选择第 iii 个地点
如果选择第 iii 个地点,我们需要找到前面最后一个可以与之共存的地点:
- 设这个地点的索引为
pre - 需要满足:
m[i] - m[pre] > k - 那么总利润 =
p[i] + dp[pre]
为什么是 dp[pre] 而不是 dp[pre] 的某种变形?
- 因为
dp[pre]已经考虑了前pre个地点的最优选择 - 选择
pre之后的地点都会与i冲突(距离 ≤k≤ k≤k) - 所以选择
i时,只能接在pre代表的方案后面
3. 状态转移方程
综合两种情况:
dp[i] = max(dp[i-1], p[i] + dp[pre])
其中:
dp[i-1]:不选第 iii 个地点p[i] + dp[pre]:选第 iii 个地点,接在最后一个兼容地点后面pre是满足m[i] - m[j] > k的最大j(1≤j<i1 ≤ j < i1≤j<i)
4. preprepre 的确定方法
为什么需要最大的 jjj?
因为:
- 位置有序性:数组已按位置升序排列
- 单调性 :如果
j满足条件,那么所有小于j的索引也满足条件 - 最优性 :
dp[j]随着 jjj 增加(非递减),所以最大的 jjj 对应的dp[j]可能更大
查找策略:
- 从
i-1开始向前遍历 - 找到第一个(从后往前看的第一个,即最大的 jjj)满足
m[i] - m[j] > k的 j - 时间复杂度:O(n)O(n)O(n),可以通过二分查找优化到 O(logn)O(log n)O(logn)
5. 边界条件
dp[0] = 0:没有地点时利润为 000pre可能为 000:表示前面没有兼容地点,此时dp[pre] = 0
算法执行流程
1. 初始化 dp[0] = 0
2. 对于 i = 1 到 n:
a. 初始化 pre = 0
b. 从 j = i-1 递减到 1:
如果 m[i] - m[j] > k:
pre = j
退出循环
c. 计算 dp[i] = max(dp[i-1], p[i] + dp[pre])
3. 输出 dp[n]
时间复杂度分析
- 每处理一个地点 iii,需要 O(i)O(i)O(i) 时间查找 preprepre
- 总时间复杂度:O(1+2+...+n)=O(n2)O(1 + 2 + ... + n) = O(n²)O(1+2+...+n)=O(n2)
- n≤100n ≤ 100n≤100,所以 O(10000)O(10000)O(10000) 完全可行
- 可优化为 O(nlogn)O(n log n)O(nlogn) 使用二分查找
空间复杂度分析
- O(n)O(n)O(n) 存储地点信息和 DPDPDP 数组
- 空间效率很高
正确性证明
最优子结构
问题的最优解包含子问题的最优解:
- 如果选择第 iii 个地点,那么前
pre个地点的选择必须是最优的 - 如果不选择第 iii 个地点,那么前 i−1i-1i−1 个地点的选择必须是最优的
无后效性
dp[i] 只依赖于 dp[i-1] 和 dp[pre],其中 pre < i,满足动态规划的无后效性原则。
完备性
状态转移覆盖了所有可能的选择情况:
- 不选当前地点
- 选当前地点,并找到前面最后一个兼容地点
参考代码
简洁版
cpp
#include <bits/stdc++.h>
using namespace std;
const int N = 105;
int T, n, k;
struct node {
int m, p;
} q[N];
int dp[N];
int main() {
// freopen(".in", "r", stdin);
// freopen(".out", "w", stdout);
cin >> T;
while (T--) {
cin >> n >> k;
for (int i = 1; i <= n; i++) cin >> q[i].m;
for (int i = 1; i <= n; i++) cin >> q[i].p;
memset(dp, 0, sizeof dp);
for (int i = 1; i <= n; i++) {
int pre = 0;
for (int j = i - 1; j >= 1; j--) {
if (q[i].m - q[j].m > k) {
pre = j;
break;
}
}
dp[i] = max(dp[i - 1], q[i].p + dp[pre]);
}
cout << dp[n] << "\n";
}
return 0;
}
详细注释版
cpp
#include <bits/stdc++.h>
using namespace std;
// 定义常量
const int N = 105; // N: 最大地点数
int T, n, k; // T: 测试组数,n: 地点数,k: 最小距离限制
// 定义结构体存储每个地点的信息
struct node {
int m; // 位置(position)
int p; // 利润(profit)
} q[N]; // 数组q存储所有地点,索引从1开始
int dp[N]; // DP数组:dp[i]表示考虑前i个地点时的最大利润
int main() {
// freopen(".in", "r", stdin);
// freopen(".out", "w", stdout);
cin >> T; // 读取测试组数
while (T--) { // 处理每组数据
cin >> n >> k; // 读取地点数n和距离限制k
// 读取n个地点的位置
for (int i = 1; i <= n; i++) cin >> q[i].m;
// 读取n个地点的利润
for (int i = 1; i <= n; i++) cin >> q[i].p;
// 初始化DP数组为0
memset(dp, 0, sizeof dp);
// 动态规划主循环
for (int i = 1; i <= n; i++) {
// pre: 在第i个地点之前,最后一个与i距离大于k的地点的索引
// 初始化为0,表示前面没有这样的地点(dp[0]=0)
int pre = 0;
// 从i-1开始向前搜索,寻找距离大于k的地点
// 因为数组位置是升序的,从后往前找到的第一个满足条件的j就是最大的j
for (int j = i - 1; j >= 1; j--) {
// 检查距离条件:当前位置 - 前面位置 > k
if (q[i].m - q[j].m > k) {
pre = j; // 找到满足条件的地点的索引
break; // 找到就退出,因为要找最大的j
}
}
// 循环结束后的pre:
// 如果找到:pre是与i距离>k的最大的j
// 如果没找到:pre保持为0
// 状态转移方程:
// 情况1:不选第i个地点 -> dp[i-1]
// 情况2:选第i个地点 -> q[i].p + dp[pre]
// dp[pre]是考虑前pre个地点的最大利润,
// 选择pre之后的地点不会与i冲突(因为距离>k)
dp[i] = max(dp[i - 1], q[i].p + dp[pre]);
}
// 输出结果:考虑所有n个地点时的最大利润
cout << dp[n] << "\n";
}
return 0;
}