【信息学奥赛一本通】1296:开餐馆

题目描述

北大信息学院的同学小明毕业之后打算创业开餐馆.

现在共有 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?

因为:

  1. 位置有序性:数组已按位置升序排列
  2. 单调性 :如果 j 满足条件,那么所有小于 j 的索引也满足条件
  3. 最优性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:没有地点时利润为 000
  • pre 可能为 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,满足动态规划的无后效性原则。

完备性

状态转移覆盖了所有可能的选择情况:

  1. 不选当前地点
  2. 选当前地点,并找到前面最后一个兼容地点

参考代码

简洁版

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;
}
相关推荐
yxm26336690812 小时前
【洛谷压缩技术续集题解】
java·开发语言·算法
Qt学视觉2 小时前
3D3-PCL全面总结
c++·opencv·3d
张人玉2 小时前
VisionPro Blob、条码识别、OCR 结构化速记版
人工智能·算法·机器学习·vsionpro
愚者游世2 小时前
力扣解决二进制&题型常用知识点梳理
c++·程序人生·算法·leetcode·职场和发展·改行学it
Geoking.2 小时前
前缀和算法:从一道 LeetCode 题看区间求和优化思想
算法·leetcode·职场和发展
爱吃rabbit的mq2 小时前
第7章 逻辑回归:二分类的基础
算法·分类·逻辑回归
DFT计算杂谈2 小时前
VASP+Wannier90 计算位移电流和二次谐波SHG
java·服务器·前端·python·算法
执着2593 小时前
力扣102、二叉树的层序遍历
数据结构·算法·leetcode
Tisfy3 小时前
LeetCode 2976.转换字符串的最小成本 I:floyd算法(全源最短路)
算法·leetcode··floyd·题解