其他分享
首页 > 其他分享> > 【总结】JOISC 2019

【总结】JOISC 2019

作者:互联网

「JOISC 2019 Day1」考试

三维偏序模板,直接跑 cdq 分治。

#define N 200005
int n, m, b[N], t, ed[N], c[N];
inline void add(int x,int y){for(; x <= t; x += x & -x)c[x] += y;}
inline int ask(int x){int sum = 0; for(; x; x -= x & -x)sum += c[x]; return sum;}
struct node{
	int op, x, y, z;
	bool operator<(const node o)const{
		if(x != o.x)return x < o.x;
		return op > o.op;
	}
}a[N], u[N];
void solve(int l,int r){
	if(l == r)return;
	int mid = (l + r) >> 1;
	solve(l, mid), solve(mid + 1, r);
	int j = mid + 1, s = 0;
	rep(i, l, mid){
		while(j <= r && a[j].y >= a[i].y){
			if(!a[j].op)add(a[j].z, 1);
			u[++s] = a[j++];
		}
		u[++s] = a[i];
		if(a[i].op)ed[a[i].op] += ask(t) - ask(a[i].z - 1);
	}
	rep(i, mid + 1, j - 1)if(!a[i].op)add(a[i].z, -1);
	while(j <= r)u[++s] = a[j++];
	rp(i, s)a[l + i - 1] = u[i];
}
int main() {
	read(n, m);
	rp(i, n)read(a[i].x, a[i].y), b[i] = a[i].z = a[i].x + a[i].y;
	sort(b + 1, b + n + 1), t = unique(b + 1, b + n + 1) - b - 1;
	rp(i, n)a[i].z = lower_bound(b + 1, b + t + 1, a[i].z) - b;
	rp(i, m)read(a[i + n].x, a[i + n].y, a[i + n].z), a[i + n].op = i, 
		a[i + n].z = lower_bound(b + 1, b + t + 1, a[i + n].z) - b;
	sort(a + 1, a + n + m + 1);
	solve(1, n + m);
	rp(i, m)printf("%d\n", ed[i]);
	return 0;
}

「JOISC 2019 Day1」聚会

交互题,给定一棵树,每次可以询问 \((x,y,z)\),返回到三点距离之和最短的点,需要在有限次数内还原这棵树。

这题看上去就不大正经,考虑乱搞,比如随机化。

我们随机一个点为根,每次询问可以知道两个点是否在一棵子树,然后分治下去即可。但是这个做法效率很低,只有 \(17\) 分。

观察一下,发现我们询问的时候有很多情况是两个点不在一棵子树,这样的询问相当于浪费了。所以我们随机两个点 \(x, y\) 出来,然后对于其余的点依次进行一次询问,就可以知道每个点是否在 \(x,y\) 之间的链上,如果不在,可以得到是接在链上的哪个点上。这样也可以递归下去,效率足够通过这题。

mt19937 rd(time(0));
void link(int x,int y){
	if(x > y)swap(x, y);
	Bridge(x, y);
}
void solve(vector<int>a){
	if(si(a) == 1)return;
	if(si(a) == 2){link(a[0], a[1]); return;}
	shuffle(a.begin(), a.end(), rd);
	int x = a[0], y = a[1], s = si(a) - 1;
	vector<Pr>u; vector<int> p;
	u.pb(mp(x, x)), u.pb(mp(y, y)),
	p.pb(x), p.pb(y);
	rep(i, 2, s){
		int z = Query(x, y, a[i]);
		u.pb(mp(z, a[i]));
		if(z == a[i])p.pb(z);
	}
	sort(u.begin(), u.end());
	for(int i = 0; i <= s; ){
		int j = i;
		while(j <= s && u[j].fi == u[i].fi)j++;
		vector<int>c;
		rep(k, i, j - 1)c.pb(u[k].se);
		solve(c), i = j;
	}
	solve(p);
}
void Solve(int n){
	vector<int>a;
	rp(i, n)a.pb(i - 1);
	solve(a);
}

「JOISC 2019 Day1」馕

给定长度为 \(M\) 的馕,和 \(N\) 个人,第 \(i\) 个人吃馕的第 \(j\) 米可以获得 \(V_i,j\) 的收益,现在需要将长为 \(M\) 的馕分成 \(N\) 段,每个人吃一段,使得每个人获得的收益至少为这个人吃掉整个馕的收益的 \(\frac{1}{N}\),切的位置可以是分数。

