7.14 海高集训 DP 专题 2

出题人:\(\text{D}\color{red}\text{eaphetS}\)

#A. [NOIP2012 提高组] 开车旅行

倍增优化 dp。

这题难就难在预处理。

首先预处理出 A 和 B 每个人从一个城市出发的目标是哪个城市。可以用平衡树找一个点的前驱和后继,或者双向链表。我当然选择了最偷懒的 set。(ps:这里如果用 set 的话有可能迭代器一直加或者减导致越界,又懒得判断,索性用了 multiset)

然后预处理出 \(f[i][j][k(0/1)]\) 表示第 \(k\) 个人从第 i 个城市出发走 \(2^j\) 天到达的城市,同时也更新 \(dp[x(0/1)][i][j][k(0/1)]\) 表示第 \(k\) 个人从第 \(i\) 个城市出发走 \(2^j\) 天后第 \(x\) 个人走了多少路程。

最后预处理完暴力跑几遍就行了。

#include <cstdio>
#include <algorithm>
#include <set>
using std::set;
int n, m, s, h[100005], des[100005][2], Min[100005][2], To[100005][22], dis[100005][22][2], tot[2], x;
struct info {
	int h, id;
	bool operator < (const info a) const {
		return h < a.h;
	};
};
set<info>box;
set<info>::iterator I;
int ab(int t) {
	return t < 0 ? -t : t;
}
void consider(int i, info p) {
	int j = p.id;
	if ((ab(h[i] - h[j]) < Min[i][0]) || (Min[i][0] == ab(h[i] - h[j]) && h[j] < h[des[i][0]])) {
		if ((Min[i][0] < Min[i][1]) || (Min[i][1] == Min[i][0] && h[des[i][0]] < h[des[i][1]]))
			Min[i][1] = Min[i][0], des[i][1] = des[i][0];
		Min[i][0] = ab(h[i] - h[j]), des[i][0] = j;
	} else if ((ab(h[i] - h[j]) < Min[i][1]) || (Min[i][1] == ab(h[i] - h[j]) && h[j] < h[des[i][0]]))
		Min[i][1] = ab(h[i] - h[j]), des[i][1] = j;
}
void doubling(int i, int val) {
	for (int k = 20; k >= 0; k--)
		if (dis[i][k][0] + dis[i][k][1] <= val && To[i][k])
			val -= (dis[i][k][0] + dis[i][k][1]), tot[1] += dis[i][k][1], tot[0] += dis[i][k][0], i = To[i][k];
	if (des[i][1] && Min[i][1] <= val)
		tot[1] += Min[i][1];
}
int main() {
	scanf("%d", &n);
	for (int i = 1; i <= n; i++) {
		scanf("%d", &h[i]);
		Min[i][1] = Min[i][0] = 2147483645;
	}
	for (int i = n; i >= 1; i--) {
		box.insert((info) {
			h[i], i
		});
		I = box.find((info) {
			h[i], i
		});
		++I;
		if (I != box.end())
			consider(i, *I), ++I, I != box.end() ? consider(i, *I), 1 : 1, --I;
		--I;
		if (I != box.begin())
			--I, consider(i, *I), I != box.begin() ? --I, consider(i, *I), 1 : 1;
	}
	for (int i = 1; i <= n; i++)
		To[i][0] = des[des[i][1]][0],
		           dis[i][0][1] = Min[i][1], dis[i][0][0] = Min[des[i][1]][0];
	for (int k = 1; k <= 20; k++)
		for (int i = 1; i <= n; i++)
			To[i][k] = To[To[i][k - 1]][k - 1],
			           dis[i][k][1] = dis[i][k - 1][1] + dis[To[i][k - 1]][k - 1][1], dis[i][k][0] = dis[i][k - 1][0] + dis[To[i][k - 1]][k - 1][0];
	scanf("%d", &x);
	double rate = 2147483645;
	int pos = 0;
	h[0] = -2147483645;
	for (int i = 1; i <= n; i++) {
		tot[0] = tot[1] = 0;
		doubling(i, x);
		double tmp = tot[0] ? 1.0 * tot[1] / tot[0] : 2147483645;
		if (tmp - rate < 0.000003 && tmp - rate > -0.000003 && h[i] > h[pos])
			pos = i;
		if (rate - tmp > 0.000003)
			pos = i, rate = tmp;
	}
	printf("%d\n", pos);
	scanf("%d", &m);
	for (int i = 1; i <= m; i++) {
		scanf("%d %d", &s, &x);
		tot[0] = tot[1] = 0;
		doubling(s, x);
		printf("%d %d\n", tot[1], tot[0]);
	}
	return 0;
}

#B. Count The Repetitions*

尝试匹配总字符。假设dp数组中dp[i]表示从s2的i字符开始匹配s1,可以匹配多少个字符。然后用dp数组做转移,统计从第1个s1开始,一直到第n1个s1,可以一共匹配s2中的多少个字符,再除以s2长度和n2即可。

#include <bits/stdc++.h>
using namespace std;
string S, T;
int N, M;
int getMaxRepetitions(string s1, int n1, string s2, int n2)
{
	int len = s2.length(), dp[len];
	memset(dp, 0, sizeof(dp));
	for (int i = 0; i < len; i++)
	{
		int p = i;
		for (int j = 0; j < s1.length(); j++)
			if (s1[j] == s2[p])
			{
				p = (p + 1) % len;
				dp[i]++;
			}
	}
	int sum = 0, idx = 0;
	for (int i = 0; i < n1; i++)
	{
		sum += dp[idx];
		idx = (idx + dp[idx]) % len;
	}
	return sum / len / n2;
}
int main()
{
	while(cin >> T >> M >> S >> N)
		cout << getMaxRepetitions(S, N, T, M) << endl;
	return 0;
}

#C. New Year Domino

题目大意

\(n\) 个多米诺骨牌,每个骨牌有两个值 \(x_i,h_i\) 表示每个骨牌的坐标和高度,第 \(i\) 个骨牌被推倒后,所有满足 \(x_j \leq x_i+h_i\) 的骨牌 \(j\) 也会被推倒,多次询问,问如果推倒第 \(x\) 个骨牌,最少需要花费多少代价才能推倒第 \(y\) 个骨牌。

代价定义:可以将任意一个骨牌的高度增加 \(1\),花费 \(1\) 的代价。

题意转换

每次查询下标在 \([l,r]\) 的线段所覆盖的区间中的 \(0\) 的个数。我们可以发现,对于每次查询推倒 \(l\) 骨牌,那么 \([1,l-1]\) 这些骨牌,就不应该对当前查询有所影响。

所以我们可以考虑将每次查询按左端点从大到小排序,并记一个时间戳,每次判断一下时间戳与查询区间的左端点即可。

做法

我们可以用线段树来进行区间覆盖 \(1\),区间查询 \(0\) 的数量,但是由于值域 \([1,10^9]\),普通线段树难以支持,所以我们用动态开点值域线段树(貌似离散化也可以,但是动态开点真的是暴力美学)。

我们每次插入一个新线段,那么则将区间 \([x_i+1,x_i+h_i]\) 这个区间赋为 \(1\)。注意要 \(+1\),因为每个骨牌自己的坐标不能直接赋为 \(1\),必须要由上一个骨牌更新过来才行。

