与一个精灵手拉着手,|
2022-05-17 21:37阅读: 441评论: 1推荐: 2

[USACO22OPEN] 262144 Revisited P [题解]

262144 Revisited P

题意

一个游戏规则如下:

给定一个长度为 N 的数组 A,每一次可以选择相邻的两个数合并,合并过后将其替换为一个大于两个数最大值的数字(例如 (5,7) 可以被合并为 8)。显然,游戏会在 N1 轮后结束,此时只会剩下一个数,你的目的是尽可能让这个数

在这个数组的所有连续子段上进行游戏,输出所有连续子段上的游戏结果的和。

对于所有数据: 2N2621441Ai106

Subtask1:N300

Subtask2:N3000

Subtask3:Ai40

Subtask4:Ai 单调不降

Subtask5: 没有额外限制

分析

Subtask1

与题目 248 相似。

dp[i][j] 表示区间 [i,j] 合并能够得到的最小值,显然,有如下状态转移方程:

dp[i][j]=Aii=j

dp[i][j]=minik<jmax(dp[i][k],dp[k+1][j])+1

时间复杂度 O(N3)

Subtask2

在子任务 1 的基础上,可以通过快速找到决策点 k 使得时间复杂度优化为 O(N2logN)

具体的,找到最大的 x 使得 dp[i][x]dp[x+1][j]。之后,我们仅需要在 k{x,x+1} 中做出决策即可。

考虑这样做为什么是对的。

显然,随着 x 的逐步增大,dp[i][x] 单调不降,dp[x+1][j] 单调不增,不难将转移转化为如下图像。

具体的,使用二分查找可以快速找到决策点。

Subtask3

与题目 262144 相似。

考虑我们怎么合并,当 N2 的幂时,每次将 (2×k1,2×k) 合并,不则每次个数减半,得到的答案最大为 max ai+logN

f[i][k] 表示最大满足区间 [i,f[i][k]] 合并出 k 的右端点。则 f[i][k+1]=f[f[i][k]+1][k]

即相当于区间 [i,f[i][k]] 合并出 k,区间 [f[i][k]+1,f[f[i][k]+1][k]] 同样合并出 k,则合并两个区间,即为如上状态转移方程。

需要注意的是,我们每次求的是满足条件的最大右端点,以 f[i][k]+1 为左端点的区间合并出 k 的右端点显然最大,所以只从这个位置更新。

最后统计答案枚举 i,k 即可,时间复杂度为 O(NmaxAi)

Subtask4

对于此类连续子段贡献问题,我们可以考虑每次向右扩展一个数,再计算囊括这个数的连续子段的贡献,这个子任务也可以通过这样的方式解决。

我枚举右端点,由于 Ai 被排序,所以这些区间合并出来的值 v 必然满足 v[Ai,Ai+logi],具体证明在子任务 3 中有简单陈述。

我们将 1i 划分为多个连续子段,满足任何一个连续子段再向左扩展一个元素就会导致其值超过 Ai。我们将一个子段看成一个元素,则有 {x,Ai,Ai,AiAi} 至多只有从左往右第一个元素可能小于 Ai,考虑反正,如果有两个,则我们显然可以向左合并一个子段,以得到更优的结果。而在这里 x 实际上等价于 Ai,用 x 合并和用 Ai 来合并实际上是一样的。

假设我们想让合并出来的结果为 v,则我们需要的元素个数为 2vA[i],倍增的向右移动,即相当于枚举不同的结果,倍增统计答案每次的复杂度显然是 logN

如何维护连续子段?

每次在最后加入新的子段 [i,i],显然 [i,i] 本身即满足如上所述的向左最大性。当我们由 i1i 更新时,每次将连续的两个子段合并,合并 AiAi1 次或者直到只剩下除 [i,i] 外的一个连续子段。由于每次子段减少一半,这显然也是 logN 的复杂度。

时间复杂度 O(NlogN)

Full Credit

考虑优化 dp 过程。称一个子段是极大的,当且仅当其向左或向右扩展都会使这个子段合并出来的值增大。

引理:设 f(N) 表示大小为 N 的序列的极大连续子段的数量,则 f(N)=O(NlogN)

证明:考虑原序列的笛卡尔树,设序列的其中一个最大元素的位置为 p,则有:

f(N)f(p1)+f(Np)+C

其中 C 为原序列包含位置 p 的最大子段数量,且 CO(plog(Np))

具体证明如下:

包含位置 p 且合并值为 Ap+k 的连续子段个数 min(p,2k),限制 p 即来自所有具有固定值但具有不同左端点的区间在这里最多有 p 个,而限制 2k 是因为值从 Ap+k1Ap+k 必然会经过扩展,而这个扩展会选择向左或是向右,这里我们需要从 p 开始扩展 k 次。

k=0log2N [包含位置 p 且合并值为 Ap+k 的连续子段个数]