这题面首先看起来就非常没有头绪,我们可以枚举一下几种可能的思路。

一个比较显然的方向是,如果依次考虑每个人,那么每个人肯定恰好吃到他总收益的 \(\frac{1}{N}\),这样对后面的人分配会更优。

然后是人类智慧的构造,我们考虑每个人的 \(N\) 等分点,使得每一段恰好是他总收益的 \(\frac{1}{N}\)。那么贪心取得第一段是所有人中 \(N\) 等分最短的一段。那么这个人被满足了,直接删去,归纳下去即可得到正确答案。

#define N 2005
int n, m, a[N][N], c[N], v[N];
struct node{
	int x, y;
	bool operator<(const node o)const{ return x * (__int128)o.y < o.x * (__int128)y; }
	bool operator>=(const node o)const{	return x * (__int128)o.y >= o.x * (__int128)y;}
}u[N][N];
signed main() {
	read(n, m);
	rp(i, n)rp(j, m)read(a[i][j]);
	rp(id, n){
		int j = 1, sum = 0, cur = 0;
		rp(i, m)sum += a[id][i];
		rp(i, m){
			while(j <= n && node{cur + a[id][i], 1} >= node{sum * j, n})
				u[id][j] = node{sum * j - cur * n + n * a[id][i] * (i - 1), n * a[id][i]}, j++;
			if(j > n)break;
			cur += a[id][i];
		}
	}
	rp(i, n){
		node cur{1, 0}; int w = 0;
		rp(j, n)if(!v[j] && u[j][i] < cur)cur = u[j][i], w = j;
		v[w] = 1, c[i] = w;
		if(i != n)printf("%lld %lld\n", cur.x, cur.y);
	}
	rp(i, n)printf("%lld ", c[i]); el;
	return 0;
}

「JOISC 2019 Day2」两个天线

\(N\) 个天线,天线高度 \(H_i\),天线 \(i\) 可以向到它距离在 \([A_i,B_i]\) 范围内的天线发送信息。多次询问区间 \([L_j,R_j]\) 内可以相互发送信息的天线的 \(|H_x- H_y|\) 的最大值。

问题是静态的,考虑离线,将所有询问按 \(R\) 排序。

我们要求 \(|H_x - H_y|\) 的最大值,可以直接去绝对值求 \(H_x - H_y\) 和 \(H_y - H_x\) 的最大值,不失一般性我们令 \(x < y\)。

那么随着询问 \(R\) 的增大,相当于每次新增一个 \(y\),将可行的 \(x\) 加入答案集合,那么 \(L\) 的限制可以通过 DS 比如线段树维护。

对于 \(y\),可能合法的 \(x\) 区间是 \([y - B_y, y - A_y]\),但是条件不充分,对于 \(x\) 还要满足 \(y \in [x + A_x, x + B_x]\),简单思考后发现 \(x\) 的限制可以通过扫描线维护。

那么我们线段树需要支持维护 \(F,G\) 序列:

1.修改 \(F_x\) 的值

2.区间操作 \((l,r,w)\),\(\forall i \in [l,r]\),\(G_i \leftarrow F_i + w\)。

3.区间查询最大值。

那么线段树维护区间 \(F,G\) 的最大值,和区间操作最大值的标记即可,时间复杂的 \(\mathcal{O}(N\log N)\)。

