最近在补全提高课所有题目的题解,宣传一下汇总的地方提高课题解补全计划
题目描述
给定一个 环,环 上有 n 个节点,编号从 1∼n,以及一辆小车车
每一个 节点 i 有一个 权值 pi 表示当车 到达该点 时,可以 获得 的 油量
还有一个 权值 di 表示当车从 节点 i 到 节点 i+1 所需要 消耗 的 油量
现有一辆车想从环上 任意点 出发,顺时针 或 逆时针 绕环一圈走回起点
行驶的过程中,油量不能为 负数,初始油量 为 起点 处所能获得的 油量
判断能否完成 环圈行驶
分析
看到 环形问题,就会想到在先前 环形区间DP 中提到的一个解决方法:拉环成链
复制 一份数组接在 原数组 后面,这样原问题就 等价于 在 新数组 中 存在 长度为 n+1 的 合法区间
然后考虑区间何时 合法 的问题,题中提及:
- 会先在一个点 获得油量
- 然后 消耗油量 到达下一个点
- 消耗后的油量 不能小于 0
这极像一个我们熟悉的 括号序列模型 — 任意 前缀区间 必然满足 左括号数≥右括号数
故想到用 前缀和思想 来分析:该 区间 如果 合法,则必然 任意前缀 获得油量 ≥ 消耗油量
这里我们要 明确 每个数组下标的实际含义,才能调对本题的 边界,不然会
WA
疯
先讨论 顺时针的走法
- d[i]:点 i→ 点 i+1 消耗 的 油量
- p[i]:到点 i 能 获得 的 油量
相对应的 前缀和 含义:
- sd[i] (d[1⋯i]):从 1 出发到 i+1 所 消耗 的 总油量
- sp[i] (p[1⋯i]):从 1 出发到 i 所 获得 的 总油量
显然,我们要到 i+1 点,需要满足 此前的剩余油量 + 该点获得的油量 ≥ 到达下点消耗的油量
因此,我们直接 比较 相同下标下的前缀和大小 即可
i 到 i+n 的耗油:d[i]+d[i+1]+⋯+d[i+n−1]
i 到 i+n 的加油:p[i]+p[i+1]+⋯+p[i+n−1]
闫氏DP分析法
状态表示-集合 fi:以 i 为 起点 和 终点,顺时针 走一圈的 方案
状态表示-属性 fi:方案 是否可行 true
状态计算 :
fi=[min
和往常一样,提出 常量 :f_i = \bigg[sd_{i - 1} - sp_{i-1} + \min\Big\{sp_j - sd_j\Big\} \ge 0 \bigg] \quad (i \le j \le i+n-1)
这样就变成一个 滑动窗口求最小值 的问题了,直接套 单调队列 即可
再推导一下滑动窗口的范围:1 \le i + n - j \le n
故 滑动窗口 满足的 最大大小 为 n,且 先算后加
再讨论 逆时针的走法
镜像分析 一下就好了,不过下标有所变化
大家可以先 平移下标,继续 套上述公式;也可以 再重新推导一遍,强化一下自己对公式推导的能力
我这里就 重新推导 一遍了
i + n 到 i 的耗油:d[i] + d[i + 1] + \cdots + d[i + n - 1]
i + n 到 i 的加油:p[i + 1] + p[i + 2] + \cdots + p[i + n]
闫氏DP分析法
状态表示-集合 f_i:以 i 为 起点 和 终点,逆时针 走一圈的 方案
状态表示-属性 f_i:方案 是否可行 true
状态计算 :
f_i = \bigg[\min\Big\{(sp_{i+n} - sp_{j-1}) - (sd_{i+n-1} - sd_{j-2})\Big\} \ge 0 \bigg] \quad (i + 1 \le j \le i+n) \\\\ f_i = \bigg[sp_{i+n} - sd_{i+n-1} + \min\Big\{-sp_{j-1} + sd_{j-2}\Big\} \ge 0 \bigg] \quad (1 \le j - i \le n)
Code
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 1e6 + 10, M = N << 1;
int n;
int p[M], d[M], que[M];
LL sp[M], sd[M];
bool f[N];
LL g(int j)
{
return sp[j] - sd[j];
}
LL h(int j)
{
return sd[j - 2] - sp[j - 1];
}
LL get_val1(int i, int j)
{
return sd[i - 1] - sp[i - 1] + g(j);
}
LL get_val2(int i, int j)
{
return sp[i + n] - sd[i + n - 1] + h(j);
}
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; i ++ )
{
scanf("%d%d", &p[i], &d[i]);
p[i + n] = p[i];
d[i + n] = d[i];
}
for (int i = 1; i <= n << 1; i ++ )
{
sd[i] = sd[i - 1] + d[i];
sp[i] = sp[i - 1] + p[i];
}
int hh = 0, tt = -1;
for (int i = 1; i <= n << 1; i ++ )
{
while (hh <= tt && i - que[hh] > n) hh ++ ;
if (i > n)
if (get_val1(i - n, que[hh]) >= 0)
f[i - n] = true;
while (hh <= tt && g(que[tt]) >= g(i)) tt -- ;
que[ ++ tt] = i;
}
hh = 0, tt = -1;
for (int i = n << 1; i; i -- )
{
while (hh <= tt && que[hh] - i > n) hh ++ ;
if (i <= n)
if (get_val2(i, que[hh]) >= 0)
f[i] = true;
while (hh <= tt && h(que[tt]) >= h(i)) tt -- ;
que[ ++ tt] = i;
}
for (int i = 1; i <= n; i ++ )
puts(f[i] ? "TAK" : "NIE");
return 0;
}
这题的边界条件分析不是一般的恶心
这个逆时针的分析写得真好! 终于懂了 感谢qwq
大佬,请问滑动窗口的范围是怎么推导出来的呢
i+n 到 i 的加油:p[i+1]+p[i+2]+⋯+p[i+n] 不应该是到p[i+n+1]吗
大佬,为什么模拟队列的tt初始值是-1不是0呢
这里都行,如果是0默认数组q[0]=0;
dalao,我不知道是哪里出了问题,今天看了一天你的思路了,你这里定义f[i]是能否走到第i个加油站,那么顺时针的时候从i走到j,考虑能否走到j的时候应该是不算加油站j的油p[j]和距离d[j]吧,但是你算从i到j的总共加油是sp[j]-sp[i-1],距离是sd[j]-sd[i-1],但是按照不选第j个加油站的p[j]和d[j],我改成sp[j-1]-sp[i-1]和sd[j-1]-sd[i-1]后也可以AC。
即将g函数改成:
LL g(int j) { return sp[j - 1] - sd[j - 1]; }
这是什么情况呢,迷惑求解😭😭😭😭😭😭
我这里定义的是从 i 走到 j+1,所以是 sp[j] - sp[i - 1],你可以看到我 j 的范围最大是 i+n-1,所以最长是恰好走一圈到 i+n
如果你定义的是从 i 走到 j,滑动窗口的最大宽度也需要修正
能AC可能是数据弱了
大佬,想请教一下,为什么逆时针的表达式不是sp [ i + n ] - sp [ j ] - ( sd [ i + n - 1 ] - sd [ j - 1 ]呢。我的理解是逆时针是从 i+n 走到 j 的情况,那么得到的油是从 i+n 到 j + 1,即sp [ i + n ] - sp [ j ] ,消耗的油是从i+n到j的,即 sd [ i + n - 1 ] - sd [ j - 1 ],这里的j-1是因为sd [ j - 1 ] 表示从0到j消耗的油量的总和。求解
同问啊,我也是遇到这个情况了,而且按照大佬的思路逆时针的加油量不应该是sp[n+i]-sp[j]吗,但是大佬写的是sp[n+i]-sp[j-1],但是这样的话就会算上j那个点的油量,但是不应该加上j点的油量呀
按照我这样的理解也是可以过的,不过要先跟新后计算是否可旅行的判断,因为我们新加的点i是可以取到的,所以其跟新的队列要先计算,不然就会没有该点的判断,彩佬取j-2就会把j-1的情况算进队列里,所以可以先计算旅行判断,我的理解是这样,还望大佬指正
大佬 状态计算提出 常量 那点看不懂
为什么提出的那一堆是常量呢
计算 i 的状态,i 是定值,当然是常量
好吧 我还是不太理解为什么
min()
中被提出来的一堆是常量我再想想吧
懂了 是我傻了