O(plogNp)O(logN!(p1)!(Np))

f(N)f(p1)+f(Np)+CO(log(p1)!)+O(log(Np)!)+O(logN!log(p1)!log(Np)!)O(logN)

之后,我们用并查集维护极长连续子段即可。

具体的,枚举值 v,找出答案为 v 的连续子段的个数。

set 维护第 v1 轮 的区间左端点。考虑如果当前一段连续的 111111 我们应该怎么维护。

事实上,我们发现,即使 11 是一个极长连续子段,但我们仍然不能直接将最开始的两个 1 通过并查集合并在一起,因为同样的道理,第二三两个 1 也能组成一个最长连续子段,这样做我们肯定会漏算。

所以只有从最后开始合并极长连续子段,再通过倍增记录前面每一个位置能够延伸至的最长连续子段的位置。比如,现将最后两个 1 合并,然后将第一个 1 的指针指向第二个 1,即第一个 1 和第二个 1 组成了一个最长连续子段,以此类推。那么,我们发现,当我们的倍增触碰到底部的时候,当前位置也同样可以并入最后的极长连续子段,而当当前段落合并完成后,我们才从 set 中将其删除。

但删除过后,我们再在这一段数的最后一个位置的末尾打上一个标记,当 Ai=v 时,Ai 自成一个极长连续子段,我们将这个并查集提取出来,同样放进 set,重新合在一起更新。

总之,我们能够通过这样的操作,计算出答案为 v 的连续字段的数量。

时间复杂度 O(Nlog2N)

code

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 3e5 + 10, M = 1e6 + 50;
inline int read()
{
	int s = 0, w = 1;
	char ch = getchar();
	while(ch < '0' || ch > '9') { if(ch == '-') w *= -1; ch = getchar(); }
	while(ch >= '0' && ch <= '9') s = s * 10 + ch - '0', ch = getchar();
	return s * w;
}
struct node{ //并查集 
	int fa[N], siz[N];
	inline void initial(int n){
		for(register int i = 0; i < n; i++) fa[i] = i, siz[i] = 1;
	}
	inline int find(int x) { return fa[x] == x ? x : fa[x] = find(fa[x]); }
	inline void merge(int x, int y){
		int fx = find(x), fy = find(y);
		if(fx == fy) return; //已经合并过
		if(siz[fx] > siz[fy]) swap(fx, fy);
		fa[fx] = fy, siz[fy] += siz[fx], siz[fx] = 0; 
	}
}T;
int n, ans;
int L[N], R[N], arr[N];
set<int> s; //储存极值为 v 的极长子段 
vector<int> vec[M];
inline int Get_R(int x)
{
	if(x == n) return n;
	return R[T.find(x)];
}
signed main()
{
	memset(L, -1, sizeof(L)), memset(R, -1, sizeof(R));
	n = read();
	for(register int i = 0; i < n; i++) arr[i] = read();
	for(register int i = 0; i < n; i++) vec[arr[i]].push_back(i); 
	T.initial(n);
	//M -> 最大值,值的极限 
	for(register int v = 1; v <= M - 1; v++){
		vector<int> ed, tem;
		int res = 0; //记录有多少个值为 v 的区间 
		for(register int x : s){ //遍历值为 v - 1 的极大区间的左端点 
			int r = Get_R(x); //找到右端点 + 1
			int nexr = max(r, r == n ? -1 : Get_R(r)); //倍增标记是否触底 
			if(nexr == r) ed.push_back(x); //为末端区间
			else{
				if(L[nexr] != -1) ed.push_back(x); //被标记过,需要被合并
				else L[nexr] = x, tem.push_back(nexr); //未被标记过,标记 
				res += (nexr - r) * T.siz[T.find(x)], R[T.find(x)] = nexr;
			}
		}
		for(register int x : ed){ //合并 
			s.erase(x);
			if(L[Get_R(x)] == -1) L[Get_R(x)] = x; 
			else T.merge(L[Get_R(x)], x);
		}
		for(register int x : tem) L[x] = -1; //清空标记 
		for(register int x : vec[v]){ //放入 arr[x] = v 的 x  
			++res, R[x] = (x + 1), s.insert(x);
			if(L[x] != -1) s.insert(L[x]);
			L[x] = -1; //清空标记 
		}
		ans = ans + res * v;
	}
	printf("%lld\n", ans);
	return 0;
}

Solution2

Subtask4 优化得到的方法,大致思路如下:

建立笛卡尔树,找到最大值,递归处理左右两个区间,然后再计算包含最大值的贡献。

后记

本文作者:╰⋛⋋⊱๑落叶๑⊰⋌⋚╯

本文链接:https://www.cnblogs.com/Defoliation-ldlh/p/16282513.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   ╰⋛⋋⊱๑落叶๑⊰⋌⋚╯  阅读(441)  评论(1编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起