#define N 200005
int n, m, h[N], l[N], r[N], ed[N];
struct node{
	int l, r, id;
	bool operator<(const node o)const{return r < o.r;}
}q[N];
struct Node{int l, r, w, val, tag;}a[N << 2];
#define L a[x].l
#define R a[x].r
#define ls (x << 1)
#define rs (ls | 1)
#define S a[x].val
#define W a[x].w
#define T a[x].tag
void build(int x,int l,int r){
	L = l, R = r, T = S = W = inf_;
	if(l == r)return ;
	int mid = (l + r) >> 1;
	build(ls, l, mid), build(rs, mid + 1, r);
}
void pushup(int x,int val){cmx(W, S + val), cmx(T, val);}
void down(int x){if(T != inf_)pushup(ls, T), pushup(rs, T), T = inf_;}
void ins(int x,int pos,int op){
	if(L == R)S = (op ? op * h[L] : inf_);
	else{
		down(x); int mid = (L + R) >> 1;
		if(mid >= pos)ins(ls, pos, op);
		else ins(rs, pos, op);
		S = max(a[ls].val, a[rs].val);
	}
}
void solve(int x,int l,int r,int val){
	if(L >= l && R <= r)pushup(x, val);
	else{
		down(x); int mid = (L + R) >> 1;
		if(mid >= l)solve(ls, l, r, val);
		if(mid < r)solve(rs, l, r, val);
		W = max(a[ls].w, a[rs].w);
	}
}
int ask(int x,int l,int r){
	if(L >= l && R <= r)return W;
	down(x); int mid = (L + R) >> 1;
	if(mid >= l)return mid < r ? max(ask(ls, l, r), ask(rs, l, r)) : ask(ls, l, r);
	return ask(rs, l, r);
}
vector<int>c[N], d[N];
signed main() {
	read(n), memset(ed, ~0, sizeof(ed));
	rp(i, n)read(h[i], l[i], r[i]);
	read(m);
	rp(i, m)read(q[i].l, q[i].r), q[i].id = i;
	sort(q + 1, q + m + 1);
	build(1, 1, n);
	int j = 1;
	rp(i, n){
		go(x, c[i])ins(1, x, -1);
		go(x, d[i])ins(1, x, 0);
		int ll = max(1LL, i - r[i]), rr = i - l[i];
		if(ll <= rr)solve(1, ll, rr, h[i]);
		while(j <= m && q[j].r == i)cmx(ed[q[j].id], ask(1, q[j].l, i)), j++;
		ll = i + l[i], rr = min(n, i + r[i]);
		if(ll <= rr)c[ll].pb(i), d[rr + 1].pb(i);
	}
	build(1, 1, n), j = 1;
	rp(i, n)c[i].clear(), d[i].clear();
	rp(i, n){
		go(x, c[i])ins(1, x, 1);
		go(x, d[i])ins(1, x, 0);
		int ll = max(1LL, i - r[i]), rr = i - l[i];
		if(ll <= rr)solve(1, ll, rr, -h[i]);
		while(j <= m && q[j].r == i)cmx(ed[q[j].id], ask(1, q[j].l, i)), j++;
		ll = i + l[i], rr = min(n, i + r[i]);
		if(ll <= rr)c[ll].pb(i), d[rr + 1].pb(i);
	}
	rp(i, m)printf("%lld\n", ed[i]);
	return 0;
}

「JOISC 2019 Day2」两道料理

两道料理分别要 \(n,m\) 个操作,每个操作需要 \(t_i\) 的时间,如果它在 \(p_i\) 之前完成,就能获得 \(w_i\) 的收益。两个料理的操作顺序已经给定,你需要归并两道料理的操作,使得总收益最大。

我们求出 \(p_i\) 表示第一道料理第 \(i\) 个操作前面最多可以进行第二道料理的前 \(p_i\) 的操作,同理对于第二道料理求出 \(q_j\),那么我们可以直接二维 DP 求出答案其中 \(f_{i,j}\) 表示两道料理分别进行了 \(i,j\) 步的最大收益。

由于方程是 \(f_{i,j} \leftarrow f_{i,j - 1},f_{i-1,j}\),每次是 \(i/j\) 增加 \(1\),让人联想到走格子的过程,DP 的过程就是从 \((0,0) \to (n,m)\) 的过程。

所以将问题进行转化,如果将 \((i,p_i)\) 看成点,那么有收益当且仅当点在路径上方,同理对于 \((q_j,j)\),就在路径下方。经典操作,我们加上所有 \((i,p_i)\) 的权值,然后将其权值取反,适当调整就转化为路径下方,求出前缀和 \(s_{i,j}\) 表示点 \((i,j)\) 下面的点权和,有新的转移方程 \(f_{i,j} = \max\{f_{i - 1, j} + s_{i - 1, j},f_{i, j - 1}\}\)。

这个形式就非常优美了,转移过程相当于将 \(f_{i - 1}\) 加上 \(s_{i - 1}\) 得到 \(f_i\),然后对 \(f_i\) 取前缀最大值。这样我们维护 \(f\) 的差分数组,\(s\) 的差分数组就是点权,所以 \(s\) 中有值的就 \(n + m\) 个。取最大值可以将差分数组中 \(<0\) 的位置加到后面,然后删除该位置。用 set/map 维护是均摊 $(n + m)\log $ 的。

