算法分析
单调队列
顺时针
每个点i
表示从i
点加oil[i]
的油再耗dist[i]
的油所剩的油量,即oil[i] - dist[i]
- 1、计算出油量的前缀和
- 2、从某个点
i
出发,顺时针走一圈,在过程中油量始终>= 0
,等价于在[i,i + n - 1]
中,对任意的j
,i <= j <= i + n - 1
,均有s[j] - s[i - 1] >= 0
,即i
固定,找s[j]
的最小值,即从[i,i + n - 1]
中找到滑动窗口的最小值 - 3、由于
2
操作,需要对i
进行反向遍历,即从n * 2
遍历到1
,又由于i <= j <= i + n - 1
,遍历到i
时需要用到i
位置的值,因此找[i,i + n - 1]
区间最小值时需要在while
后面的语句找
逆时针
每个点i
表示从i
点加oil[i]
的油再耗dist[i - 1]
的油所剩的油量,即oil[i] - dist[i - 1]
,其中1
号点浩的是dist[n]
的油,因此需要初始化dist[0] = dist[n]
- 1、计算出油量的后缀和
- 2、从某个点
i
出发,逆时针走一圈,在过程中油量始终>= 0
,等价于在[i - n + 1,i]
中,对任意的j
,i - n + 1 <= j <= i
,均有s[j] - s[i + 1] >= 0
,即i
固定,找s[j]
的最小值,即从[i - n + 1,i]
中找到滑动窗口的最小值 - 3、由于
2
操作,需要对i
进行正向遍历,即从1
遍历到n * 2
,又由于i - n + 1 <= j <= i
,遍历到i
时需要用到i
位置的值,因此找[i - n + 1,i]
区间最小值时需要在while
后面的语句找
时间复杂度 $O(n)$
参考文献
算法提高课
Java 代码
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
public class Main {
static int N = 100000 * 2 + 10;
static int[] oil = new int[N];
static int[] dist = new int[N];
static long[] s = new long[N];
static int[] q = new int[N];
static boolean[] st = new boolean[N];//表示从i点出发是否有解
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter log = new BufferedWriter(new OutputStreamWriter(System.out));
int n = Integer.parseInt(br.readLine());
for(int i = 1;i <= n;i ++)
{
String[] s1 = br.readLine().split(" ");
oil[i] = Integer.parseInt(s1[0]);
dist[i] = Integer.parseInt(s1[1]);
}
//顺时针
for(int i = 1;i <= n ;i ++) s[i] = s[i + n] = oil[i] - dist[i];
for(int i = 1;i <= n * 2;i ++) s[i] += s[i - 1];
int hh = 0,tt = -1;
for(int i = n * 2;i >= 1;i --)
{
if(hh <= tt && q[hh] > i + n - 1) hh ++;
while(hh <= tt && s[q[tt]] >= s[i]) tt --;
q[++ tt] = i;
if(i <= n && s[q[hh]] - s[i - 1] >= 0) st[i] = true;
}
//逆时针
dist[0] = dist[n];
for(int i = n;i >= 1;i --) s[i] = s[i + n] = oil[i] - dist[i - 1];
for(int i = n * 2;i >= 1;i --) s[i] += s[i + 1];
hh = 0;
tt = -1;
for(int i = 1;i <= n * 2;i ++)
{
if(hh <= tt && q[hh] < i - n + 1) hh ++;
while(hh <= tt && s[q[tt]] >= s[i]) tt --;
q[++ tt] = i;
if(i >= n + 1 && s[q[hh]] - s[i + 1] >= 0) st[i - n] = true;
}
for(int i = 1;i <= n;i ++)
{
if(st[i]) log.write("TAK\n");
else log.write("NIE\n");
}
log.flush();
}
}
感谢 视频看的有点懵懂 一下子搞清楚了
搞懂了Orz
答主可否详细说明一下,为什么顺时针中“由于2操作,需要对i进行反向遍历”,找出一个区间的最小值,正向反向遍历不是应该没有区别吗?
自己的理解,错误了请勿见怪:因为顺时针时,遍历的是起点i,而起点i依赖的是它身后n个数据,在遍历到i时,它后面的数据还没有考虑到,不会给i带来帮助,起不到dp的作用。而如果是倒序遍历的话,等遍历到i时,其实i 需要依赖的信息都已经计算完成,通过滑动窗口中保留的最小值,可以直接为i服务,i的计算就很方便了,这也就是为什么这道题说的是单调队列优化DP问题的原因,本身还是一个DP问题,DP就是需要解决数据的供给关系,联想一下最简单的数字三角形问题,也就明白了为什么要倒着来看了。
👍
懂了,谢谢!
讲的透彻
我们维护的是一个长度为n+1窗口内的最小值,无论你是从前到后还是从后到前添加元素进窗口我都可以动态的维护窗口内的单调性,最值都在队头对尾取,遍历顺序没什么影响。只不过顺时针和逆时针要注意最值和窗口左侧元素比还是窗口右侧元素比而已
# woc
爱了,终于懂了
nb
有帮助就好,有问题多交流
其实DP的本质是打表,一般的开发步骤是先搞出来状态转移方程,然后使用瞪眼大法(观察法)观察,f[i]是依赖f[i-1],还是f[i+1],f[i-1]就正序遍历,f[i+1]就倒序遍历,反正顺序不重要,重要的是提前准备好需要依赖的项。可以在做动态规划题时,增加一个思考枚举顺序的步骤,这样就不会出错了。
我记得CSP-J 2020年有一道题是方格取数,可以从上,下,左三个方向走到,那道题就是按列进行的枚举,因为按行的话就无法实现提前准备好数据,总结一句话:枚举无固定顺序,一切要看状态方程。
你是超人
其实不一定非要倒序枚举的,可以正序,也仅仅只是一个下标偏移的问题而已,并没有那么复杂。可以参考我的这个题解
https://www.acwing.com/file_system/file/content/whole/index/content/12170234/?from=app_share
y总听思路,题解看细节
or2,y总这里是倒序?!太让我费解了QAQ
“每个点i表示从i点加oil[i]的油再耗dist[i - 1]的油所剩的油量。”这是为什么啊。题目里面没说逆时针怎么耗油的,这是怎么看出来的
写的不错