一些必须要加的优化:

查询时,如果当前节点的 \(tag\)(区间赋值标记)为 \(1\),那么说明其子区间所有的值都被赋为 \(1\),所以 \(0\) 的数量为 \(0\),直接返回 \(0\) 即可。

对于每个骨牌,要将其坐标全部减去最小的坐标,防止 \(\color{blue}\text{MLE}\)

其它的一些细节可以看下代码,写的时候注意一下就好。

#include <bits/stdc++.h>
using namespace std;
struct Domino
{
	int lc, rc, sum, tag;
}Tree[8000080];
int tot = 0, n, m, rt, maxn = -1, minn = 1e9, ans[200020];;
inline void pushup(int u)
{
	Tree[u].sum = Tree[Tree[u].lc].sum + Tree[Tree[u].rc].sum;
}
inline void Lazy(int u, int l, int r)
{
	Tree[u].tag = 1;
	Tree[u].sum = r - l + 1;
}
inline void pushdown(int u, int l, int r)
{
	int mid = (l + r) >> 1;
	if (Tree[u].lc == 0)
		Tree[u].lc = ++tot;
	if (Tree[u].rc == 0)
		Tree[u].rc = ++tot;
	Lazy(Tree[u].lc, l, mid);
	Lazy(Tree[u].rc, mid + 1, r);
	Tree[u].tag = 0;
}
inline void Modify(int &u, int l, int r, int L, int R)
{
	if (L > R)
		return;
	if (!u)
		u = ++tot;
	if (l == L && r == R)
	{
		Lazy(u, l, r);
		return;
	}
	if (Tree[u].tag)
		pushdown(u, l, r);
	int mid = (l + r) >> 1;
	if (R <= mid)
		Modify(Tree[u].lc, l, mid, L, R);
	else if (L > mid)
		Modify(Tree[u].rc, mid + 1, r, L, R);
	else
	{
		Modify(Tree[u].lc, l, mid, L, mid);
		Modify(Tree[u].rc, mid + 1, r, mid + 1, R);
	}
	pushup(u);
}
inline int query(int u, int l, int r, int L, int R)
{
	if (!u)//查询的优化1
		return r - l + 1;
	if (l == L && r == R)
		return r - l + 1 - Tree[u].sum;
	if (Tree[u].tag)//查询的优化2
		return 0;
	int mid = (l + r) >> 1;
	if (R <= mid)
		return query(Tree[u].lc, l, mid, L, R);
	else if (L > mid)
		return query(Tree[u].rc, mid + 1, r, L, R);
	else return query(Tree[u].lc, l, mid, L, mid) + query(Tree[u].rc, mid + 1, r, mid + 1, R);
}
struct pile
{
	int x, h, id;
	bool operator < (const pile &u) const { //骨牌和查询的内容和排序方式差不多
		return x < u.x; //所以查询按-x加入到数组中方便排序
	}
} line[200020], in[200020];
int main()
{
	scanf("%d", &n);
	for (int i = 1; i <= n; i++)
	{
		scanf("%d %d", &line[i].x, &line[i].h);
		minn = min(minn, line[i].x);
	}
	for (int i = 1; i <= n; i++) //重点优化
		line[i].x = line[i].x - minn + 1;
	sort(line + 1, line + 1 + n);
	scanf("%d", &m);
	for (int i = 1, u; i <= m; i++)
	{
		scanf("%d %d", &u, &in[i].h);
		in[i].x = -u;
		in[i].id = i;
		maxn = max(maxn, -in[i].x);
	}
	for (int i = n; i >= maxn; i--)
		Modify(rt, 1, 1e9, line[i].x + 1, min(line[i].x + line[i].h, int(1e9)));
	sort(in + 1, in + 1 + m);
	for (int i = 1; i <= m; i++)
	{
		while (maxn > -in[i].x)
		{
			maxn--;
			Modify(rt, 1, 1e9, line[maxn].x + 1, min(line[maxn].x + line[maxn].h, int(1e9)));
		}
		ans[in[i].id] = query(rt, 1, 1e9, line[-in[i].x].x, line[in[i].h].x) - query(rt, 1, 1e9, line[-in[i].x].x, line[-in[i].x].x);//减去后面那个是为了防止,推倒了骨牌l,但是骨牌l的坐标值为0导致多算了一个0
	}
	for (int i = 1; i <= m; i++)
		printf("%d\n", ans[i]);
	return 0;
}

#D. Sum Over Zero

前置

\(dp_i\) 表示到位置 \(i\) 为止有多少点没有选

\(sum_i\) 表示 \(\sum_{i=1}^{n}a_i\)

关于为什么要令 \(dp_i\) 是没有选的最优解:\(dp\) 表示选与不选在复杂度 \(O(n^2)\) 时是都可以实现的,但是在复杂度 \(O(n\log{n})\) 时,由于要将之前的信息放到数据结构里,这个信息是不能随位置而改变的,但是如果是 \(dp_i\) 表示选的话,dp 转移方程就是 \(dp_i=\max(dp_j+i-j)\),这个信息会改变所以不适合用数据结构维护。这也是“正难则反”的思想。

朴素 dp

\(dp_i\) 可以由两种方法转移:

  1. \(dp_i\) 不取,此时 \(dp_i=dp_{i-1}+1\)
  2. \(dp_i\) 取,从 \(1\)\(i-1\) 枚举 \(j\)。考虑什么样的 \(j\) 可以转移,即 \(j+1\)\(i\) 这一段都取了,即 \(sum_i-sum_j\geq0\)。此时 \(dp_i=\min(dp_j)\)

复杂度 \(O(n^2)\)

for (int i = 1; i <= n; i++) {
	dp[i] = dp[i - 1] + 1;
	for (int j = 1; j < i; j++)
		if (sum[i] >= sum[j])
			dp[i] = min(dp[i], dp[j]);
}

如何优化

这个 dp 的复杂度不够优秀,我们考虑用数据结构维护。

发现对于 \(i\) 查询的是,位置在 \(i\) 之前,前缀和小于 \(i\) 的所有 \(dp\) 的最小值。用线段树(其他数据结构也可以)维护这个信息。

将前缀和离散化,更新时查询比当前前缀和小的最小值,在完成 \(dp_i\) 的更新之后将其插入线段树。

复杂度 \(O(n\log{n})\)