#define N 1000005
int n, m; LL ans;//, s[N][N];
struct node{LL t, p, w;}a[N], b[N];
vector<Pr>c[N]; set<int>s;
int main() {
	read(n, m);
	rp(i, n)read(a[i].t, a[i].p, a[i].w), a[i].t += a[i - 1].t;
	rp(i, m)read(b[i].t, b[i].p, b[i].w), b[i].t += b[i - 1].t;
	rp(i, n){
		LL t = a[i].p - a[i].t;
		int l = 0, r = m, j = ~0;
		while(l <= r){
			int mid = (l + r) >> 1;
			if(b[mid].t <= t)j = mid, l = mid + 1;
			else r = mid - 1;
		}
		ans += a[i].w; 
		if(j + 1 <= m)c[i - 1].pb(mp(j + 1, -a[i].w));
	}
	rp(i, m){
		LL t = b[i].p - b[i].t;
		int l = 0, r = n, j = ~0;
		while(l <= r){
			int mid = (l + r) >> 1;
			if(a[mid].t <= t)j = mid, l = mid + 1;
			else r = mid - 1;
		}
		if(j >= 0)c[j].pb(mp(i, b[i].w));
	}
	map<int,LL>f; LL st = 0;
	rp(i, n){
		go(x, c[i - 1]){
			if(!x.fi)st += x.se;
			else {
				f[x.fi] += x.se;
				if(f[x.fi] < 0)s.insert(x.fi);
			}
		}
		while(!s.empty()){
			int x = *s.begin(); s.erase(x);
			if(f.count(x) && f[x] < 0){
				LL w = f[x]; f.erase(x);
				auto y = f.lower_bound(x);
				if(y != f.end()){
					int z = (*y).fi;
					f[z] += w;
					if(f[z] < 0)s.insert(z);
				}
			}
		}
	}
	go(x, f)ans += x.se;
	go(x, c[n])ans += x.se;
	printf("%lld\n", ans + st);
	return 0;
}

「JOISC 2019 Day3」指定城市

给定一棵树,双向边,每条边两个方向的权值分别为 \(C_i, D_i\),多次询问 \(k\),表示选出 \(k\) 个点,依次将以每个点为根的内向树边权赋值为 \(0\),需要求出最后树的边权之和的最小值。

当 \(k=1\) 的时候,我们求出 \(w_x\) 表示以 \(x\) 为根的内向树边权和,总和减去 \(\max\{w\}\) 即为答案,\(w\) 可以用换根 DP 求得。

考虑 \(k > 1\) 的情况,有一个关键结论是询问 \(k + 1\) 的答案一定是 \(k\) 的答案基础上,加上一个点。

注意当 \(k = 1\) 的时候结论不成立!只有 \(k > 1\) 时结论成立。我开始猜了这个结论敲暴力验证了一下 \(k = 1 \to k =2\) 发现不满足条件就排除了,结果裂开。以后做这种结论题还要考虑到 corner case。

我们先考虑 \(k = 2\) 怎么做,不难直接推出如果选择两个点 \(x,y\),最大删除的边的和为 \(\dfrac{w_x + w_y + dis(x,y)}{2}\),其中 \(dij(x,y)\) 表示 \(x,y\) 之间路径的双向边权和。这个式子是直径的格式,直接二次扫描换根可以求得。

然后在这条直径的基础上增加点,就相当于将直径缩成一个点,并以之为根,每次增加一条根到叶子的路径。这是个经典问题,直接长链剖分后排序选择即可。

那么这个关键结论怎么证明呢,因为 \(k = 2\) 时选择的是直径,所以在此基础上新增一个点,不会修改原直径,因为不可能有比直径更长的路径。\(k = 1\) 就纯属只存在一个点的特例,所以不满足结论。

时间复杂度 \(\mathcal{O}(N\log N)\),瓶颈在于排序,基数排序可以优化至线性。

