YBTOJ 5.6单调队列

A.滑动窗口

image
image

板子题

点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 1000037;
long long a[N];
int qmax[N], qmin[N];
int main() {
    int n, k;
    scanf("%d%d", &n, &k);

    for (int i = 1; i <= n; i++) scanf("%lld", &a[i]);

    int hmin = 0, tmin = -1;
    for (int i = 1; i <= n; i++) {
        if (hmin <= tmin && i - qmin[hmin] >= k)
            hmin++;

        while (hmin <= tmin && a[i] < a[qmin[tmin]]) tmin--;
        qmin[++tmin] = i;

        if (i >= k)
            printf("%lld ", a[qmin[hmin]]);
    }

    printf("\n");

    int hmax = 0, tmax = -1;
    for (int i = 1; i <= n; i++) {
        if (hmax <= tmax && i - qmax[hmax] >= k)
            hmax++;

        while (hmax <= tmax && a[i] > a[qmax[tmax]]) tmax--;
        qmax[++tmax] = i;

        if (i >= k)
            printf("%lld ", a[qmax[hmax]]);
    }

    return 0;
}

B.粉刷木板

image
image

非常令人头疼的一道题
特别推荐下神 wind_whisper 的写法 不仅压到一维个人认为反而还比二维更为清晰明了
我们考虑每次刷对于整个序列的影响
因为要求的是一段连续长度不小于 \(L\) 的区间 所以我们考虑分别枚举左右端点
这样就有 \(f[r] = max(f[l] + (r - l) * p)\)
并且刷完之后要对后面点也进行更新 即 \(f[i] = max(f[i], f[i - 1])\)
这里我们就能发现一个转移顺序的问题 假如说你先转移了比较靠后的一段 然后转移了另一个比较靠前并且价值很大的一段 那么更新的时候就会把比较靠后的一段的贡献刷掉
所以要对 \(s\) 进行排序然后再转移
但是发现这样的做法是 \(O(m * n^2)\) 的 考虑优化
显然问题出在对 \(l\) \(r\) 的枚举上 我们想想能否优化这个过程
那我们考虑这题里的 \(l\) \(r\) 有什么特殊之处 然后我们就发现了 \(l\) \(r\) 之差不小于 \(L\)
进一步考虑 也就是说 对于一个给定的 \(l\) 它的 \(r\) 一定是在一段长度固定的区间
又因为这题要求最大值 所以考虑能否用单调队列优化
但是又因为这题要转移的是 \(f[r]\) 所以我们应该是固定 \(r\) 然后把 \(l\) 用单调队列优化掉
那我们把 \(r\) 提出来 就有 \(f[r] = r * p + max(f[l] - p * l)\) 后面那坨就可以用单调队列优化
代码我写的二维的 比较臃肿

点击查看代码
#include <bits/stdc++.h>
using namespace std;

const int N = 2e4 + 0721;
const int M = 121;
int f[M][N];
int q[N], h, t;
struct node {
	int l, p, s;
	friend bool operator<(node a, node b) {
		return a.s < b.s;
	}
} ban[M];
int n, m;

signed main() {
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= m; ++i) scanf("%d%d%d", &ban[i].l, &ban[i].p, &ban[i].s);
	
	sort(ban + 1, ban + 1 + m);
	for (int i = 1; i <= m; ++i) {
		h = 1, t = 0;
		for (int k = max(0, (ban[i].s - ban[i].l)); k < ban[i].s; ++k) {
			while (h <= t && f[i - 1][q[t]] - q[t] * ban[i].p <= f[i - 1][k] - k * ban[i].p) --t;
			q[++t] = k;
		}
		for (int j = 1; j <= n; ++j) {
			f[i][j] = max(f[i - 1][j], f[i][j - 1]);
			if (j >= ban[i].s) {
				while (h <= t && j - q[h] > ban[i].l) ++h;
				if (h <= t) f[i][j] = max(f[i][j], (j - q[h]) * ban[i].p + f[i - 1][q[h]]);
			}
		}
	}
	
	printf("%d",f[m][n]);
	
	return 0;
}

C.耗费体力

image
image

生动形象地诠释了题干的每个条件都不白给
\(f[i]\) 表示跳到 \(i\) 的最小体力花费
容易有

  • \(f[i] = f[k] (h[k] > h[i])\)
  • \(f[i] = f[k] + 1 (h[k] \le h[i])\)
    然后 \(k\) 显然是 \(i\) 前面一段长度固定的区间 可以单调队列优化
    然后就卡住了
    瓶颈在于因为 \(k\) 分高于 \(i\) 和低于 \(i\) 两种
    我们考虑“比你小还比你强原则” 能想到优先让 \(f\) 值小的在里面 \(f\) 值相同就优先让高度大的在里面 因为更有可能触发 \(h[k] > h[i]\)
    那么这么干很容易想到一个问题:有没有可能这个点 \(f\) 值小但是比较矮会触发额外体力耗费 然后它后面有个不会触发额外体力耗费的稍大一点的点 那就会出问题
    然后发现题干中额外体力耗费固定是 \(1\)
    所以以上那种情况不可能发生 撑死是等于 这样做一定不会更劣