#include <bits/stdc++.h>
using namespace std;
long long n, val[200020], dp[200020], sum[200020], Sum[200020];
struct Node
{
	long long val;
}Tree[4000040];
inline void build(long long K, long long l, long long r)
{
	Tree[K].val = 0x3f3f3f3f;
	if (l == r)
		return ;
	long long mid = l + r >> 1;
	build(K << 1, l, mid);
	build(K << 1 | 1, mid + 1, r);
}
inline void modify(long long K, long long l, long long r, long long pos, long long val)
{
	if (l == r)
	{
		Tree[K].val = min(Tree[K].val, val);
		return ;
	}
	long long mid = (l + r) >> 1;
	if (pos <= mid)
		modify(K << 1, l, mid, pos, val);
	else modify(K << 1 | 1, mid + 1, r, pos, val);
	Tree[K].val = min(Tree[K << 1].val, Tree[K << 1 | 1].val);
}
inline long long query(long long K, long long l, long long r, long long L, long long R)
{
	if (L <= l && R >= r)
		return Tree[K].val;
	long long mid = l + r >> 1, ret = 0x3f3f3f3f;
	if (L <= mid)
		ret = min(ret, query(K << 1, l, mid, L, R));
	if (R > mid)
		ret = min(ret, query(K << 1 | 1, mid + 1, r, L, R));
	return ret;
}
int main()
{
	scanf("%lld", &n);
	for (long long i = 1; i <= n; i++)
	{
		scanf("%lld", &val[i]);
		sum[i] = val[i] + sum[i - 1];
		Sum[i] = sum[i];
	}
	sort(Sum, Sum + n + 1);
	long long len = unique(Sum, Sum + n + 1) - Sum;
	build(1, 0, len);
	long long Rnk = lower_bound(Sum, Sum + len, 0) - Sum;
	modify(1, 0, len, Rnk, 0);
	for (long long i = 1; i <= n; i++)
	{
		dp[i] = dp[i - 1] + 1;
		if (sum[i] >= 0)
			dp[i] = 0;
		long long pos = lower_bound(Sum, Sum + len, sum[i]) - Sum, tmp = query(1, 0, len, 0, pos);
		dp[i] = min(dp[i], tmp);
		modify(1, 0, len, pos, dp[i]);
	}
	printf("%lld", n - dp[n]);
	return 0;
}

#E. Hot Start Up (hard version)

考虑将问题转化一下,将我们要完成的任务划分为若干段区间,然后两个处理器交错处理这些区间。显然,这和原问题等价,考虑以此为切入点 dp。

\(f(i,j)\) 表示先执行 \(i\) 再执行 \(j\)\(j\) 的花费,\(calc(l,r)\) 表示处理完任务 \(l\) 后,顺次执行区间 \([l+1,r]\) 内所有任务的总花费。

\(dp_{i}\) 表示以 \(i\) 为一段区间结尾,前面划分后的最小代价。

那么枚举上一段区间结束位置,不难得到转移式:\(dp_{i}=\min\limits_{k<i}\{dp_{k}+calc(k+1,i)+f(las,k+1)\}\)

此时我们会发现遇到了一点问题:我们无从得知 \(f(las,k+1)\) 的值。如果要再记录一维 \(las\),状态数肯定爆炸。

这里采用一个很经典的思想:费用预支。

\(f(las,k+1)\) 无法在 \((k+1,i)\) 的区间里处理,那么我们就在 \((las+1,k)\) 的区间转移时把它求出来。

所以正确的转移式为:

\[dp_{i}=\min_{k<i}\{dp_k+calc(k+1,i)+f(k,i+1)\} \]

特别的,我们在 \(0\)\(n+1\) 处均加入一个花费恒为 \(0\) 的任务,规避掉一些特殊情况。

固定左端点,不断扩大右端点即可在 \(O(n^2)\) 内预处理所有的 \(calc(l,r)\),转移 \(O(n^2)\),可以通过 easy version。

接下来考虑优化,先处理 \(calc\) 函数。

\(g_i=f(i,i+1)\),则有 \(calc(l,r)=\sum\limits_{i=l}^{r-1}g_i\)

所以我们求出 \(g\) 的前缀和数组 \(sum_i=\sum\limits_{j=0}^ig_j\),即可得到 \(calc(l,r)=sum_{r-1}-sum_{l-1}\)

将其代回原转移式可得 \(dp_{i}=\min\limits_{k<i}\{dp_k+sum_{i-1}-sum_k+f(k,i+1)\}\)

按下标分类一下,\(dp_i=\min\limits_{k<i}\{dp_k-sum_k+f(k,i+1)\}+sum_{i-1}\)

前面部分的 \(dp\) 求完后,\(dp_k-sum_k\) 是可以统一维护的,那么只剩下 \(f(k,i+1)\) 一项了。考虑这东西等于什么。

\[f(x,y)=\begin{cases}hot_{a_y}& a_x=a_y\\cold_{a_y}& a_x\neq a_y\end{cases} \]

由于保证 \(hot_i\le cold_i\),所以实际上还可以更进一步得到:

\[f(x,y)=\min\begin{cases}hot_{a_y}& a_x=a_y\\cold_{a_y}\end{cases} \]

因此转移式也可以进行调整。

\[dp_{i}=sum_{i-1}+\min_{k<i}\begin{cases}dp_{k}-sum_k+cold_{a_{i+1}}\\ dp_k-sum_k+hot_{a_{i+1}}&a_k=a_{i+1}\end{cases} \]

那么我们只需要在 dp 的过程中顺带维护 \(dp_i-sum_i\) 的全局最小值和每类最小值即可,复杂度 \(O(n)\)

#include <bits/stdc++.h>
using namespace std;
long long T, n, k, a[300030], hot[300030], cold[300030], sum[300030], dp[300030], mn[300030];
inline long long calc(long long x, long long y)
{
	return a[x] == a[y] ? hot[a[y]] : cold[a[y]];
}
int main()
{
	scanf("%lld", &T);
	while (T--)
	{
		scanf("%lld %lld", &n, &k);
		for (long long i = 1; i <= n; i++)
			sum[i] = dp[i] = 0;
		for (long long i = 1; i <= k; i++)
			mn[i] = 1e18;
		a[n + 1] = hot[n + 1] = cold[n + 1] = 0;
		for (long long i = 1; i <= n; i++)
			scanf("%lld", &a[i]);
		for (long long i = 1; i <= k; i++)
			scanf("%lld", &cold[i]);
		for (long long i = 1; i <= k; i++)
			scanf("%lld", &hot[i]);
		sum[0] = calc(0, 1);
		for (long long i = 1; i <= n; i++)
			sum[i] = sum[i - 1] + calc(i, i + 1);
		long long alm = 0;
		for (long long i = 1; i <= n; i++)
		{
			dp[i] = alm + cold[a[i + 1]] + sum[i - 1];
			dp[i] = min(dp[i], mn[a[i + 1]] + hot[a[i + 1]] + sum[i - 1]);
			mn[a[i]] = min(mn[a[i]], dp[i] - sum[i]);
			alm = min(alm, dp[i] - sum[i]);
		}
		printf("%lld\n", dp[n]);
	}
	return 0;
}

#F. Non-equal Neighbours

\(f(i,j)\) 表示考虑前 \(i\) 个数,当前 \(b_i=j\) 的方案数,那么显然有:

\[f(i,j)=\begin{cases}0, &j>a_i\\(\sum_{x=1}^{a_{i-1}}f(i-1,x))-f(i-1,j), &j\le a_i\end{cases} \]

\(f(i)\) 看作序列,用线段树维护即可。需要支持区间推平(\(j>a_i\) 的情况),区间取反(\(f(i,j)\gets -f(i-1,j)\)),区间加(\(f(i,j)\gets -f(i-1,j)+\sum_{x=1}^{a_{i-1}}f(i-1,x)\)),区间查询(查询 \(\sum_{x=1}^{a_{i-1}}f(i-1,x)\))。