#define N 200005
int n, m, h[N], tot = 1; LL w[N], sum, ed[N], d[N], f[N], v[N];
struct edge{int to, nxt, val;}e[N << 1];
void add(int x,int y,int z){e[++tot].nxt = h[x], h[x] = tot, e[tot].to = y, e[tot].val = z;}
void dfs(int x,int fa){for(int i = h[x]; i; i = e[i].nxt)if(e[i].to != fa)dfs(e[i].to, x), w[x] += w[e[i].to] + e[i].val;}
void calc(int x,int fa){for(int i = h[x]; i; i = e[i].nxt)if(e[i].to != fa)w[e[i].to] = w[x] - e[i].val + e[i ^ 1].val, calc(e[i].to, x);}
void Dfs(int x,int fa){f[x] = fa; for(int i = h[x]; i; i = e[i].nxt)if(e[i].to != fa)d[e[i].to] = d[x] + e[i].val + e[i ^ 1].val, Dfs(e[i].to, x);}
vector<LL>c;
LL solve(int x,int fa){
	LL cur = 0;
	for(int i = h[x]; i; i = e[i].nxt)if(e[i].to != fa){
		LL w = e[i ^ 1].val + solve(e[i].to, x);
		if(!cur)cur = w;
		else c.pb(min(cur, w)), cmx(cur, w);
	}return cur;
}
int main() {
	read(n);
	rp(i, n - 1){
		int x, y, l, r;
		read(x, y, l, r), sum += l + r;
		add(x, y, r), add(y, x, l);
	}
	dfs(1, 0), calc(1, 0);
	rp(i, n)cmx(ed[1], w[i]);
	Dfs(1, 0); int A = 1;
	rp(i, n)if(d[i] + w[i] > d[A] + w[A])A = i;
	d[A] = 1; Dfs(A, 0); int B = 1;
	rp(i, n)if(d[i] + w[i] > d[B] + w[B])B = i;
	ed[2] = (d[B] + w[A] + w[B]) / 2; int x = B;
	while(x)v[x] = 1, x = f[x];
	x = B; while(x){
		for(int i = h[x]; i; i = e[i].nxt)if(!v[e[i].to])
			c.pb(solve(e[i].to, x) + e[i ^ 1].val);
		x = f[x];
	}
	sort(c.begin(), c.end()), reverse(c.begin(), c.end());
	int t = 2;
	go(x, c)ed[t + 1] = ed[t] + x, t++;
	while(t < n)ed[t + 1] = ed[t], t++;
	read(m); while(m--){int x; read(x); printf("%lld\n", sum - ed[x]);}
	return 0;
}

「JOISC 2019 Day3」开关游戏

给定两个 \(0/1\) 串 \(s,t\),每次可以选择一个区间进行赋值/取反操作,问最少次数将 \(s\) 变成 \(t\)。

关键结论:先取反再进行赋值操作。如果先赋值再取反,赋值的一段还是相同的,一定可以交换顺序变成先取反再赋值。

这样我们就可以直接 DP 了,\(f_{i,0/1/2,0/1}\) 表示以 \(i\) 结尾,是否有以 \(i\) 结尾的赋值操作,是否有以 \(i\) 结尾的取反操作,直接转移就是线性的了。

#define N 1000005
int n, f[N][3][2]; char s[N], t[N];
int main() {
	read(n), scanf("%s%s", s + 1, t + 1);
	memset(f, 0x3f, sizeof(f)), f[0][2][0] = 0;
	rp(i, n){
		rep(x, 0, 1){
			int y = x != (t[i] - '0');
			rep(l, 0, 2)rep(r, 0, 1)cmn(f[i][x][y], f[i - 1][l][r] + (l != x) + (y && !r));
		}
		int y = s[i] != t[i];
		rep(l, 0, 2)rep(r, 0, 1)cmn(f[i][2][y], f[i - 1][l][r] + (y && !r));
	}
	int ans = inf;
	rep(x, 0, 2)rep(y, 0, 1)cmn(ans, f[n][x][y]);
	cout << ans << endl;
	return 0;
}

「JOISC 2019 Day3」穿越时空 Bitaro

有 \(N\) 个点,相邻两点之间路径开放的时间是 \([l_i,r_i]\),经过一条路需要 \(1\) 的时间。每次操作可以让当前时间 \(-1\),多次询问第 \(x\) 秒从 \(s\) 出发,第 \(y\) 秒到 \(t\) 结束最少进行的操作次数,单点修改。

令当前时间为 \(t\),对于一条路径,就需要花费 \(\max\{0, t - r_i\}\),然后将 \(t\) 对 \(l\) 取 \(\max\),对 \(r\) 取 \(\min\)。

我们发现对于两个相邻的路径可以合并成一个新的路径,即对 \([l,r]\) 取交,如果有交集,那么可以直接看成一条新的路径。如果没有交,那么无论初始时间是什么,最后离开路径的时间是相同的,分开讨论即可。具有结合律,这样就可以用线段树维护。

