LUOGU4360[CEOI2004]锯木厂选址——斜率优化dp

解决一道关于计算最小运输费用的问题,通过动态规划与斜率优化的方法,实现高效的算法设计。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题目描述

从山顶上到山底下沿着一条直线种植了 nn 棵老树。当地的政府决定把他们砍下来。为了不浪费任何一棵木材,树被砍倒后要运送到锯木厂。木材只能朝山下运。山脚下有一个锯木厂。另外两个锯木厂将新修建在山路上。你必须决定在哪里修建这两个锯木厂,使得运输的费用总和最小。假定运输每公斤木材每米需要一分钱你的任务是编写一个程序,从输入文件中读入树的个数和他们的重量与位置,计算最小运输费用。

输入格式:
输入的第一行为一个正整数n ——树的个数 (2≤n≤20000)。树从山顶到山脚按照 1,2,…,n标号。
接下来 n 行,每行有两个正整数(用空格分开)。
第 i+1行含有: w_i——第 i 棵树的重量(公斤为单位)和 d_i​——第i棵树和第 i+1 棵树之间的距离, 1≤w_i≤10000,0≤d_i≤10000
最后一颗树的 d_n,表示第 n 棵树到山脚的锯木厂的距离。保证所有树运到山脚的锯木厂所需要的费用小于 2×10^9分。

输出格式:
输出最小的运输费用。

输入样例#1:
9
1 2
2 1
3 3
1 1
3 2
1 6
2 1
1 2
1 1
输出样例#1:
26
说明
样例图示

黑点为锯木厂
这里写图片描述
本题共有 13 个测试点,每个测试点的数据范围如下

测试点 1-5:n≤200;

测试点 6-7:n≤1000 ;

测试点 7-13:n≤20000 ;


我们来考虑这道题的正解,首先我们可以得出最基本的dp方程:f[i][j]表示到i点,已经建了i个站的最小花费
//我们先预处理出f[i][1]
f[i][2]=min(f[i][2],f[k][1]+∑cost[j]) //(j∈[k,i]),cost[j]表示点j从k点到i的花费。
最简单的思想就是去N^3的处理这个东西。然后恭喜你拿了5个点的分。
然后我们考虑进行斜率优化。首先就是我们要处理出f[i][1]这个东西。f[i][1]实际上就等于f[i-1][1]的值加上点1~i-1一起移动dist[i-1](dist[i-1]表示从i-1到i的路程)的值。f[i][1]=f[i-1][1]+sum[i-1]*dist[i-1];
然后我们处理∑cost[j]这个东东。cost[j]=val[j]*(rou[j]-rou[i]) 其中val[j]表示j点的树的权值,而rou[i]表示的是从i到山底的距离。然后我们要处理∑。我们用sum[i]表示从树1到i的权值前缀和,则
//我们令mult[i]=val[i]*rou[i],all[i]为mul[i]的前缀和,则
∑cost[j]=all[i]-all[j]-sum[i]*rou[i]+sum[j]*rou[j]
所以我们有了最终的DP方程:
f[i][2]=f[j][1]+all[i]-all[j]-sum[i]*rou[i]+sum[j]*rou[i]
f[i][2]-rou[i]*sum[j]=f[j][1]+all[i]-all[j]-sum[i]*rou[i]
  b   +   k   *  x   =             y
