关于天数限制的动态规划的一类常见技巧

发布时间 2023-07-23 17:58:13作者: Phrvth

关于天数限制的动态规划的一类常见技巧

例题:P6647 [CCC2019] Tourism

题目大意:

给定 \(n\) 个景点,每天可以游览至多 \(k\) 个景点,满足用 \(t\) 天浏览,\(t\) 必须最小,能得到的最大评分是多少?

解决方法:

首先不考虑天数限制,考虑动态规划

明显的,设 \(f_i\) 为前 \(i\) 天所能获得的最大评分,一天可以从 \([i-k,i)\) 转移,容易写出状态转移方程:

\[f_i=\max_{i-k\le j<i}\{f_j+\max_{j<k\le i}\{A_k\}\} \]

但是加上天数限制怎么做?

天数最少是不是说明转移次数越少?

那现在每次转移的代价都是一样的,如果我们给每次转移加上一个代价,是不是就可以让 \(f\) 迫不得已选择转移次数少的那个?

那怎么给每个转移加上一个代价呢?

我们是不是只要在转移后面加上一个 \(-\infty\),转移次数越多积累的 \(-\infty\) 越多,是不是就越小?

于是,我们更改状态转移方程,为了更加直观,我们定义 \(Sum(i,j)=\max_{i\le k \le j}\{A_k\}\),则状态转移方程如下:

\[f_i=\max_{i-k\le j<i}\{f_j+Sum(j+1,i)\}-\infty \]

直接暴力的话时间复杂度为 \(O(n^3)\),肯定不行

我们考虑优化转移过程

当我看到 \(i-k\le j<i\) 了,迅速想到了单调队列,但真的可以吗?

我们可以看到 \(Sum(j+1,i)\) 是个定值吗?很明显并不是,所以里面的式子他是在一直变化的,这让我们想到了具有 动态修改功能的线段树可以 \(\log\) 级维护

那变化的值呢?

很明显,如果一个 \(i\) 进来,那么所有比他小的数 \(j\) 都要增加 \(A_i-A_j\) 这个值,什么东西能维护这个东西呢?

能想到用单调栈,如果一个数 \(j\)\(i\) 弹出,那么什么区间会被影响到呢?

自行模拟一下,可以发现 \([T_{top-1},T_{top}-1]\) 这个区间被影响了,区间操作即可

最后把 \(f_{i-1}+A_i\) 这个值进行单修,最后区查即可

Qestion

那如果被 \(j\) 弹掉的 \(k\) 呢?它的大小似乎没有更新

Answer

由于在 \(k\) 的值是小于等于在 \(j\) 的值的,当且仅当在 \(k\) 更新 \(j\) 时取等,这等同于加 \(j\),并没有影响(毕竟是最大值嘛

最后献上代码

#include <bits/stdc++.h>

using namespace std;

const int MAXN = 1e6 + 7;

typedef long long ll;

const ll Inf = 1e12;

struct Segt_Tree{//需要实现区查区改单改 
	struct Node {ll sum, lz; }t[4 * MAXN];
	#define ls(i) (i << 1)
	#define rs(i) (i << 1 | 1)
	
	void push_up(int cur) {t[cur].sum = max(t[ls(cur)].sum, t[rs(cur)].sum); }
	void f(int cur, int l, int r, ll k) {
		t[cur].sum += k;
		t[cur].lz += k;
	}
	void push_down(int cur, int l, int r) {
		int mid = l + r >> 1;
		f(ls(cur), l, mid, t[cur].lz); f(rs(cur), mid + 1, r, t[cur].lz);
		t[cur].lz = 0;
	}
	
	void modify_qj(int cur, int l, int r, int x, int y, ll k) {
		if (x <= l && y >= r) {
			f(cur, l, r, k); return ;
		}
		int mid = l + r >> 1; push_down(cur, l, r);
		if (x <= mid) modify_qj(ls(cur), l, mid, x, y, k);
		if (y > mid ) modify_qj(rs(cur), mid + 1, r, x, y, k);
		push_up(cur);	
	}
	void modify_dd(int cur, int l, int r, int x, ll k) {
		if (l == r) {
			f(cur, l, r, k); return ;
		}	
		int mid = l + r >> 1; push_down(cur, l, r);
		if (x <= mid) modify_dd(ls(cur), l, mid, x, k);
		else modify_dd(rs(cur), mid + 1, r, x, k);
		push_up(cur);
	}
	ll query(int cur, int l, int r, int x, int y) {
		if (x <= l && y >= r) return t[cur].sum;
		int mid = l + r >> 1; ll ans = -1e18; push_down(cur, l, r);
		if (x <= mid) ans = max(ans, query(ls(cur), l, mid, x, y));
		if (y > mid ) ans = max(ans, query(rs(cur), mid + 1, r, x, y));
		return ans; 
	}
}t;

int n, k, A[MAXN];

int T[MAXN], tp;

ll F[MAXN];

// f[i]= max{f[j] + g(j+1, i)} - inf

int main () {
	cin >> n >> k;	
	for (int i = 1; i <= n; i ++) cin >> A[i];
	for (int i = 1; i <= n; i ++) {
		while (tp && A[T[tp]] <= A[i]) {
			t.modify_qj(1, 0, n, T[tp - 1], T[tp] - 1, A[i] - A[T[tp]]);
			tp --;
		}
		T[++ tp] = i;
		t.modify_dd(1, 0, n, i - 1, F[i - 1] + A[i]);
		F[i] = t.query(1, 0, n, max(i - k, 0), i - 1) - Inf;
		
	}
	cout << F[n] + ((n - 1) / k + 1) * Inf << '\n';
	return 0;
}

积累到一个 trick好叭(很开心

完结撒花✿✿ヽ(°▽°)ノ✿