#define N 300005
int n, m, ed[N], t; Pr u[N];
struct node{
	int l, r, p, w;
	void ins(int ll,int rr){
		l = ll, r = rr, w = 0;
		if(ll == rr)p = ll;
	}
};
node operator+(node x, node y){
	if(x.l < x.r){
		if(y.l == y.r){
			if(y.p >= x.r)return node{y.l, y.r, x.r, y.w};
			if(y.p <= x.l)return node{y.l, y.r, x.l, y.w + x.l - y.p};
			return node{y.l, y.r, y.p, y.w};
		}
		if(y.l >= x.r)return node{y.l, y.l, x.r, 0};
		if(y.r <= x.l)return node{y.r, y.r, x.l, x.l - y.r};
		return node{max(x.l, y.l), min(x.r, y.r), 0, 0};
	}
	if(y.l == y.r){
		return node{y.l, y.l, x.p, x.w + y.w + max(0LL, x.l - y.p)};
	}
	if(y.l >= x.r)return node{y.l, y.l, x.p, x.w};
	if(y.r <= x.l)return node{y.r, y.r, x.p, x.w + x.l - y.r};
	return node{x.l, x.r, x.p, x.w};
}
struct Node{ int l, r; node val;}a[N << 2];
#define L a[x].l
#define R a[x].r
#define ls (x << 1)
#define rs (ls | 1)
#define S a[x].val
void build(int x,int l,int r){
	L = l, R = r;
	if(l == r)S.ins(u[l].fi - l, u[l].se - l - 1);
	else{
		int mid = (l + r) >> 1;
		build(ls, l, mid), build(rs, mid + 1, r);
		S = a[ls].val + a[rs].val;
	}
}
void ins(int x,int pos,Pr w){
	if(L == R)S.ins(w.fi, w.se);
	else{
		int mid = (L + R) >> 1;
		if(mid >= pos)ins(ls, pos, w);
		else ins(rs, pos, w);
		S = a[ls].val + a[rs].val;
	}
}
node ask(int x,int l,int r){
	if(L >= l && R <= r)return S;
	int mid = (L + R) >> 1;
	if(mid >= l)return mid < r ? ask(ls, l, r) + ask(rs, l, r) : ask(ls, l, r);
	return ask(rs, l, r);
}
struct query{int op, x, y, z, k;}q[N];
signed main() {
	read(n, m);
	rp(i, n - 1)read(u[i].fi, u[i].se);
	if(n > 1)build(1, 1, n - 1);
	rp(i, m){
		int op, x, y, z, k;
		read(op, x, y, z);
		if(1 == op){
			ins(1, x, mp(y - x, z - x - 1));
			q[i] = {-1, n - x, y, z, 0};
		}
		else{
			read(k);
			if(x == z)ed[++t] = max(0LL, y - k);
			else if(x < z){
				y -= x, k -= z; node w = ask(1, x, z - 1);
				if(w.l == w.r)ed[++t] = w.w + max(0LL, y - w.p) + max(0LL, w.l - k);
				else ed[++t] = max(0LL, y - w.r) + max(0LL, max(w.l, min(y, w.r)) - k);
			}
			else{
				q[i] = {++t, n - x + 1, y, n - z + 1, k};
			}
		}
	}
	reverse(u + 1, u + n);
	if(n > 1)build(1, 1, n - 1);
	rp(i, m){
		if(-1 == q[i].op)ins(1, q[i].x, mp(q[i].y - q[i].x, q[i].z - q[i].x - 1));
		else if(q[i].op){
			int x = q[i].x, y = q[i].y, z = q[i].z, k = q[i].k;
			y -= x, k -= z; node w = ask(1, x, z - 1);
			if(w.l == w.r)ed[q[i].op] = w.w + max(0LL, y - w.p) + max(0LL, w.l - k);
			else ed[q[i].op] = max(0LL, y - w.r) + max(0LL, max(w.l, min(y, w.r)) - k);
		}
	}
	rp(i, t)printf("%lld\n", ed[i]);
	return 0;
}

「JOISC 2019 Day4」蛋糕拼接 3

给定 \(N\) 个二元组 \((V_i,C_i)\),选出 \(M\) 个排成一个环,收益是 \(\sum V_{P_i} - \sum |C_{P_i} - C_{P_{i - 1}}|\),求最大收益。