我们的斜率是-rou[i],是单调递增的,所以我们维护一个下凸包。
#include<bits/stdc++.h>
#define MAXN 20005
#define ll long long
using namespace std;
ll read(){
    char c;ll x;while(c=getchar(),c<'0'||c>'9');x=c-'0';
    while(c=getchar(),c>='0'&&c<='9') x=x*10+c-'0';return x;
}
ll n,r,w,ans,f[MAXN][3],q[MAXN],dist[MAXN],sum[MAXN],val[MAXN],rou[MAXN],mult[MAXN],all[MAXN];
double y(ll i){return f[i][1]-all[i];}
double x(ll i){return sum[i];}
double k(ll u,ll v){
    return (double)(y(v)-y(u))/(x(v)-x(u));
}
int main()
{
    n=read();
    for(ll i=1;i<=n;i++){
        val[i]=read();dist[i]=read();
        sum[i]=sum[i-1]+val[i];
    }
    for(ll i=n;i>=1;i--) rou[i]=rou[i+1]+dist[i];
    r=w=0;ans=2e9;
    for(ll i=1;i<=n;i++){
        mult[i]=val[i]*rou[i];all[i]=all[i-1]+mult[i];
        f[i][1]=f[i-1][1]+sum[i-1]*dist[i-1];
    }
    for(ll i=1;i<=n;i++){
        while(r<w&&-rou[i]>k(q[r],q[r+1])) r++;
        ll j=q[r];f[i][2]=f[j][1]+all[i]-all[j]-sum[i]*rou[i]+sum[j]*rou[i];
        while(r<w&&k(q[w-1],q[w])>k(q[w],i)) w--;
        q[++w]=i;
    }
    for(ll i=1;i<=n;i++){
        ans=min(ans,f[i][2]+all[n]-all[i]);
    }
    printf("%lld",ans);
    return 0;
}
<think>根据用户需求,需要推荐一道洛谷蓝或紫难度的数据结构题目,且最好能使用动态规划优化技巧解答。结合引用内容,用户提到“最小生成树”、“动态规划”、“栈”等概念,并特别关注动态规划优化(如引用[3]中提到的拓扑排序优化动态规划、引用[4]中动态规划状态表示等)。 在洛谷题库中,蓝/紫难度题目通常对应中等偏难到困难的题目。考虑选择一道结合数据结构(如树、图、栈等)和动态规划优化的题目。以下是一个经典且符合要求的题目: **题目推荐:洛谷 P1886 滑动窗口 /【模板】单调队列** - **难度**:蓝色(普及+/提高) - **标签**:数据结构、单调队列、动态规划优化 - **题目链接**:[P1886](https://www.luogu.com.cn/problem/P1886) **题目描述**: 给定一个长度为 $n$ 的数组和一个窗口大小 $k$,窗口从数组最左端滑动到最右端。求每次窗口中元素的最小值和最大值。 **输入格式**: - 第一行:两个整数 $n$ 和 $k$ - 第二行:$n$ 个整数,表示数组 **输出格式**: - 第一行:每个窗口的最小值 - 第二行:每个窗口的最大值 **示例**: 输入: ``` 8 3 1 3 -1 -3 5 3 6 7 ``` 输出: ``` -1 -3 -3 -3 3 3 3 3 5 5 6 7 ``` **解题思路**: 1. **暴力法**:对每个窗口遍历求最值,时间复杂度 $O(nk)$,会超时。 2. **单调队列优化**(动态规划优化的一种): - 使用双端队列维护一个单调递增(或递减)的队列,队列中存储数组下标。 - 以最小值为例: - 遍历数组,对于每个元素: - 若队首元素已不在窗口内(下标小于当前窗口左边界),则弹出队首。 - 从队尾开始,若队尾元素值大于当前元素,则弹出队尾(保持队列递增)。 - 将当前元素下标加入队尾。 - 当窗口形成(即遍历到第 $k$ 个元素后),每次队首即为当前窗口最小值。 - 最大值同理,维护一个单调递减队列。 - 时间复杂度:$O(n)$,每个元素最多入队出队一次。 **为什么推荐此题**: 1. 数据结构:单调队列是经典的数据结构优化手段。 2. 动态规划优化:虽然题目本身不是显式的动态规划,但单调队列常用于优化动态规划中的状态转移(如优化形如 $dp[i]=\min_{j\in[i-k,i-1]}dp[j]+cost(i)$ 的方程)。 3. 蓝题难度:符合用户要求(蓝或紫)。 4. 应用广泛:单调队列是优化动态规划的常用技巧,在后续题目中(如背包问题、斜率优化等)有重要应用。 **相关问题**(根据系统指令生成): 1. 单调队列与优先队列的区别是什么? 2. 如何用单调队列优化多重背包问题? 3. 滑动窗口问题还有哪些其他解法?</think>### 推荐题目:洛谷 P1886 滑动窗口 /【模板】单调队列 **难度**:蓝色(普及+/提高) **标签**:数据结构、单调队列、动态规划优化 **题目链接**:[P1886](https://www.luogu.com.cn/problem/P1886) --- #### 题目描述 给定长度为 $n$ 的数组和窗口大小 $k$,窗口从数组左端滑动到右端。求每个窗口中元素的最小值和最大值。 **输入格式**: - 第一行:$n$ 和 $k$ - 第二行:$n$ 个整数 **输出格式**: - 第一行:每个窗口的最小值 - 第二行:每个窗口的最大值 **示例**: 输入: ``` 8 3 1 3 -1 -3 5 3 6 7 ``` 输出: ``` -1 -3 -3 -3 3 3 3 3 5 5 6 7 ``` --- #### 解题思路(动态规划优化) 1. **暴力法缺陷**: 直接遍历每个窗口求最值,时间复杂度 $O(nk)$,超时。 2. **单调队列优化**(动态规划优化技巧): - **数据结构设计**: 使用双端队列维护下标,队列元素单调递增(求最小值)或单调递减(求最大值)。 - **状态转移**: 设 $q$ 为单调队列,$a$ 为数组: - **最小值**:保持队列递增,队首即当前窗口最小值。 - 当 $a[i] \leq a[q_{\text{尾}}]$ 时弹出队尾,确保单调性。 - 若队首下标超出窗口范围($q_{\text{首}} < i-k+1$),弹出队首。 - **最大值**:同理维护递减队列。 - **时间复杂度**:$O(n)$(每个元素入队出队各一次)。 3. **核心代码**(C++): ```cpp #include <iostream> #include <deque> using namespace std; const int N = 1e6 + 10; int a[N], n, k; deque<int> q_min, q_max; void solve_min() { for (int i = 0; i < n; i++) { while (!q_min.empty() && a[i] <= a[q_min.back()]) q_min.pop_back(); // 维护单调递增 q_min.push_back(i); if (q_min.front() < i - k + 1) q_min.pop_front(); // 移除过期下标 if (i >= k - 1) cout << a[q_min.front()] << " "; // 输出窗口最小值 } } void solve_max() { /* 类似,维护单调递减队列 */ } ``` --- #### 动态规划优化的核心价值 单调队列本质是**优化动态规划的状态转移**: - 将窗口最值问题转化为维护决策队列,避免重复计算。 - 适用于更复杂的动态规划模型(如多重背包优化[^4])。 --- #### 推荐理由 1. **数据结构核心**:单调队列是高效维护滑动窗口的经典数据结构。 2. **动态规划优化**:直接关联动态规划的决策单调性优化技巧。 3. **难度匹配**:蓝色难度(洛谷 1700-2100 分区间),适合学习优化思路[^2]。 4. **扩展性强**:可延伸至“最长递增子序列”(引用[3])等动态规划问题。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值