#include <bits/stdc++.h>
#define mod 998244353
using namespace std;
long long ans, c, n, a[200020], b[200020];
inline void Mod(long long &x)
{
	x = (x % mod + mod) % mod;
}
struct Segment_Tree
{
	struct seg
	{
		long long l, r, sum, cov, add, neg;
		seg() {l = r = sum = add = cov = neg = 0;}
	} Tree[800080];
	inline void build(long long K, long long l, long long r)
	{
		Tree[K].l = l;
		Tree[K].r = r;
		if (l == r)
			return;
		long long mid = (l + r) >> 1;
		build(K << 1, l, mid);
		build(K << 1 | 1, mid + 1, r);
	}
	inline void pushdown(long long K)
	{
		if (Tree[K].cov)
		{
			Tree[K << 1].cov = Tree[K << 1 | 1].cov = Tree[K].cov;
			Tree[K].cov = 0;
			Tree[K << 1].neg = Tree[K << 1 | 1].neg = 0;
			Tree[K << 1].add = Tree[K << 1 | 1].add = 0;
			Tree[K << 1].sum = 0;
			Tree[K << 1 | 1].sum = 0;
		}
		if (Tree[K].neg)
		{
			Tree[K << 1].neg ^= 1;
			Tree[K << 1 | 1].neg ^= 1;
			Tree[K << 1].add *= -1;
			Tree[K << 1 | 1].add *= -1;
			Mod(Tree[K << 1].add);
			Mod(Tree[K << 1 | 1].add);
			Tree[K << 1].sum *= -1;
			Tree[K << 1 | 1].sum *= -1;
			Mod(Tree[K << 1].sum);
			Mod(Tree[K << 1 | 1].sum);
			Tree[K].neg = 0;
		}
		if (Tree[K].add)
		{
			Tree[K << 1].add += Tree[K].add, Tree[K << 1].add %= mod;
			Tree[K << 1].sum += 1ll * (b[Tree[K << 1].r] - b[Tree[K << 1].l - 1]) * Tree[K].add % mod, Tree[K << 1].sum %= mod;
			Tree[K << 1 | 1].add += Tree[K].add, Tree[K << 1 | 1].add %= mod;
			Tree[K << 1 | 1].sum += 1ll * (b[Tree[K << 1 | 1].r] - b[Tree[K << 1 | 1].l - 1]) * Tree[K].add % mod, Tree[K << 1 | 1].sum %= mod;
			Tree[K].add = 0;
		}
	}
	inline void modify(long long K, long long l, long long r, long long d)
	{ //add
		if (l > r)
			return;
		if (l <= Tree[K].l && Tree[K].r <= r)
		{
			Tree[K].sum += 1ll * (b[Tree[K].r] - b[Tree[K].l - 1]) * d % mod;
			Tree[K].sum %= mod;
			Tree[K].add += d;
			Tree[K].add %= mod;
			return;
		}
		pushdown(K);
		long long mid = (Tree[K].l + Tree[K].r) >> 1;
		if (l <= mid)
			modify(K << 1, l, r, d);
		if (r > mid)
			modify(K << 1 | 1, l, r, d);
		Tree[K].sum = Tree[K << 1].sum + Tree[K << 1 | 1].sum;
		Tree[K].sum %= mod;
	}
	inline void cover(long long K, long long l, long long r)
	{
		if (l > r)
			return;
		if (l <= Tree[K].l && Tree[K].r <= r)
		{
			Tree[K].cov = 1;
			Tree[K].add = Tree[K].sum = Tree[K].neg = 0;
			return;
		}
		pushdown(K);
		long long mid = (Tree[K].l + Tree[K].r) >> 1;
		if (l <= mid)
			cover(K << 1, l, r);
		if (r > mid)
			cover(K << 1 | 1, l, r);
		Tree[K].sum = Tree[K << 1].sum + Tree[K << 1 | 1].sum;
		Tree[K].sum %= mod;
	}
	inline void Modify(long long K, long long l, long long r)
	{
		if (l > r)
			return;
		if (l <= Tree[K].l && Tree[K].r <= r)
		{
			Tree[K].neg ^= 1;
			Tree[K].sum *= -1, Tree[K].add *= -1;
			Mod(Tree[K].sum);
			Mod(Tree[K].add);
			return;
		}
		pushdown(K);
		long long mid = (Tree[K].l + Tree[K].r) >> 1;
		if (l <= mid)
			Modify(K << 1, l, r);
		if (r > mid)
			Modify(K << 1 | 1, l, r);
		Tree[K].sum = Tree[K << 1].sum + Tree[K << 1 | 1].sum;
		Tree[K].sum %= mod;
	}
	inline long long query(long long K, long long l, long long r)
	{
		if (l > r)
			return 0;
		if (l <= Tree[K].l && Tree[K].r <= r)
			return Tree[K].sum;
		pushdown(K);
		long long mid = (Tree[K].l + Tree[K].r) >> 1, ans = 0;
		if (l <= mid)
			ans = (ans + query(K << 1, l, r)) % mod;
		if (r > mid)
			ans = (ans + query(K << 1 | 1, l, r)) % mod;
		return ans;
	}
} T;
int main()
{
	scanf("%lld", &n);
	c = n;
//	离散化一波
	for (long long i = 1; i <= n; i++)
	{
		scanf("%lld", &a[i]);
		b[i] = a[i];
	}
	sort(b + 1, b + c + 1);
	c = unique(b + 1, b + c + 1) - b - 1;
	for (long long i = 1; i <= n; i++)
		a[i] = lower_bound(b + 1, b + c + 1, a[i]) - b;
//	for(long long i = 1; i <= n; i++) printf("%d ", a[i]);puts("!!!");
	T.build(1, 1, c);
	for (long long i = 1; i <= a[1]; i++)
		T.modify(1, i, i, 1);
	for (long long i = 2; i <= n; i++)
	{
		long long sum = T.query(1, 1, c);
//		printf("sum = %lld\n", sum);
		T.cover(1, a[i] + 1, c);
//		printf("sum = %lld\n", T.query(1, 1, c));
		T.Modify(1, 1, a[i]);
//		printf("sum = %lld\n", T.query(1, 1, c));
		T.modify(1, 1, a[i], sum);
//		printf("sum = %lld\n", T.query(1, 1, c));
	}
	for (long long i = 1; i <= a[n]; i++)
		ans = (ans + T.query(1, i, i)) % mod;
	printf("%lld", ans);
	return 0;
}

#G. Animal Observation (hard version)

Discription.