显然我们将选出的 \(M\) 个元素按 \(C\) 排序的收益是最大的,收益就是 \(\sum V - 2(C_{\max} - C_{\min})\),我们枚举 \(C\) 最小的 \(l\),和最大的 \(r\),然后从区间 \([l,r]\) 中选择最大的 \(M\) 个 \(V\) 即为答案。

这样我们用可持久化线段树可以 \(\mathcal{O}(\log)\) 的时间内求出 \(f(l,r)\) 表示固定最小的 \(l\) 和最大的 \(r\) 后的答案,再猜一个结论, \(g_r\) 表示能使 \(f_{l,r}\) 取得最大值的 \(l\),那么 \(g_r\) 不减,即答案具有决策单调性。写个 1d1d 分治即可。

#define N 200005
int rt[N], idx, n, m, b[N], t; LL ans = Inf_;
struct Node{int l, r, sz; LL sum;}a[N << 5];
void ins(int &x,int y,int l,int r,int pos,int val){
	x = ++idx, a[x] = a[y], a[x].sz++, a[x].sum += val;
	if(l == r)return;
	int mid = (l + r) >> 1;
	if(mid >= pos)ins(a[x].l, a[y].l, l, mid, pos, val);
	else ins(a[x].r, a[y].r, mid + 1, r, pos, val);
}
LL ask(int x,int y,int l,int r,int k){
	if(l == r)return k * (LL)b[l];
	int mid = (l + r) >> 1, rs = a[a[x].r].sz - a[a[y].r].sz;
	if(rs >= k)return ask(a[x].r, a[y].r, mid + 1, r, k);
	return ask(a[x].l, a[y].l, l, mid, k - rs) + a[a[x].r].sum - a[a[y].r].sum;
}
struct node{
	int w, c;
	bool operator<(const node o)const{return c < o.c;}
}u[N];
LL calc(int l,int r){
	return ask(rt[r], rt[l - 1], 1, t, m) - 2 * (u[r].c - u[l].c);
}
void solve(int l,int r,int L,int R){
	if(l > r)return;
	int x = (l + r) >> 1, rr = min(R, x - m + 1), p = 0; LL mx = Inf_;
	rep(i, L, rr){
		LL w = calc(i, x);
		if(w >= mx)mx = w, p = i;
	}
	cmx(ans, mx), solve(l, x - 1, L, p), solve(x + 1, r, p, R);
}
int main() {
	read(n, m);
	rp(i, n)read(u[i].w, u[i].c), b[i] = u[i].w;
	sort(b + 1, b + n + 1), sort(u + 1, u + n + 1);
	t = unique(b + 1, b + n + 1) - b - 1;
	rp(i, n)ins(rt[i], rt[i - 1], 1, t, lower_bound(b + 1, b + t + 1, u[i].w) - b, u[i].w);
	solve(m, n, 1, n);
	printf("%lld\n", ans);
	return 0;
}

「JOISC 2019 Day4」合并

给定一棵树,每个点有一种颜色,如果能将颜色分为两组,使得两组内的点各构成一个连通块,则不合法。问最少合并多少种颜色使得树合法。

不合法的充要条件是存在一条边,不存在颜色在边的两侧都有。

所以对于每种颜色,将其构成的虚树缩成一个点。缩完后还是一棵树,这时候所有的点两两不同,那么答案就是 \(\left\lfloor\dfrac{Leaf+1}{2}\right\rfloor\)。

#define N 500005
int n, m, f[N][20], t, d[N], s[N];
vector<int>e[N], c[N];
void dfs(int x,int fa){
	d[x] = 1 + d[f[x][0] = fa];
	rp(i, t)f[x][i] = f[f[x][i - 1]][i - 1];
	go(y, e[x])if(y != fa)dfs(y, x);
}
int lca(int x,int y){
	if(d[x] < d[y])swap(x, y);
	pre(i, t, 0)if(d[f[x][i]] >= d[y])x = f[x][i];
	if(x == y)return x;
	pre(i, t, 0)if(f[x][i] != f[y][i])x = f[x][i], y = f[y][i];
	return f[x][0];
}
void link(int x,int y){s[x]++, s[y]++, s[lca(x, y)] -= 2;}
int fa[N], w[N];
void calc(int x,int pa){
	go(y, e[x])if(y != pa)calc(y, x), s[x] += s[y];
	if(s[x])fa[x] = pa; else fa[x] = x;
}
int get(int x){return fa[x] == x ? x : fa[x] = get(fa[x]);}
int main() {
	read(n, m), t = log2(n);
	rp(i, n - 1){
		int x, y; read(x, y);
		e[x].pb(y), e[y].pb(x);
	}
	rp(i, n){
		int x; read(x);
		c[x].pb(i);
	}
	dfs(1, 0);
	rp(i, m){
		int k = si(c[i]) - 1;
		rp(j, k)link(c[i][0], c[i][j]);
	}
	calc(1, 0);
	rp(x, n)go(y, e[x]){
		int p = get(x), q = get(y);
		if(p != q)w[p] ++;
	}
	int sum = 0;
	rp(i, n)if(fa[i] == i)sum += w[i] == 1;
	printf("%d\n", (sum + 1) / 2);
	return 0;
}