点击查看代码
#include <bits/stdc++.h>
using namespace std;

const int N = 1e6 + 0721;
int a[N], q[N], f[N];
int h, t;
int n, m;

int main() {
	scanf("%d", &n);
	for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);
	scanf("%d", &m);
	
	while (m--) {
		int len;
		scanf("%d", &len);
		h = 1, t = 0;
		q[++t] = 1;
		for (int i = 2; i <= n; ++i) {
			while (h <= t && i - q[h] > len) ++h;
			if (a[q[h]] > a[i]) f[i] = f[q[h]];
			else f[i] = f[q[h]] + 1;
			while (f[i] < f[q[t]] || (f[i] == f[q[t]] && a[i] >= a[q[t]])) --t;
			q[++t] = i;
		}
		printf("%d\n",f[n]);
	}
	
	return 0;
}

D.燃放烟火

image
image

首先考虑简化条件 \(t_i\)\(d\) 放一起显然就代表着两次烟火之间能移动的最大距离
我们设 \(f[i][j]\) 表示燃放第 \(i\) 个烟火时在 \(j\) 位置的最大幸福值
然后显然 \(f[i][j]\) 的状态能从 \(f[i - 1]\)\(j\) 前面加后面一段连续的区间转移过来
这里要注意一个问题 因为单调队列优化 \(DP\) 只能更新当前定区间结尾的 \(DP\)
所以对于目标状态夹在中间的 我们要正反扫两遍
又因为 \(f[i]\) 只和 \(f[i - 1]\) 有关 所以开两个数组来回滚就行
复杂度 \(O(m * n)\)

点击查看代码
#include <bits/stdc++.h>
#define ll long long

using namespace std;

const int N = 152152;
ll f[2][N];
int t[N], q[N];
int n, m, d;

int main() {
	scanf("%d%d%d", &n, &m, &d);
	
	int p = 0;
	for (int i = 1; i <= m; ++i) {
		p = 1 - p;
		int a, b;
		scanf("%d%d%d", &a, &b, &t[i]);
		ll len = 1ll * (t[i] - t[i - 1]) * d;
		int h = 1, t = 0;
		for (int j = 1; j <= n; ++j) {
			while (h <= t && j - q[h] > len) ++h;
			while (h <= t && f[1 - p][q[t]] < f[1 - p][j]) --t;
			q[++t] = j; 
			f[p][j] = f[1 - p][q[h]] - abs(a - j) + b;
		}
		h = 1, t = 0;
		for (int j = n; j >= 1; --j) {
			while (h <= t && q[h] - j > len) ++h;
			while (h <= t && f[1 - p][q[t]] < f[1 - p][j]) --t;
			q[++t] = j; 
			f[p][j] = max(f[p][j], f[1 - p][q[h]] - abs(a - j) + b);
		}
	} 
	
	ll ans = -0x7ffffffffffffff;
	for (int i = 1; i <= n; ++i) ans = max(ans, f[p][i]);
	printf("%lld",ans);
	
	return 0;
}

E.跳房子

image
image
image

这竟然是道普及组的题
至少花多少金币 看起来就很二分
我们还是设 \(f[i]\) 表示跳到第 \(i\) 格的最大得分 考虑能从哪转移过来
发现是 \(i\) 前面和 \(i\) 隔着一些距离的连续一段定区间
那就上单调队列
那如何控制这个队列队尾元素的位置呢 实际上再弄个指针就行了

点击查看代码
#include <bits/stdc++.h>
#define ll long long

using namespace std;

const int N = 5e5 + 0721;
const ll inf = 0x7ffffffffffffff;
int loc[N], c[N], q[N];
ll f[N];
int n, d, k;

bool check(int x) {
	int l = max(1, d - x), r = d + x;
	f[0] = 0;
	for (int i = 1; i <= n; ++i) f[i] = -inf;
	int h = 1, t = 0, j = 0;
	for (int i = 1; i <= n; ++i) {
		while (loc[i] - loc[j] >= l) {
			while (h <= t && f[q[t]] < f[j]) --t;
			q[++t] = j;
			++j;
		}
		while (h <= t && loc[i] - loc[q[h]] > r) ++h;
		if (h <= t) f[i] = f[q[h]] + c[i];
		if (f[i] >= k) return 1;
	}
	return 0;
}

int main() {
	scanf("%d%d%d", &n, &d, &k);
	for (int i = 1; i <= n; ++i) scanf("%d%d", &loc[i], &c[i]);
	
	int l = 0, r = loc[n];
	int ans, mid;
	while (l <= r) {
		mid = l + (r - l >> 1);
		if (check(mid)) {
			ans = mid;
			r = mid - 1;
		} else
			l = mid + 1;
	}
	if (l >= loc[n]) printf("-1");
	else printf("%d",ans);
	
	return 0;
}
posted @ 2023-07-03 18:59  Steven24  阅读(48)  评论(0编辑  收藏  举报