给定一个矩阵,现在你可以对每一行取出一个这一行和下一行组成的 \(2\times k\) 的子矩阵,并取走其中的所有数(如果最后一行的话就只能取一行。
现在你需要让你取出的所有数最大化。

Solution.

首先,有一个很显然的 \(O(nm^2)\) 的 dp。
\(dp_{i,j}\) 表示第 \(i\)\(i+1\) 行选 \([j,j+k-1]\) 这个子矩阵时的最大答案。
那么 \(dp_{i,j}\) 可以从所有的 \(dp_{i-1,k}\) 转移过来,主要就是去重。
但是如果暴力枚举时肯定能算出去重函数的,所以我们得到了一份 TLE on \(13\) 的代码,如下

#include <bits/stdc++.h>
using namespace std;
long long ans, n, m, K, a[55][20020], pre[55][20020], dp[55][20020];
inline long long calc(long long i, long long l, long long r)
{
	if (l > r)
		swap(l, r);
	return max(pre[i][l + K - 1] - pre[i][r - 1], 0ll);
}
int main()
{
	scanf("%lld %lld %lld", &n, &m, &K);
	for (long long i = 1; i <= n; i++)
		for (long long j = 1; j <= m; j++)
		{
			scanf("%lld", &a[i][j]);
			pre[i][j] = pre[i][j - 1] + a[i][j];
		}
	for (long long j = 1; j <= m - K + 1; j++)
		dp[1][j] = pre[1][j + K - 1] - pre[1][j - 1] + pre[2][j + K - 1] - pre[2][j - 1];
	for (long long i = 2; i <= n; i++)
		for (long long j = 1; j <= m - K + 1; j++)
			for (long long k = 1; k <= m - K + 1; k++)
				dp[i][j] = max(dp[i][j], dp[i - 1][k] + pre[i + 1][j + K - 1] - pre[i + 1][j - 1] + pre[i][j + K - 1] - pre[i][j - 1] - calc(i, k, j));
	for (long long j = 1; j <= m - K + 1; j++)
		ans = max(ans, dp[n][j]);
	printf("%lld", ans);
	return 0;
}

然后我们就能获得……\(0\) 分的好成绩,毕竟 F1 都不让这种方法过。

然后我们考虑优化,F1 那题有一个限制 \(k\le\min(20,m)\)
思考思考这个限制有什么用:我们会发现,因为上面的重复(calc)中有一个 max,我们考虑去掉它。
首先,矩阵中所有的数都是正数,那么也就是说只有在 l+K-1>=r 时才会取到 pre[i][l + K - 1] - pre[i][r - 1]
那么这就好办了,只有在 \(j-K+1\le k\le j+K-1\) 时那个 calc 函数才会有用。
然后又因为 \(k\) 很小,那么我们直接暴力转移这样的状态,剩下的因为 calc 函数等于 \(0\),所以我们直接记录前缀最大值后缀最大值就好了,这样复杂度是 \(O(nmk)\) 的,足以通过 F1,代码

#include <bits/stdc++.h>
using namespace std;
long long ans, n, m, K, a[55][20020], s[55][20020], dp[55][20020], mx1[55][25005], mx2[55][25005];
inline long long calc(long long i, long long l, long long r)
{
	if (l > r)
		swap(l, r);
	return l + K - 1 >= r ? s[i][l + K - 1] - s[i][r - 1] : 0;
}
int main()
{
	scanf("%lld %lld %lld", &n, &m, &K);
	for (long long i = 1; i <= n; i++)
		for (long long j = 1; j <= m; j++)
		{
			scanf("%lld", &a[i][j]);
			s[i][j] = s[i][j - 1] + a[i][j];
		}
	for (long long j = 1; j <= m - K + 1; j++)
		dp[1][j] = s[1][j + K - 1] - s[1][j - 1] + s[2][j + K - 1] - s[2][j - 1];
	for (long long j = 1; j <= m - K + 1; j++)
		mx1[1][j] = max(mx1[1][j - 1], dp[1][j]);
	for (long long j = m - K + 1; j >= 1; j--)
		mx2[1][j] = max(mx2[1][j + 1], dp[1][j]);
	for (long long i = 2; i <= n; i++)
	{
		for (long long j = 1; j <= m - K + 1; j++)
		{
			long long now = s[i + 1][j + K - 1] - s[i + 1][j - 1] + s[i][j + K - 1] - s[i][j - 1];
			dp[i][j] = max(dp[i][j], now + max(j > K ? mx1[i - 1][j - K] : 0, mx2[i - 1][j + K]));
			for (long long k = max(j - K + 1, 1ll); k <= min(j + K - 1, m - K + 1); k++)
				dp[i][j] = max(dp[i][j], dp[i - 1][k] + now - calc(i, k, j));
		}
		for (long long j = 1; j <= m - K + 1; j++)
			mx1[i][j] = max(mx1[i][j - 1], dp[i][j]);
		for (long long j = m - K + 1; j >= 1; j--)
			mx2[i][j] = max(mx2[i][j + 1], dp[i][j]);
	}
	for (long long j = 1; j <= m - K + 1; j++)
		ans = max(ans, dp[n][j]);
	printf("%lld", ans);
	return 0;
}

然后,我们继续思考,如果 \(k\) 很大这样复杂度还是无法通过 F2,我们仔细观察一下,发现上面的暴力转移过程中还可以继续分类讨论,如下

for (int k = max(j - K + 1, 1); k <= j; k++)
	dp[i][j] = max(dp[i][j], dp[i - 1][k] + now - pre[i][k + K - 1] + pre[i][j - 1]);
for (int k = j; k <= min(j + K - 1, m - K + 1); k++)
	dp[i][j] = max(dp[i][j], dp[i - 1][k] + now - pre[i][j + K - 1] + pre[i][k - 1]);

那么这样就很显然了吧。
在上面那一行中,now + pre[i][j-1]是不变的,而 dp[i - 1][k] + pre[i][k + K - 1]\(j\) 无关。
下面那一行中,now - pre[i]是不变的,而dp[i - 1][k] + pre[i][k - 1]是与 \(j\) 无关的。
那么我们只需要用线段树等数据结构来维护区间最大值就好了,复杂度是 \(O(nm\log m)\)

#include <bits/stdc++.h>
using namespace std;
long long ans, n, m, K, a[55][20020], pre[55][20020], dp[55][20020], mx1[55][25005], mx2[55][25005], t[25005];
inline long long calc(long long i, long long l, long long r)
{
	if (l > r)
		swap(l, r);
	return pre[i][l + K - 1] - pre[i][r - 1];
}
struct Segment_Tree
{
	long long t[100010];
	inline void build(long long x, long long l, long long r, long long *c)
	{
		if (!(l ^ r))
		{
			t[x] = c[l];
			return;
		}
		build(x << 1, l, (l + r) >> 1, c);
		build(x << 1 | 1, ((l + r) >> 1) + 1, r, c);
		t[x] = max(t[x << 1], t[x << 1 | 1]);
	}
	inline long long query(long long x, long long l, long long r, long long L, long long R)
	{
		if (L > r || l > R)
			return -1e9;
		else if (L <= l && r <= R)
			return t[x];
		return max(query(x << 1, l, (l + r) >> 1, L, R), query(x << 1 | 1, ((l + r) >> 1) + 1, r, L, R));
	}
}T1, T2;
int main()
{
	scanf("%lld %lld %lld", &n, &m, &K);
	for (long long i = 1; i <= n; i++)
		for (long long j = 1; j <= m; j++)
		{
			scanf("%lld", &a[i][j]);
			pre[i][j] = pre[i][j - 1] + a[i][j];
		}
	for (long long j = 1; j <= m - K + 1; j++)
		dp[1][j] = pre[1][j + K - 1] - pre[1][j - 1] + pre[2][j + K - 1] - pre[2][j - 1];
	for (long long j = 1; j <= m - K + 1; j++)
		mx1[1][j] = max(mx1[1][j - 1], dp[1][j]);
	for (long long j = m - K + 1; j >= 1; j--)
		mx2[1][j] = max(mx2[1][j + 1], dp[1][j]);
	for (long long i = 2; i <= n; i++)
	{
		for (long long k = 1; k <= m - K + 1; k++)
			t[k] = dp[i - 1][k] - pre[i][k + K - 1];
		T1.build(1, 1, m - K + 1, t);
		for (long long k = 1; k <= m - K + 1; k++)
			t[k] = dp[i - 1][k] + pre[i][k - 1];
		T2.build(1, 1, m - K + 1, t);
		for (long long j = 1; j <= m - K + 1; j++)
		{
			long long now = pre[i + 1][j + K - 1] - pre[i + 1][j - 1] + pre[i][j + K - 1] - pre[i][j - 1];
			dp[i][j] = max(dp[i][j], now + max(j > K ? mx1[i - 1][j - K] : 0, mx2[i - 1][j + K]));
			dp[i][j] = max(dp[i][j], T1.query(1, 1, m - K + 1, max(j - K + 1, 1ll), j) + now + pre[i][j - 1]);
			dp[i][j] = max(dp[i][j], T2.query(1, 1, m - K + 1, j, min(j + K - 1, m - K + 1)) + now - pre[i][j + K - 1]);
//			for(long long k = max(j - K + 1, 1); k <= j; k++) dp[i][j] = max(dp[i][j],dp[i - 1][k] + now - pre[i][k + K - 1] + pre[i][j - 1]);
//			for(long long k = j; k <= min(j + K - 1, m - K + 1); k++) dp[i][j] = max(dp[i][j], dp[i - 1][k] + now - pre[i][j + K - 1] + pre[i][k - 1]);
		}
		for (long long j = 1; j <= m - K + 1; j++)
			mx1[i][j] = max(mx1[i][j - 1], dp[i][j]);
		for (long long j = m - K + 1; j >= 1; j--)
			mx2[i][j] = max(mx2[i][j + 1], dp[i][j]);
	}
	for (long long i = 1; i <= m - K + 1; i++)
		ans = max(ans, dp[n][i]);
	printf("%lld", ans);
	return 0;
}

#H. Artistic Partition

题意

\(c(l,r)=\sum\limits_{i=l}^r\sum\limits_{j=i}^r[\gcd(i,j)\ge l]\)。你可以将\(1\sim n\)\(n\) 个数分成 \(k\)\(0=x_1<x_2<x_3<\cdots<x_k<x_{k+1}=n\),最小化

\[\sum_{i=1}^{k}c(x_i+1,x_{i+1}) \]

数据范围\(n,k\le 10^5,T\) 组询问, \(T\le 3\times 10^5\)

solution

一个暴力的 DP 做法是:设 \(f_{i,k}\) 表示前 \(i\) 个数分成 \(k\) 段的最小值。转移是

\[f_{i,k}=\min_{j=1}^i\{f_{j-1,k-1}+c(l,r)\} \]

边界:\(f_{i,0}=+\inf\)\(f_{0,k}=0\)

询问数很多,可以预处理出 DP 数组 \(O(1)\) 查询。

DP状态数是 \(O(n^2)\) 的,无法承受。观察 \(c(l,r)\) 的性质,发现 \(c(l,r)\ge r-l+1\),也就是说 \(f_{n,k}\ge n\)。其次,\(c(l,2l-1)=(2l-1)-l+1=l\),也就是说,若 \(n<2^k\),一定可以把 \(n\) 分成若干段,满足 \(c(l,r)=r-l+1\),即\(f_{n,k}=n(2^k>n)\)。剩下的需要考虑的 \(k\) 就是 \(O(\log n)\) 级别的了。

状态数没有太大的优化空间了,考虑优化 \(c(l,r)\) 的计算。大力推式子:

\[\begin{aligned} c(l,r)&=\sum_{i=l}^r\sum_{j=i}^r[\gcd(i,j)\ge l]\\ &=\sum_{k=l}^r\sum_{i=l}^r\sum_{j=i}^r[\gcd(i,j)= k]\\ &=\sum_{k=l}^r\sum_{i=\lceil\frac{l}{k}\rceil}^{\lfloor\frac{r}{k}\rfloor}\sum_{j=i}^{\lfloor\frac{r}{k}\rfloor}[\gcd(i,j)=1]\\ &=\sum_{k=l}^r\sum_{j=1}^{\lfloor\frac{r}{k}\rfloor}\sum_{i=1}^j[\gcd(i,j)=1]\\ &=\sum_{k=l}^r\sum_{i=1}^{\lfloor\frac{r}{k}\rfloor}\varphi(i) \end{aligned} \]

\(s(n)=\sum_{i=1}^n\varphi(i)\),即 \(\varphi\) 的前缀和。则

\[c(l,r)=\sum_{k=l}^rs(\lfloor\frac{r}{k}\rfloor) \]

预处理出 \(s(n)\) 之后即可 \(O(\sqrt{r})\) 查询。实际上,\(c(l,r)\) 的计算复杂度大概是 \(O(\sqrt{r-l})\) 的。或者可以 \(O(n\sqrt{n})\) 预处理 \(O(1)\) 查询。

考虑优化 DP 转移。根据这个 DP 分段的形式,大胆猜测 \(c(l,r)\) 满足四边形不等式,那么这样DP转移就可以使用决策单调性优化转移了。\(c(l,r)\) 满足四边形不等式的证明如下:

\(l_1<l_2<r_1<r_2\),我们需要证明 \(c(l_1,r_1)+c(l_2,r_2)\le c(l_1,r_2)+c(l_2,r_1)\)

\(f(l,r,p)=\sum_{k=l}^p(\lfloor\frac{r}{k}\rfloor)\),那么

\[\begin{aligned} c(l_1,r_2)+c(l_2,r_1)&=f(l_1,r_2,r_2)+f(l_2,r_1,r_1)\\ &=(f(l_1,r_2,l_2-1)+f(l_2,r_2,r_2))+(f(l_1,r_1,r_1)-f(l_1,r_1,l_2-1))\\ &=f(l_1,r_2,l_2-1)-f(l_1,r_1,l_2-1)+c(l_1,r_1)+c(l_2,r_2)\\ \end{aligned} \]

显然 \(f(l_1,r_2,l_2-1)\ge f(l_1,r_1,l_2-1)\),得证。

剩下的部分可以使用 \(\text{1D1D}\) 决策单调性优化 \(\text{DP}\) 的常见套路(分治/单调队列)进行。因为有 \(O(\log n)\) 层,每层的复杂度是 \(O(n\log n)\) 的,转移的时间复杂度是 \(O(n\log^2n)\) 的,加上预处理的复杂度,总时间复杂度 \(O(n\log^2n+n\sqrt{n})\) ,空间复杂度 \(O(n\sqrt{n})\)

实际上,采用分治做法,假设当前的转移区间是 \([L,R]\),当前需要寻找转移点的是 \(mid\)。首先\(O(\sqrt{r-l})\) 求出 \(c(R+1,mid)\),然后从 \(\min(mid,R)\)\(L\) 枚举转移点 \(i\)\(c(i,mid)=c(i+1,mid)+s(\lfloor\frac{mid}{i}\rfloor)\)。这个做法的时间复杂度不太好证,不过预处理跑的很快,而且代码复杂度很低。

#include <bits/stdc++.h>
#define inf 1e18
using namespace std;
long long prime[100010], pcnt, phi[100010], T, n, k, dp[100010][18];
bool vis[100010];
inline void init(long long n)
{
	phi[1] = 1;
	for (long long i = 2; i <= n; ++i)
	{
		if (!vis[i])
		{
			prime[++pcnt] = i;
			phi[i] = i - 1;
		}
		for (long long j = 1; j <= pcnt && prime[j] * i <= n; ++j)
		{
			vis[prime[j] * i] = 1;
			if (i % prime[j] == 0)
			{
				phi[i * prime[j]] = phi[i] * prime[j];
				break;
			}
			phi[i * prime[j]] = phi[i] * (prime[j] - 1);
		}
	}
	for (long long i = 1; i <= n; ++i)
		phi[i] += phi[i - 1];
}
inline long long calc(long long l, long long r)
{
	long long res = 0;
	for (long long i; l <= r; l = i + 1)
	{
		i = min(r / (r / l), r);
		res += phi[r / l] * (i - l + 1);
	}
	return res;
}
inline void query(long long l, long long r, long long L, long long R)
{
	if (l > r)
		return;
	long long mid = (l + r) >> 1, sum = calc(R + 1, mid), pos = 0, mn = inf;
	for (long long i = min(mid, R); i >= L; --i)
	{
		sum += phi[mid / i];
		if (sum + dp[i - 1][k - 1] <= mn)
		{
			mn = sum + dp[i - 1][k - 1];
			pos = i;
		}
	}
	dp[mid][k] = mn;
	query(l, mid - 1, L, pos);
	query(mid + 1, r, pos, R);
}
int main()
{
	init(100000);
	for (long long i = 1; i <= 100000; ++i)
		dp[i][1] = (i * (i + 1)) >> 1;
	for (k = 2; k <= 17; ++k)
		query(1, 100000, 1, 100000);
	scanf("%lld", &T);
	while (T--)
	{
		scanf("%lld %lld", &n, &k);
		if (k >= 20 || n < (1 << k))
		{
			printf("%lld\n", n);
			continue;
		}
		printf("%lld\n", dp[n][k]);
	}
	return 0;
}

#I. Another n-dimensional chocolate bar

看到积式,我们自然能想到用 dp 来解决此类问题。

\(f_{i,j}\) 表示前 \(b_{1,2,\cdots,i}\) 的值已经确定,后面的数的限制为 \(\prod\limits_{p=i+1}^n b_p\geq j\) 的前提下, \(\prod\limits_{p=1}^i\lfloor\dfrac{a_p}{b_p}\rfloor \times \dfrac{1}{a_p}\) 的最大值。

自然可以通过枚举 \(b_i\) 的值来转移:

\(b_i=x\) ,则有转移: \(f_{i,\lceil\frac{j}{x}\rceil}\leftarrow f_{i-1,j}\times\lfloor\dfrac{a_p}{x}\rfloor \times \dfrac{1}{a_p}\)

初始状态为: \(f_{0,k}=1\),答案为: \(f_{n,1}\times k\)

直接转移是 \(O(nk)\) 的,考虑优化。

可以发现,第二维的值是由 \(k\) 不断除以整数后向上取整得到的。

向上取整有一个性质:

\(\lceil\dfrac{k}{a}\rceil=\lfloor\dfrac{k-1}{a}\rfloor+1\)

而且这个式子可以嵌套:

\(\lceil\dfrac{\lceil\frac{k}{a}\rceil}{b}\rceil=\lfloor\dfrac{\lfloor\frac{k-1}{a}\rfloor+1-1}{b}\rfloor+1=\lfloor\dfrac{\lfloor\frac{k-1}{a}\rfloor}{b}\rfloor+1=\lfloor\dfrac{k-1}{ab}\rfloor+1\)

这样我们就可以把向上取整转化为向下取整了,用类似杜教筛的转移方式就可以得到 \(O(nk^{0.75})\) 的做法。

#include <bits/stdc++.h>
using namespace std;
long long n, k, a[110], v[7070], m, id[10000010];
bool vis[110][7070];
double f[110][7070];
int main()
{
	scanf("%lld %lld", &n, &k);
	for (long long i = 1; i <= n; ++i)
		scanf("%lld", &a[i]);
	if (k == 1)
	{
		double ans = 1.0;
		printf("%.20lf\n", ans);
		return 0;
	}
	for (long long l = 1, r; l < k; l = r + 1)
	{
		r = (k - 1) / ((k - 1) / l);
		v[++m] = (k - 1) / l;
		id[v[m]] = m;
	}
	v[++m] = 0;
	id[0] = m;
	f[0][1] = 1;
	for (long long i = 1; i <= n; ++i)
		for (long long j = 1; j <= m; ++j)
			if (f[i - 1][j])
			{
				for (long long l = 1, r; l <= v[j]; l = r + 1)
				{
					r = v[j] / (v[j] / l);
					f[i][id[v[j] / r]] = max(f[i][id[v[j] / r]], (a[i] / l) / (double)a[i] * f[i - 1][j]);
				}
				f[i][m] = max(f[i][m], (a[i] / (v[j] + 1)) / (double)a[i] * f[i - 1][j]);
			}
	printf("%.20lf", f[n][m] * k);
	return 0;
}

#J. [JRKSJ R4] kth

题意

给定一个 \([1,n]\) 的排列 \(p\) ,以任意点为起点出发,每次可以向左或向右移动一步,求经过 \(m\) 个点(即相当于走 \(m-1\) 步)后可以产生的序列中字典序第 \(k\) 小的序列。

分析

对于 \(20\%\) 的数据,有 \(m,n\leq70\) 。我们很显然可以利用 dp 来求解,设 \(dp_{i,j}\) 表示从第 \(i\) 个位置开始,走 \(j-1\) 步,经过 \(j\) 个点的方案数,则有转移方程:

\(dp_{i,j}=dp_{i-1,j-1}+dp_{i+1,j-1}\)

考虑第一步可以走到第 \(i-1\) 个位置或者第 \(i+1\) 个位置,很容易知道,这个转移方程是显然的。

我们预处理出所有方案数,利用 dfs 找出每一步应走的位置,具体方法为:

设当前位置为 \(i\) ,还需要经过 \(x\) 个点(还需走 \(x\) 步),查找第 \(k\) 小的排列。若 \(p_{i-1}<p_{i+1}\) ,令 \(j=i-1\) ,否则令 \(j=i+1\) ,此时若 \(dp_{j,x}\geq k\) ,则直接进入 \(j\) 继续搜索,反之,则令 \(k\) 减去 \(dp_{j,x}\) ,进入另一侧进行搜索。

代码

dp 部分

for(int i=1;i<=n;i++) dp[i][1]=1;
for(int j=2;j<=m;j++){
	for(int i=1;i<=n;i++){
		dp[i][j]=dp[i-1][j-1]+dp[i+1][j-1];
	}
}

dfs 部分

void dfs(int pos,int last,int num){
	if(!pos) return;
	int now=0,tmp=n;
	if(last==1){
		ans+=p[2]; ans%=mod;
		return dfs(pos-1,2,num);
	}
	if(last==n){
		ans+=p[n-1]; ans%=mod;
		return dfs(pos-1,n-1,num);
	}
    if(p[last-1]>p[last+1]){
    	if(num>dp[last+1][pos]){
    		ans+=p[last-1]; ans%=mod;
			dfs(pos-1,last-1,num-dp[last+1][pos]);
		}
		else{
			ans+=p[last+1]; ans%=mod;
			dfs(pos-1,last+1,num);
		}
	}
	else{
		if(num>dp[last-1][pos]){
			ans+=p[last+1]; ans%=mod;
			dfs(pos-1,last+1,num-dp[last-1][pos]);
		}
		else{
			ans+=p[last-1]; ans%=mod;
			dfs(pos-1,last-1,num);
		}
	}
}

优化

我们发现,这个转移的时间复杂度是 \(O(nm)\) , dfs 的时间复杂度是 \(O(m)\) ,显然无法通过本题数据范围的数据,并且, dp 的转移过程中,数据呈现指数级增长,即使采用高精度也不能在合理的空间复杂度内存储。然而, \(k\) 很小,在 \(10^{18}\) 范围内。而我们可以推断出,比 \(k\) 小的 dp 值,一定在 \(\log_2k\) 的量级左右。因此,前面的大多数步一定在取字典序最小的部分,也就是说,一直在 \(1\)\(1\) 左右两侧中较小的数中循环。所以,这样我们将 \(m\) 转化为 \(\log_2k\) 的量级,时间复杂度和空间复杂度均为 \(O(n\log k)\) ,可以拿到 \(70\) 分的成绩。

再进一步,发现在 \(i\geq j\) 时,每一步均有两种走法,即有 \(dp_{i,j}=2^{j-1}\) ,因此,考虑只转移 \(i<j\) 的部分,当 \({i\geq j}\) 时,直接返回 \(2^{j-1}\) 即可。利用记忆化搜索可以更方便实现。

最终,我们成功地把转移过程中对于 \(n,m\) 的计算均降到了 \(\log_2k\) 的量级内,可以通过本题。

时间复杂度为 \(O(n)\) (读入数据的时间复杂度)。

注意要用 __int128!!!

//码量是真的大woc
//错一次都得对着170行的代码硬调。。。。。
//差点调死我。。。。。。
#include <bits/stdc++.h>
using namespace std;
const __int128 inf = 2e18;
const __int128 mod = 1ll << 32;
signed p[20000005] = {}, unp[20000005] = {};
__int128 ans = 0, memory[1005][1005] = {}, Base[100] = {};
long long n, m, k;
inline long long read()
{
	long long x = 0, f = 1;
	char ch = getchar();
	while(ch < '0' || ch > '9')
	{
		if(ch == '-')
			f = -1;
		ch = getchar();
	}
	while(ch >= '0' && ch <= '9')
	{
		x = x * 10 + ch - '0';
		ch = getchar();
	}
	return x * f;
}
inline void write(__int128 x)
{
	if(x > 9)
		write(x / 10);
	putchar(x % 10 + 48);
}
inline __int128 ksm(__int128 x, __int128 y)
{
	__int128 res = 1;
	while(y)
	{
		if(y & 1)
			res *= x;
		x *= x;
		y >>= 1;
	}
	return res;
}
inline __int128 f(__int128 i, __int128 j)
{
	if(i <= 0 || i > n)
		return 0;
	i = min(i, n + 1 - i);
	if(i >= j && j > 70)
		return inf;
	if(i >= j)
		return Base[j - 1];
	if(memory[i][j])
		return memory[i][j];
	return memory[i][j] = min(inf, f(i - 1, j - 1) + f(i + 1, j - 1));
}
inline void dfs(__int128 pos, __int128 last, __int128 num)
{
	if(!pos)
		return;
	__int128 now = 0, tmp = n;
	if(last == 1)
	{
		ans += p[2];
		ans %= mod;
		return dfs(pos - 1, 2, num);
	}
	if(last == n)
	{
		ans += p[n - 1];
		ans %= mod;
		return dfs(pos - 1, n - 1, num);
	}
	if(p[last - 1] > p[last + 1])
		if(num > f(last + 1, pos))
		{
			ans += p[last - 1];
			ans %= mod;
			dfs(pos - 1, last - 1, num - f(last + 1, pos));
		}
		else
		{
			ans += p[last + 1];
			ans %= mod;
			dfs(pos - 1, last + 1, num);
		}
	else
		if(num > f(last - 1, pos))
		{
			ans += p[last + 1];
			ans %= mod;
			dfs(pos - 1, last + 1, num - f(last - 1, pos));
		}
		else
		{
			ans += p[last - 1];
			ans %= mod;
			dfs(pos - 1, last - 1, num);
		}
}
inline void solveK1()
{
	if (n == 1) return (void)printf("-1");
	__int128 t1 = unp[1], t2;
	if (t1 == 1) t2 = 2;
	else if (t1 == n)
		t2 = n - 1;
	else if (p[t1 + 1] > p[t1 - 1])
		t2 = t1 - 1;
	else t2 = t1 + 1;
	__int128 pt1 = p[t1], pt2 = p[t2], mpt = m / 2;
	if (m % 2 == 1) ans = ((mpt) * (pt1 + pt2) + pt1) % mod;
	else ans = ((mpt) * ((pt1 + pt2) % mod)) % mod;
	write(ans);
}
inline void solve20()
{
	__int128 tmp = 0, pos = 0;
	for (__int128 i = 1; i <= n; i++)
	{
		tmp += f(unp[i], m);
		if (tmp >= k)
		{
			ans += i;
			ans %= mod;
			pos = i;
			break;
		}
	}
	if (!pos) return (void) puts("-1");
	dfs(m - 1, unp[pos], k - tmp + f(unp[pos], m));
	write(ans);
}
inline void solve()
{
	__int128 tmp = log2(k) + 1;
	if(ksm(2, tmp) == k)
		--tmp;
	__int128 t1 = unp[1], t2;
	if(t1 == 1)
		t2 = 2;
	else if(t1 == n)
		t2 = n - 1;
	else if(p[t1 + 1] > p[t1 - 1])
		t2 = t1 - 1;
	else t2 = t1 + 1;
	ans = (((m - tmp) / 2) * (p[t1] + p[t2]) % mod + ((m - tmp) % 2) * p[t1] % mod) % mod;
	__int128 st;
	if((m - tmp) % 2)
		st = t1;
	else st = t2;
	__int128 kk = 0;
	bool flag = false;
	for(__int128 i = 1; i <= n; ++i)
		kk += f(i, tmp);
	if(kk >= k)
		flag = true;
	if(!flag)
		return (void)printf("-1");
	dfs(tmp, st, k);
	write(ans);
}
int main()
{
	Base[0] = 1;
	for(__int128 i = 1; i <= 70; ++i)
		Base[i] = Base[i - 1] * 2;
	n = read(), m = read(), k = read();
	for(__int128 i = 1; i <= n; ++i)
	{
		p[i] = read();
		unp[p[i]] = i;
	}
	if(k == 1) return solveK1(), 0;
	__int128 tmp = log2(k) + 1;
	if(ksm(2, tmp) == k)
		--tmp;
	if(m < tmp || m <= 70) return solve20(), 0;
	return solve(), 0;
}
posted @ 2023-07-14 13:40  Naitoah  阅读(75)  评论(0)    收藏  举报