「JOISC 2019 Day4」矿物

交互题,给定 \(N\) 种颜色,每种颜色恰好 \(2\) 个球,每次可以向集合中插入/删除一个球,然后得到集合中有多少种颜色。你需要在 \(10^6\) 次操作内将球两两配对 \(N\le 4.3\times 10^4\)。

首先不难想到生日悖论,每次随机向集合中加入一个球,当集合中出现相同的球时就配对,然后清空集合。这样做期望次数是 \(\mathcal{O}(N\sqrt N)\),实际得分 \(6\) 分。

这个数据范围显然是留给 \(\log\) 做法的,我们优先考虑分治。现在我们 solve(S) 表示将集合 \(S\) 中的球配对。假设一共有 \(n\) 对,取 \(m = n / 2\),将集合中的球依次加入直到有 \(m\) 对同色球,然后再扫一遍将集合中没有配对的球删去就可以得到 \(\mathcal{O}(N\log N)\) 的算法。但是常数非常大,只能得到 \(40\) 分。

想办法优化常数,如果我们在调用 solve(S) 的时候,已经将每对球中恰好一个球加入集合中,那么我们扫一遍可以同时分出两组 \(m\) 对球,并且每对球都是恰好一个在集合中。这样做每次 solve 的操作次数都是严格小于 \(|S|\) 次,但由于毒瘤出题人,仍只有 \(40\) 分,但是操作次数已经由 \(2\times 10^6\) 优化到 \(1.3\times 10^6\)。

我们发现操作多的原因在于扫的时候,如果一个球不能配对,就需要再操作一次把它放回集合。我们观察一下发现,我们不需要每对球恰好一个在集合中,我们只需要将 \(S\) 能分成的两组 \(A,B\) 使得每组中不存在相同的颜色,而这可以通过最开始扫一遍将每个球染色即可。这样每次 solve 的操作次数都是严格小于 \(\frac{3}{4}|S|\),可以得到 \(85\) 分。

由于出题人丧心病狂的卡常,最后一个点多了大概一万次操作我们注意到每次 solve 有 \(\frac{3}{4}\) 的常数,那么我们每次在中点分治并不是最优的,将分治点偏移一点,调参就过了。

#define S 86005
mt19937 rd(time(0));
int v[S], lst = 0, idx, id[S];
int Query(int);
void Answer(int,int);
int ask(int x){v[x] ^= 1; return Query(x);}
void solve(vector<int>c){
	int n = si(c);
	if(n == 2){Answer(c[0], c[1]); return ;}
	vector<int>p, q, l, r;
	go(x, c)if(id[x])p.pb(x); else q.pb(x);
	int m = max(1, (int)(n * 0.185)), s = 0;
	rep(i, 0, m - 1)lst = ask(q[i]), r.pb(q[i]);
	rep(i, m, si(q) - 1)l.pb(q[i]);
	int op = !v[q[0]];
	go(x, p){
		if(s == m)l.pb(x);
		else{
			int w = ask(x);
			if((w != lst) ^ op)l.pb(x);
			else r.pb(x), s++;
			lst = w;
		}
	}
	solve(l), solve(r);
}
void Solve(int n){
	n <<= 1;
	vector<int>a;
	rp(i, n)a.pb(i);
	shuffle(a.begin(), a.end(), rd);
	rp(i, n){
		int x = a[i - 1];
		int w = ask(x);
		if(w == lst)id[x] = 1;
		lst = w;
	}
	solve(a);
}

标签:总结,rp,int,mid,JOISC,2019,solve,read,return
来源: https://www.cnblogs.com/7KByte/p/16324655.html