[UOJ NOI Round#6 Day1] 题解

\(trip\)

\(Problem\)

给定一张 \(n\) 个点 \(m\) 条边的图,边有边权

\(H\) 要和 \(k\) 个朋友依次在这张图上会面,第 \(i\) 个朋友初始位置在 \(p_i\),小 \(H\) 一开始在 \(1\) 号点。

当他和一个或多个朋友处于同一位置时,则视为完成了会面。

\(H\) 和他的朋友们每秒钟移动的速度均为 \(1\),他想知道,倘若他和朋友按照一个商量好的策略移动,那么他和所有网友顺次会面最少需要花费多少秒?

注意,小 \(H\) 可以在边上折返,也能在边上完成会面。例如,一条长度为 \(1\) 的边连接了 \(x\)\(y\) 两点,小 \(H\) 和一个朋友于 \(t\) 时刻分别处于点 \(x\) 和点 \(y\),小 \(H\) 可以花费 \(0.5s\) 走到边中间,和朋友完成会面,再花费 \(0.5s\) 返回点 \(x\)

\(Scope\ Limitation\)

\(2\leq n\leq 10^5,n-1\leq m\leq 2\times 10^5,1\leq k\leq 20\),图无重边、自环。

\(Solution\)

注意到一个关键点转化:设 \(f(x) = max\{dis(p_i,x)\}(0\leq i\leq k)\),特殊的,我们令 \(p_0 = 1\)。其中 \(x\) 可以是图上任意一点,这个点可以在边上\(dis(u,v)\) 表示图上任意两点间的距离。而题目实际上是让我们求 \(f(x)\)最小值

证明实际上也相当简单,即考虑和某个朋友相遇后,让该朋友继续跟着小 \(H\) 移动,最后所有人一定会移动到同一个点,而我们的答案只和最后一个到达该点的人有关。

我们可以在 \(\mathcal O(knlogn)\) 的时间复杂度内处理出所有人到图上节点的距离,但这样我们仍然不能得出答案,因为最后相聚的点可能在边上。

略作思考,我们发现实际上对于一条两端点分别为 \(x\)\(y\) 的边,令一些人到达节点 \(x\),再令一些人到达节点 \(y\),分别处理出到达 \(x\)\(y\) 的人中花费时间的最值,就能简单的算出这两人怎么行动会是最优解。

如果直接枚举所有情况,时间复杂度是 \(\mathcal O(2^km+knlogn)\),能够拿到 \(85\) 分。

事实上,我们只需对所有人到达点 \(x\) 花费的时间排序,让一个前缀走 \(x\),一个后缀走 \(y\) 即可。这不难理解,因为我们只需要知道最后到达 \(x\)\(y\) 的人,前缀走 \(x\),可以确定出最后到达 \(x\) 的人,后缀走 \(y\),亦能通过预处理计算出最后到达 \(y\) 的人,且我们一定不需要前缀中的某些数,其不会让答案更优。

时间复杂度 \(\mathcal O(km + knlogn)\)

\(code\)

#include <bits/stdc++.h>
#define st first
#define nd second
#define mk make_pair
#define pii pair<int, int>
#define int long long
using namespace std;
const int N = 2e5 + 10, INF = 1e15;
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 edge{ int v, w; };
struct record{ int u, v, w; }rec[N];
struct node{
	int v, p;
	bool operator < (const node &x) const{ return v > x.v; } 
};
int n, m, k, ans;
int dis[22][N];
bool vis[N];
vector<edge> G[N];
inline void DJ(int st, int id)
{
	priority_queue<node> q;
	memset(vis, false, sizeof(vis));
	memset(dis[id], 127 / 3, sizeof(dis[id]));
	dis[id][st] = 0, q.push((node){0, st});
	while(!q.empty()){
		node u = q.top(); q.pop();
		if(vis[u.p]) continue;
		vis[u.p] = true;
		for(register edge to : G[u.p]){
			if(dis[id][u.p] + to.w < dis[id][to.v]){
				dis[id][to.v] = dis[id][u.p] + to.w;
				q.push((node){dis[id][to.v], to.v});
			}
		}
	}
}
inline void update(int l, int r, int id)
{
	if(l == -1 || r == -1) { ans = min(ans, max(l, r) * 2); return; } //全部聚集在其中一边 
	int c = abs(l - r), z = rec[id].w; //c 表示时间差,z 表示边长 
	ans = min(ans, 2 * max(l, r) + max(0ll, z - c)); //先到的一方先走 c 的距离,之后 z - c 的距离一起走
}
signed main()
{
	n = read(), m = read(), ans = INF;
	for(register int i = 1; i <= m; i++){
		int x = read(), y = read(), z = read();
		G[x].push_back((edge){y, z}), G[y].push_back((edge){x, z});
		rec[i] = (record){x, y, z};
	}
	k = read();
	for(register int i = 1, x; i <= k; i++) x = read(), DJ(x, i);
	DJ(1, k + 1);
	for(register int i = 1; i <= m; i++){
		vector<pii> vec;
		int x = rec[i].u, y = rec[i].v;
		for(register int j = 1; j <= k + 1; j++) vec.push_back(mk(dis[j][x], j));
		sort(vec.begin(), vec.end());
		int mx[22];
		mx[vec.size()] = -1;
		for(register int j = vec.size() - 1, id; j >= 0; j--)
			id = vec[j].nd, mx[j] = max(mx[j + 1], dis[id][y]);
		update(-1, mx[0], i);
		for(register int j = 0; j < (int)vec.size(); j++) update(vec[j].st, mx[j + 1], i);
	}
	printf("%lld\n", ans);
	return 0;
}

\(show\)

\(Problem\)

有一个长度为 \(n\)\(01\)\(S\),你需要计算 \(t\) 次操作后能得到多少不同的 \(01\) 串。

一次操作定义为:在串中选择两个位置插入一对 \(01\) 使得 \(0\)\(1\) 前。

\(Scope\ Limitation\)

\(1\leq n,t\leq 300\)

\(Solution\)

考虑状态压缩。

首先,进行一个转化,注意到插入依次匹配且 \(0\) 必须在 \(1\) 之前十分像括号序列,将 \(0\) 视为左括号,将 \(1\) 视为右括号。

设最后的零一串为 \(T\)\(f_{i,j}\) 表示对于 \(T\) 长度为 \(i\) 的前缀,是否能通过 \(S\) 长度为 \(j\) 的前缀加上若干个通过操作插入的 \(01\) 得到。

\(f_{i,j}\) 的转移如下:

  • \(T_{i+1} = S_{j+1}:f_{i,j}\rightarrow f_{i+1,j+1}\)
  • \(a_{i+1}\geq b_i:f_{i,j}\rightarrow f_{i+1, j}\)

第一个转移是简单的,即 \(T\)\(S\) 直接相匹配。

第二个转移相当于在这个位置插入了一个与 \(S\) 不匹配的括号,我们令 \(a_i\)\(b_i\) 分别表示 \(T\)\(S\) 作为括号序列的权值,即若 \(T_i\) 是左括号,有 \(a_i = a_{i-1} + 1\),否则,\(a_i = a_{i-1}-1\)\(b_i\) 则类似。限制条件实际上即是满足了去除 \(S\) 原有的括号,我们插入的括号必须是一个能够完全匹配的括号序列。

明白了 \(f_{i,j}\) 的转移后,考虑设计状态。

\(dp_{i,j,V}\) 表示 \(T\) 填了 \(i\) 位后,\(a_i = j\),且当前状态与 \(S\) 的匹配度压缩为 \(V\) 的方案数。其中,\(V\) 是一个二进制数,第 \(k\) 位表示 \(f_{i,k}\) 的值。

最后按照上述转移计算,最后状态中 \(V\)\(n\) 位为 \(1\)\(j = b_n\) 计入答案。

倘若直接转移,我们的状态数大概是 \(\mathcal O(2^n (n + 2\times t)^2)\),忽略一些无用状态,再进行滚动,同时用 \(\text{map}\) 记录每个状态的值,能够通过 \(45\) 分的部分。

考虑优化我们的状态。

我们发现了这样一个事实,即对于 \(V\),他对应的下两个状态的 \(V'\) 的最高位事实上只和当前 \(V\) 的最高位有关,这是显然的,于是我们只需要记录 \(V\) 的最高位。

但这样存在一个问题,我们现在记录的状态中,省略了低位,但是事实上,我们的最高位有可能无法向后更新,而低位却能。简单来说,还是 \(a_{i+1}\geq b_i\) 的问题,我们能够插入一些左括号,不断叠高 \(a\) 的值。

对此,设计一个反悔操作。

\(S\) 上预处理一个 \(pre_i\),记录最大的 \(j\) 满足 \(j < i\)\(b_{j} < b_i\)

这样做有什么好处?细想我们的转移,很显然能够转移到当前状态,一定是满足 \(a_i\ge b_i\) 的,而若不能转移,只存在一种情况,就是 \(a_i = b_i\),且我们想在 \(i+1\) 这个位置填入一个右括号。这时,我们只需要让 \(S\)\([pre_i + 1,i]\) 这段区间由操作插入负责,就可以降低 \(b_i\),从而实现目标。

同时 \([pre_i + 1,i]\) 一定是可以由插入负责的,因为按照上述定义,若 \(pre_i\) 存在,这一段区间就是一段能够完全匹配的括号序列,或者单个左括号。

这样,状态的数量的极值就变成了 \(\mathcal O(n(n+2\times t) ^ 2)\),这一定是跑不满的,足以通过此题。

\(code\)

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e3, M = 3e2 + 10, mod = 998244353;
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;
}
//括号序列的值、f 的压缩、该状态对应的数量
struct Status{ int a, v; };
char s[N];
int n, t, opt, cnt;
int b[N], pre[N], dp[2][N << 1][M];
bool vis[2][N << 1][M];
vector<Status> stu[2];
signed main()
{
	n = read(), t = read(), cin >> (s + 1), b[0] = 910;
	for(register int i = 1; i <= n; i++){
		if(s[i] == '0') b[i] = b[i - 1] + 1;
		else b[i] = b[i - 1] - 1;
	}
	for(register int i = 0; i <= n; i++){
		pre[i] = -1;
		for(register int j = i - 1; j >= 0; j--)
			if(b[j] < b[i]) { pre[i] = j; break; }
	}
	dp[opt ^ 1][910][0] = 1, stu[opt ^ 1].push_back((Status){910, 0});
	t = n + 2 * t;
	while(t--){
		//清空当前状态
		for(register Status u : stu[opt]) vis[opt][u.a][u.v] = false, dp[opt][u.a][u.v] = 0;
		stu[opt].clear();
		for(register Status u : stu[opt ^ 1]){ //枚举上一轮状态 
			Status to;
			//这一轮是左括号
			to.a = u.a + 1, to.v = -1;
			if(s[u.v + 1] == '0'){
				to.v = u.v + 1;
				(dp[opt][to.a][to.v] += dp[opt ^ 1][u.a][u.v]) %= mod;
				if(!vis[opt][to.a][to.v]) stu[opt].push_back(to), vis[opt][to.a][to.v] = true;
			}
			else{
				if(to.a >= b[u.v]){
					to.v = u.v;
					(dp[opt][to.a][to.v] += dp[opt ^ 1][u.a][u.v]) %= mod;
					if(!vis[opt][to.a][to.v]) stu[opt].push_back(to), vis[opt][to.a][to.v] = true;
				}
			}
			//若这一轮是右括号 
			to.a = u.a - 1, to.v = -1;
			if(s[u.v + 1] == '1'){
				to.v = u.v + 1;
				(dp[opt][to.a][to.v] += dp[opt ^ 1][u.a][u.v]) %= mod;
				if(!vis[opt][to.a][to.v]) stu[opt].push_back(to), vis[opt][to.a][to.v] = true;
			}
			else{
				if(to.a >= b[u.v]){
					to.v = u.v;
					(dp[opt][to.a][to.v] += dp[opt ^ 1][u.a][u.v]) %= mod;
					if(!vis[opt][to.a][to.v]) stu[opt].push_back(to), vis[opt][to.a][to.v] = true;
				}
				else{
					if(pre[u.v] != -1){
						(dp[opt][to.a][pre[u.v]] += dp[opt ^ 1][u.a][u.v]) %= mod;
						if(!vis[opt][to.a][pre[u.v]]) stu[opt].push_back((Status){to.a, pre[u.v]}), vis[opt][to.a][pre[u.v]] = true; 
					}
				}
			}
		}
		opt ^= 1, cnt++;
	}
	printf("%lld\n", dp[opt ^ 1][b[n]][n]);
	return 0;	
}
/*
2 1
00
*/

\(game\)

\(Problem\)

给定一个依次排列长度为 \(n\) 的序列,两个人将在这个序列上做一个游戏:

  • 两人依次选择序列里的数,不能选择已经被自己或对方选走的数,小 \(B\) 先手。
  • \(A\) 每次只会选择剩下的数中编号最小的那个,小 \(B\) 会自己制定策略。

给定 \(q\) 次询问,每次询问给出两个数 \(l\)\(r\),问在区间 \([l,r]\) 上进行这个游戏,小 \(B\) 选取的数和最大是多少?

\(Scope\ Limitation\)

\(1\leq n,q\leq 2\times 10^5\)

\(Solution\)

依次考虑每个数,我们发现,小 \(B\) 的选取有这样一个限制,对于前 \(i\) 个数,他最多选取 \(\lceil\frac{i}{2} \rceil\) 个。

设计状态 \(f_{i,j}\) 表示前 \(i\) 个数选取了 \(j\) 个的答案,直接转移即可,时间复杂度 \(\mathcal O(n^2q)\),期望得分 \(40\) 分。

仔细想了想这个限制,发现我们向后面加数,并不会影响到前面没有被选择的数,也就是说前面没有被选择的数依然不会被选择,也不会增加选取前面的数的机会。但他有可能可以替换掉被选取的数,因为它在更靠后的位置,这是十分显然的。

于是我们得到了一个反悔贪心,用小根堆维护,每次小根堆内的数小于可以选取的数,就选;若不能直接选,比较堆顶和当前数即可。

时间复杂度 \(\mathcal O(qnlogn)\),期望得分依旧是 \(40\) 分。

对于这种区间的多次询问,我们很自然的想到了莫队。

但是莫队有一个问题,向后加数当然是容易的,但是如何向前加数以及如何删除成了我们难以解决的问题。

向前加数能够相当于将我们上面的过程倒了过来,用一个大根堆维护没有选取的数,每向前加入一个数,如果可以选择,就选大根堆堆顶。但这样显然是错误的,有可能会出现连续选择首位两个数的情况,对于这种情况,我们很难有效地进行处理。

仔细思考发现,倘若只向前加入一个数,是十分难维护的。我们换一种思维,每次向前加入两个数,同时将它们插入到大根堆,然后直接取堆顶,就避免了这种情况。

至于删除,我们选择逃避,利用回滚莫队即可。

当由于向前操作只能偶数倍的进行,可能会和某些回答要求的范围错开,于是我们需要做两次回滚莫队,每次调节一下左端点的奇偶性。同时由于回滚需要删除,优先队列不支持删除操作,所以需要用到 \(\text{set}\),常数略大。

时间复杂度 \(\mathcal O(n\sqrt n logn)\),期望得分 \(70\) 分。

如果将 \(\text{set}\) 换成压位 \(trie\),时间复杂度将会优化为 \(\mathcal O(n\sqrt n log_w n)\)\(\mathcal O(n\sqrt n loglog n)\),期望得分 \(70\)\(100\)

我们继续优化上述解法, 考虑分治。

对于区间 \([l,mid]\),用主席树维护每个后缀选取了哪些数,对区间 \([mid + 1,r]\),用主席树维护每个前缀没有选哪些数,接下来考虑将两个区间合并。

合并区间 \([x,mid]\)\([mid + 1, y]\) 时,设区间 \([x,mid]\) 中选了的数变为没有选择的有 \(a\) 个,区间 \([mid + 1, y]\) 中选了的数变为选了的有 \(b\) 个,显然 \(a = b\),即将两个区间内的数状态做了次替换。考虑二分,二分出一个最大的 \(k\) 满足 \([x,mid]\) 中选取的数里面第 \(k\) 小的数小于 \([mid + 1, y]\) 中没有选取的数里面第 \(k\) 大的数。

复杂度 \(\mathcal O((n+q)log^2n)\)

\(code\)

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2e5 + 10, M = 42, INF = 1e15;
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 Query{ int l, r, id; }que[N];
int n, Q, lim;
int a[N], t[N], ans[N];
int tot, pre[N], suf[N], rt1[N], rt2[N];
int ls[N * M], rs[N * M], siz[N * M], sum[N * M];
vector<Query> vec[N];
inline void update(int &rt, int l, int r, int x, int c)
{
	int u = ++tot;
	ls[u] = ls[rt], rs[u] = rs[rt];
	siz[u] = siz[rt] + c, sum[u] = sum[rt] + t[x] * c, rt = u;
	if(l == r) return;
	int mid = (l + r) >> 1;
	if(x <= mid) update(ls[rt], l, mid, x, c);
	else update(rs[rt], mid + 1, r, x, c);
}
inline int Search(int k, int l, int r, int x)
{
	if(l == r) return l;
	int mid = (l + r) >> 1;
	if(x <= siz[ls[k]]) return Search(ls[k], l, mid, x);
	else return Search(rs[k], mid + 1, r, x - siz[ls[k]]);
}
inline int ask(int k, int l, int r, int x)
{
	if(l == r) return x * t[l];
	int mid = (l + r) >> 1;
	if(x <= siz[ls[k]]) return ask(ls[k], l, mid, x);
	else return ask(rs[k], mid + 1, r, x - siz[ls[k]]) + sum[ls[k]];
}
inline int Sol(int l, int r) //反悔贪心 
{
	int res = 0;
	priority_queue<int, vector<int>, greater<int> > q;
	for(register int i = l; i <= r; i++){
		if((int)q.size() < (i - l + 2) / 2) res += t[a[i]], q.push(t[a[i]]);
		else if(q.top() < t[a[i]]) res -= q.top(), res += t[a[i]], q.pop(), q.push(t[a[i]]); 
	}
	return res;
}
inline void Binary(int l, int r, vector<Query> &Qy)
{
	if(Qy.empty()) return;
	int mid = (l + r) >> 1;
	vector<Query> Ls, Rs; //左右儿子
	for(register int i = l; i <= r; i++) vec[i].clear();
	for(register Query it : Qy){
		if(it.r <= mid) Ls.push_back(it);
		else if(it.l > mid) Rs.push_back(it);
		else vec[it.l].push_back(it);
	}
	for(register int opt = 0; opt < 2; opt++){ //调节奇偶性
		int p = mid + opt;
		rt1[p] = rt2[p + 1] = 0, pre[p] = suf[p + 1] = 0, tot = 0;
		int L = INF, R = 0;
		for(register int i = p - 1; i >= l; i -= 2)
			for(register Query it : vec[i]) L = it.l, R = max(R, it.r);
		priority_queue<int> ded;
		priority_queue<int, vector<int>, greater<int>> hav;
		for(register int i = p + 1; i <= R; i++){
			rt1[i] = rt1[i - 1], pre[i] = pre[i - 1];
			if(!((i - p) % 2)){ //偶数形扩展,一次扩展两个 
				int x = i, y = i - 1;
				if(a[x] < a[y]) swap(x, y);
				pre[i] += t[a[x]], hav.push(a[x]);
				if(!hav.empty() && a[y] > hav.top())
					pre[i] -= t[hav.top()], pre[i] += t[a[y]], update(rt1[i], 1, lim, hav.top(), 1), hav.pop(), hav.push(a[y]);
				else update(rt1[i], 1, lim, a[y], 1); //维护一个没选的前缀 
			}
		}
		for(register int i = p; i >= L; i--){ //维护一个选了的后缀 
			rt2[i] = rt2[i + 1], suf[i] = suf[i + 1], ded.push(a[i]);
			if(!((p - i + 1) % 2)) update(rt2[i], 1, lim, ded.top(), 1), suf[i] += t[ded.top()], ded.pop();
		}
		for(register int i = p - 1; i >= L; i -= 2){
			for(register Query it : vec[i]){
				int x = 0, y = min((p - i + 1) / 2, (it.r - p) / 2), tem = suf[i] + pre[it.r];
				while(x < y){
					int amo = (x + y + 1) >> 1;
					if(Search(rt1[it.r], 1, lim, (it.r - p) / 2 - amo + 1) >= Search(rt2[i], 1, lim, amo)) x = amo;
					else y = amo - 1;
				}
				if(x){
					tem += sum[rt1[it.r]] - ask(rt2[i], 1, lim, x);
					if(x != (it.r - p) / 2) tem -= ask(rt1[it.r], 1, lim, (it.r - p) / 2 - x);
				}
				ans[it.id] += tem;
			}
		}
	}
	Binary(l, mid, Ls), Binary(mid + 1, r, Rs);
}
signed main()
{
	n = read(), Q = read();
	for(register int i = 1; i <= n; i++) a[i] = read(), t[i] = a[i];
	sort(t + 1, t + n + 1), lim = unique(t + 1, t + n + 1) - t - 1;
	for(register int i = 1; i <= n; i++) a[i] = lower_bound(t + 1, t + lim + 1, a[i]) - t;
	vector<Query> Qy;
	for(register int i = 1, l, r; i <= Q; i++){
		l = read(), r = read();
		if((r - l + 1) & 1) ans[i] += t[a[r]], r--;
		if(l > r) continue;
		if(r - l + 1 <= 20) ans[i] += Sol(l, r);
		else Qy.push_back((Query){l, r, i});
	}
	Binary(1, n, Qy);
	for(register int i = 1; i <= Q; i++) printf("%lld\n", ans[i]);
	return 0;
}
posted @ 2022-08-12 11:44  ╰⋛⋋⊱๑落叶๑⊰⋌⋚╯  阅读(51)  评论(0编辑  收藏  举报