ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

【总结】JOISC 2019

2022-05-29 19:34:09  阅读:214  来源: 互联网

标签:总结 rp int mid JOISC 2019 solve read return


「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

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有