【总结】JOISC2022

【总结】JOISC2022#

Day1#

T1:给定一棵树,有 m 个人,每个人从 si 出发要到 ti,每次可以指定一个人走一条边。问是否存在一种方案让每个人都到 ti,且满足任何两个人不同时出现在同一个节点,且每个人不走重复路径

分析一下,对于一个人的操作一定是连续的。如果我们先移动 x,再移动 y,然后移动 x,说明 y 挡住了 x,而 x 一定不会挡住 y。那么我们先移动 y,再移动 x 一定更优。

所以我们要求一个顺序 p,使得按顺序依次将每个人移动到终点。那么对于两个人 i,j,如果 sij 的路径上,则 i 必须在 j 前面,如果 tij 的路径上,则 i 必须在 j 后面。

经典拓扑序建图,倍增优化建图即可,时空复杂度 O((n+m)logn)

#define N 120005
#define M 4400005
int n, m, s[N], t[N], d[N], in[M], f[N][17], u[N][17], v[N][17], idx, T, dfn[N], tot, sz[N]; 
vector<int>e[N], w[M]; queue<int>q;
void dfs(int x,int fa){
	d[x] = d[f[x][0] = fa] + 1, dfn[x] = ++tot, sz[x] = 1;
	u[x][0] = ++idx, v[x][0] = ++idx;
	rp(i, T){
		f[x][i] = f[f[x][i - 1]][i - 1];
		u[x][i] = ++idx;
		w[u[x][i - 1]].pb(idx); if(f[x][i - 1])w[u[f[x][i - 1]][i - 1]].pb(idx);
		v[x][i] = ++idx;
		w[idx].pb(v[x][i - 1]); if(f[x][i - 1])w[idx].pb(v[f[x][i - 1]][i - 1]);
	}
	go(y, e[x])if(y != fa)dfs(y, x), sz[x] += sz[y];
}
void ins(int id,int x,int y){
	if(dfn[x] <= dfn[y] && dfn[x] + sz[x] > dfn[y]){
		pre(i, T, 0)if(d[f[y][i]] >= d[x])w[u[y][i]].pb(id), y = f[y][i];
	}
	else{
		x = f[x][0];
		if(d[x] < d[y])swap(x, y);
		pre(i, T, 0)if(d[f[x][i]] >= d[y])w[u[x][i]].pb(id), x = f[x][i];
		if(x == y)w[u[x][0]].pb(id);
		else{
			pre(i, T, 0)if(f[x][i] != f[y][i])
				w[u[x][i]].pb(id), w[u[y][i]].pb(id), x = f[x][i], y = f[y][i];
			w[u[x][0]].pb(id), w[u[y][1]].pb(id);
		}
	}
}
void ins_(int id,int x,int y){
	if(dfn[y] <= dfn[x] && dfn[y] + sz[y] > dfn[x]){
		pre(i, T, 0)if(d[f[x][i]] >= d[y])w[id].pb(v[x][i]), x = f[x][i];
	}
	else{
		y = f[y][0];
		if(d[x] < d[y])swap(x, y);
		pre(i, T, 0)if(d[f[x][i]] >= d[y])w[id].pb(v[x][i]), x = f[x][i];
		if(x == y)w[id].pb(v[x][0]);
		else{
			pre(i, T, 0)if(f[x][i] != f[y][i])
				w[id].pb(v[x][i]), w[id].pb(v[y][i]), x = f[x][i], y = f[y][i];
			w[id].pb(v[x][0]), w[id].pb(v[y][1]);
		}
	}
}
void solve(){
	read(n), T = log2(n), tot = 0;
	rp(i, n)e[i].clear();
	rp(i, idx)w[i].clear(), in[i] = 0;
	rp(i, n - 1){
		int x, y; read(x, y);
		e[x].pb(y), e[y].pb(x);
	}read(m), idx = m;
	dfs(1, 0);
	rp(i, m){
		int x, y; read(x, y);
		w[i].pb(u[x][0]), w[v[y][0]].pb(i);
		ins(i, x, y), ins_(i, x, y);
	}
	int sz = 0;
	rp(i, idx)go(x, w[i])in[x]++;
	rp(i, idx)if(!in[i])q.push(i);
	while(!q.empty()){
		int x = q.front(); q.pop(), sz += x <= m;
		go(y, w[x]){
			in[y]--; if(!in[y])q.push(y);
		}
	}if(sz == m)puts("Yes"); else puts("No");
}
int main() {
	int T; read(T);
	while(T--)solve();
	return 0;
}

T2:给定 n×m 的网格图,第 i 行权值为 ai,第 j 列权值为 bj,每次只能向右或者向下移动,求 (1,1)(n,m) 的最短路。

对于 Sub1 直接 NM 跑 DP即可。

对于 Sub2,对于 ai,如果前后都有数 ai,那么 i 一定用不上。所以优化后图的规模是 2000×2000,可以通过。

对于 Sub3,我们需要观察一些性质。

如果我们只经过一次转弯从 (i,j)(x,y),有两种路可以选择,分别是 (i,j)(i,y)(x,y)(i,j)(x,j)(x,y)

这两条路对应的大小分别是 (yj)ai+(xi)by(xi)bj+(yj)ax

我们比较并化简一下可以得到 axaixi>bybjyj 时选择第一条路更优,否则选择第二条路。

想必到这里已经很清楚了,我们的答案就是进行若干次这样的选择。而上面的式子显然时比较斜率,斜率越小越好。我们先对 a,b 求出下凸包,不在凸包上的一定存些斜率更小的两个点可以替代,然后合并两个凸包即可。

#define N 100005
int n, m, p[N], q[N], l, r; LL a[N], b[N];

int main() {
	read(n, m);
	rp(i, n)read(a[i]);
	rp(i, m)read(b[i]);
	p[l = 1] = q[r = 1] = 1;
	rep(i, 2, n){
		while(l > 1 && (a[i] - a[p[l]]) * (p[l] - p[l - 1]) <= (a[p[l]] - a[p[l - 1]]) * (i - p[l]))l--;
		p[++l] = i;
	}
	rep(i, 2, m){
		while(r > 1 && (b[i] - b[q[r]]) * (q[r] - q[r - 1]) <= (b[q[r]] - b[q[r - 1]]) * (i - q[r]))r--;
		q[++r] = i;
	}
	int x = 1, y = 1, s = 1, t = 1; LL ans = 0;
	while(x < n || y < m){
		if(s == l || (t != r && (a[p[s + 1]] - a[p[s]]) * (q[t + 1] - q[t]) > (b[q[t + 1]] - b[q[t]]) * (p[s + 1] - p[s])))
			ans += (q[t + 1] - q[t]) * a[x], y = q[++t];
		else ans += (p[s + 1] - p[s]) * b[y], x = p[++s];
	}
	printf("%lld\n", ans);
	return 0;
}

T3:求有多少个长度为 N 的字符串满足 M 个条件,每个条件形如 (i,j) 表示删除字符 si 后剩下串大于等于删除 sj 后剩下的串。

观察一下,我们令 ti 表示 sisi+1 的大小关系,那么限制 (i,j)(i>j) 表示区间 [i,j) 中的第一个不等号是 >(i,j)(i<j) 表示第一个不等号是 <

设计 DP,用 fi,j 表示前 i 个字符满足所有 (l,r),liir 的限制条件,以 j 结尾的方案。前缀和优化转移即可,时间复杂度 O(N|S|+MlogM)

#define N 500005
int n, m, f[N][26], up[N][26], dn[N][26];
vector<int>u[N], v[N];
multiset<int>s, t;
int main() {
	read(n, m);
	rp(i, m){
		int x, y;
		read(x, y);
		if(x < y)v[x + 1].pb(x), v[y + 1].pb(-x);
		else u[y + 1].pb(y), u[x + 1].pb(-y);
	}int ans = 26;
	rep(i, 2, n){
		go(x, u[i])
			if(x > 0)s.insert(x); else s.erase(s.find(-x));
		go(x, v[i])
			if(x > 0)t.insert(x); else t.erase(t.find(-x));
		int sl = 2, sr = 2;
		if(!s.empty())sl = *s.rbegin() + 1;
		if(!t.empty())sr = *t.rbegin() + 1;
			if(sl < i)rep(r, 0, 24)ad(f[i][r], dn[i - 1][r + 1]), su(f[i][r], dn[sl - 1][r + 1]);
			if(sr < i)rp(r, 25)ad(f[i][r], up[i - 1][r - 1]), su(f[i][r], up[sr - 1][r - 1]);
		if(s.empty())rep(r, 0, 25)ad(f[i][r], 25 - r);
		if(t.empty())rep(r, 0, 25)ad(f[i][r], r);
		rep(j, 0, 25)ad(ans, f[i][j]);
		up[i][0] = f[i][0]; rp(j, 25)up[i][j] = (up[i][j - 1] + f[i][j]) % P;
		dn[i][25] = f[i][25]; pre(j, 24, 0)dn[i][j] = (dn[i][j + 1] + f[i][j]) % P;
		rep(j, 0, 25)ad(up[i][j], up[i - 1][j]), ad(dn[i][j], dn[i - 1][j]);
	}cout << ans << endl;
	return 0;
}

Day2#

T1:有三个操作,花费 A 的代价在当前串结尾加一个字符,花费 B 的代价将当前串剪切进剪切板(注意是剪切不是复制),花费 C 的代价在当前串结尾粘贴。

我们定义 fl,r 表示区间 [l,r] 的答案,直接按题意 DP 的复杂度是 O(N4) 的。

具体转移有两种,fl,r=A+fl,r1,和枚举 k,用 s[l,k] 表示这次剪切的内容,然后贪心在 s[l,r] 中删去最多个不重复的子串 s[l,k]

用字符串哈希可以快速判断。为减少状态,我们添加一个转移 fl,r=A+fl+1,r。然后进行剪切操作当且仅当 s[l,p]=s[q,r]。这样预处理 KMP,每次跳 next 指针可以保证一定是合法的剪切。对于两个相同的子串我们只用计算一次,每跳一次 next 必然存在两个相同的子串,所以最多转移 O(N2) 次。

问题在于快速求出区间 [l,r] 中最多能选多少个不重复的 [l,p],可以预处理 gl,r,k 表示和 s[l,r] 相同的不重复的后面 2k 个子串左端点。倍增一下即可。时间复杂度 O(N2logN)

#define N 2505
typedef unsigned long long ull;
int n, nxt[N][N], ste[12][N][N]; char s[N]; LL A, B, C, f[N][N]; ull h[N], pw[N];
#define g(l, r) (h[r] - h[l - 1] * pw[r - (l) + 1])
#define M 19989997
LL g[M + 500]; ull ky[M + 500];
pair<ull, int>b[N];
int main() {
	read(n);
	scanf("%s", s + 1);
	rp(l, n){
		int j = 0;
		rep(r, l + 1, n){
			while(j && s[r] != s[l + j])j = nxt[l][l + j - 1];
			if(s[r] == s[l + j])j++;
			nxt[l][r] = j;
		}
	}
	pw[0] = 1; rp(i, n)h[i] = h[i - 1] * R + s[i], pw[i] = pw[i - 1] * R;
	rp(i, n){
		int T = 0;
		rp(j, n - i + 1)b[++T] = mp(g(j, j + i - 1), j);
		sort(b + 1, b + T + 1, [](Pr x, Pr y){return x.fi != y.fi ? x.fi < y.fi : x.se > y.se;});
		int k = 1;
		rp(j, T){
			while(k < j && b[k].fi != b[j].fi)k++;
			while(k < j && b[j].se + i <= b[k + 1].se)k++;
			if(k < j && b[j].se + i <= b[k].se)ste[0][b[j].se][i] = b[k].se;
		}
	}
	rp(k, 11)rp(j, n)rp(i, n - j + 1)ste[k][i][j] = ste[k - 1][ste[k - 1][i][j]][j];
	
	read(A, B, C); int tem = 0;
	rp(r, n)pr(l, r){
		if(l == r){f[l][r] = A; continue;}
		ull v = g(l, r), now = v % M;
		while(ky[now] && ky[now] != v)now++;
		if(ky[now]){f[l][r] = g[now]; continue;}
		ky[now] = v;
		LL cur = min(f[l + 1][r], f[l][r - 1]) + A;
		int t = (r - l + 1) / 2, k = nxt[l][r];
		while(k && k > t)k = nxt[l][l + k - 1];
		for(; k; k = nxt[l][l + k - 1]){
			int cnt = r - l + 1, sum = 2, p = l;
			pre(i, 11, 0)if(ste[i][p][k] && ste[i][p][k] + k - 1 <= r - k)p = ste[i][p][k], sum += 1 << i;
			cnt -= sum * k;
			cmn(cur, f[l][l + k - 1] + B + sum * C + cnt * A);
		}
		assert(tem <= 100000000);
		f[l][r] = g[now] = cur;
	}
	printf("%lld\n", f[1][n]);
	return 0;
}

T2:通信题,给 Alice 一颗树,Alice 将树上的点重新标号,标号区间为 [1,2N+19]。Bob 现在得知两个点的标号,告诉 Alice 20bits 的信息,Alice 反馈给 Bob X bits 的信息,Bob 要利用反馈的信息算出这两个点的距离,X 越小得分越高。

20 bits的信息显然不能直接表示两个点 x,y,所以考虑将树上的点分组,然后传组号。

不难想到树分块,令 B=10,每块大小 [10,19]。最多 1000 块正好 10 bits 可以表示一个组号。

知道组号后把两个块内子树和两个块之间的距离反馈回去。所以我们对树但 dfs 序重标号,然后将 dfs 时入栈和出栈操作作为 0/1 反馈回去。这样就可以还原出一棵树。

写的好能拿 70 分左右,满分待补。

T3:给定若干三元组 (a,b,c),求三元组的三元组 (A,B,C) 满足 Aa>Ba,CaBb>Ab,CbCc>Ac,Bc,使 Aa+Bb+Cc 最大。

考虑按 c 排序,对于每个三元组 X,将 c 更小的三元组加入集合 S。我们只用找到集合中最大的 Aa>BaAb<Bb 的三元组即可。直接 set 维护即可,时间复杂度 O(NlogN)

#define N 150005
int n, ans = ~0, sa, sb;
struct node{
	int a, b, c;
	bool operator<(const node o)const{return c < o.c;}
}a[N];
set<Pr>s;
void ins(int x,int y){
	if(x <= sa && y <= sb)return;
	if(x < sa)sb = y;
	else if(y < sb)sa = x;
	while(true){
		auto cur = s.lower_bound(mp(x + 1, 0));
		if(cur == s.end())break;
		if((*cur).se < y)cmx(sa, (*cur).fi), cmx(sb, y), s.erase(cur);
		else break;
	}
	while(true){
		auto cur = s.lower_bound(mp(x, 0));
		if(cur == s.begin())break;
		cur--; 
		if((*cur).se > y)cmx(sa, x), cmx(sb, (*cur).se), s.erase(cur);
		else break;
	}
	s.insert(mp(x, y));
	while(!s.empty()){
		auto cur = s.begin();
		if((*cur).fi <= sa && (*cur).se <= sb)s.erase(cur);
		else break;
	}
}
void calc(int l,int r){
	rep(i, l, r)if(sa > a[i].a && sb > a[i].b)cmx(ans, sa + sb + a[i].c);
	rep(i, l, r)ins(a[i].a, a[i].b);
}
int main() {
	read(n);
	rp(i, n)read(a[i].a, a[i].b, a[i].c);
	sort(a + 1, a + n + 1);
	for(int i = 1; i <= n;){
		int j = i;
		while(a[j].c == a[i].c)j++;
		calc(i, j - 1), i = j;
	}printf("%d\n", ans);
	return 0;
}

Day3#

T1:通信题,Alice 要告诉 Bob 一个 <1018 的数,Alice 给出两个等长的 0/1s,ts,t 会被归并成一个新串并告诉 Bob,Bob 要还原出这个数,归并策略是任意的(不随机)。

这题确实非常难以下手,这个归并就相当于洗扑克的叠牌,洗了后原来的信息就没了。

由于数 <1018,意味着我们需要保留 60 个有效位。

对于 s,t,考虑牺牲一个串成全另一个串。首先我们让 0/1 串等价于 1/1 串,那么我们让 t={1,1,1,1,},那么对于 t 的前缀和是 0/1

那么对于串 s,我们将每一位重复 3 遍。对于归并后的串求前缀和 u,显然 ui=3k+{1/0/1}k 是唯一的,后面的 {1/0/1}t 串带来的波动,而 k 则是有效信息,如果 k 增了说明该有效位是 1,否则是 0

所以需要 180 个 bits 能得到 80 分。满分待补。

T2: 给定一棵树,支持两个操作。1 x y z 表示到点 x 的距离 y 的点都 ×z2 x 表示查询点 x 的权值,y40

定义 fi,j 表示将点 i 子树中距离 j 的点都乘 fi,j。每次修改和询问最多和最多 40 个祖先有关。直接模拟复杂度是 nd2,差分一下可以做到 O(nd)

#define N 200005
int n, u[N], fa[N], w[N], t, P; vector<int>e[N];
void dfs(int x,int f){
	fa[x] = f; go(y, e[x])if(y != f)dfs(y, x);
}
int f[41][N], c[10];
int main() {
	read(n, P);
	rp(i, n - 1){
		int x, y; read(x, y);
		e[x].pb(y), e[y].pb(x);
	}
	srand(time(0));
	dfs(rand() % n + 1, 0);
	rp(i, n){
		read(u[i]);
		rep(j, 0, 40)f[j][i] = 1;
	}
	int T; read(T);
	while(T--){
		int op, x, y; LL z; read(op, x);
		if(1 == op){
			read(y, z);
			int d = y, r = x;
			while(r && d >= 0){
				if(fa[r] && d > 1)
					f[d][r] = f[d][r] * z % P, f[d - 1][r] = f[d - 1][r] * z % P;
				else rep(i, 0, d)f[i][r] = f[i][r] * z % P;
				d--, r = fa[r];
			}
		}
		else{
			LL cur = u[x];
			y = x; int d = 40;
			while(y && d >= 0){
				cur = cur * f[40 - d][y] % P, d--, y = fa[y];
			}printf("%lld\n", cur);
		}
	}
	return 0;
}

T3:

待补

Day4#

T1:交互题。n 种颜色,每种颜色 m 个球。n 个不同颜色的球可以组成一组。每次可以询问一个集合 S 中的球最多能组成多少组。现在要将所有球分成 m 组,最多询问 50000 次,n400,m25

从左向右一次询问区间 [1,i],第一个返回 1 的位置 x 一定是该颜色的第一次出现。

x 加入当前集合 ans,然后从后向前扫,如果当前扫到位置 i,区间 [1,i] 加上 ans 集合里的数不能组成一组,则把 i+1 加入 ans 集合。

扫一个来回后 ans 集合就是一个合法组。直接扫需要询问 nm2 次,显然是无法通过的。

我们可以优化一下,向右扫的过程我们可以直接二分答案。向左扫虽然也可以二分,但是需要二分的次数太多反而是负优化,我们类似循环展开每次向前跳 3 个位置即可。这样期望的询问次数 mlognm+nm26 勉强可以通过。

但是如果所有球是有序的会被卡成 nm23,所以我们开始前对所有标号随机打乱,这样无论数据如何对于我们都是随机均匀的。

mt19937 rd(765234);
#define maxn 10005
int p[maxn], a[maxn], b[maxn], t;
vector<int>c;
int ask(int x){
	vector<int>u = c;
	rp(i, x)u.pb(p[a[i]]);
	return Query(u);
}
void Solve(int n,int m){
	rp(i, n * m)p[i] = i, a[i] = i;
	shuffle(p + 1, p + n * m + 1, rd);
	t = n * m;
	rp(i, m){
		if(i == m){
			rp(j, t)c.pb(p[a[j]]);
			Answer(c); return ;
		}
		int l = n, r = min(t, 4000), w = 0;
		while(l <= r){
			int mid = (l + r) >> 1;
			if(ask(mid))w = mid, r = mid - 1;
			else l = mid + 1;
		}
		c.pb(p[a[w]]), a[w--] = 0;
		while(si(c) < n){
			if(w < 5){
				if(!ask(w - 1)){
					c.pb(p[a[w]]), a[w] = 0;
				}w--;
			}
			else{
				if(ask(w - 3))w -= 3;
				else{
					if(!ask(w - 1)){
						c.pb(p[a[w]]), a[w] = 0, w--;
					}
					else if(!ask(w - 2)){
						c.pb(p[a[w - 1]]), a[w - 1] = 0, w -= 2;
					}
					else c.pb(p[a[w - 2]]), a[w - 2] = 0, w -= 3;
				}
			}
		}
		Answer(c), c.clear();
		int T = 0;
		rp(j, t)if(a[j])b[++T] = a[j];
		t = T; rp(j, t)a[j] = b[j];
	}
}

T2:给定一个序列,支持单点修改和区间查询。每个数可以吞并相邻的不大于它的数,变成两数之和。每次询问区间内有多少个数可以吞下整个区间。

最大数一定可以,我们以最大值为分界,如果左边之和大于最大值,则递归统计左边,对右边同理,类似于笛卡尔树。直接做是 O(QN) 可以得到 25 分。如果没有修改操作,我们可以从区间左端点每次跳到右边第一个比自己大的位置,一直到区间最大值,并统计答案,倍增可以优化至 O(QlogN),可以得到 48 分。

其余待补。

T3:给定带权无向图,每次询问 X,表示如果所有边权变为 |Xwi| 的最小生成树。

将所有边按照边权排序,求出前缀最小生成树和后缀生成树。对于一次询问 X 相当于合并前后两颗树得到新的最小生成树。

直接做时间复杂度 O((q+m)nα(n)),只有 42 分。

qnα(n) 太大了考虑优化掉。

对于预处理的过程,从小到大每次加入一条边,如果成环则删除一条边。

如果没有删边,那么对于所有 x,都要选择当前的边。否则我们设删除的边为 p,当前边为 q,那么 p 一定是 q 对应树的路径最大的边,边权 <p 的其他边一定不在路径上,所以当 xwp+wq2 时选择 p,否则选择 q

直接做是 O(mnα(n)+q),由于每次是加一条边并删一条边,可以直接在树上找最大的边,复杂度 O(mn+q),也可以用 LCT 维护做到 O(m(logm+logn)+q)

#define N 505
#define M 100005
int n, m, fa[N];
int get(int x){return fa[x] == x ? x : fa[x] = get(fa[x]);}
struct node{
	int u, v, w;
	bool operator<(const node o)const{return w < o.w;}
}e[M], u[N], v[N];
vector<node>a;
int main() {
	read(n, m);
	rp(i, m)read(e[i].u, e[i].v, e[i].w);
	sort(e + 1, e + m + 1);
	int t = 0;
	rp(i, m){
		rp(i, n)fa[i] = i;
		fa[e[i].u] = e[i].v;
		int p = 0, dl = 0; 
		v[++p] = e[i];
		rp(i, t){
			if(dl)v[++p] = u[i];
			else{
				int x = get(u[i].u), y = get(u[i].v);
				if(x != y)fa[x] = y, v[++p] = u[i];
				else dl = i;
			}
		}
		if(!dl)a.pb(node{-1, e[i].w, 0}), a.pb(node{2, -2 * e[i].w, e[i].w});
		else {
			a.pb(node{-1, u[dl].w, (e[i].w + u[dl].w + 1) >> 1});
			a.pb(node{-1, e[i].w, (e[i].w + u[dl].w + 1) >> 1});
			a.pb(node{2, -2 * e[i].w, e[i].w});
		}
		t = p; rp(i, t)u[i] = v[i];
	}
	sort(a.begin(), a.end());
	int j = 0, sz = si(a); LL l = 0, r = 0;
	int T; read(T);
	while(T--){
		int x; read(x);
		while(j < sz && a[j].w <= x)l += a[j].u, r += a[j].v, j++;
		printf("%lld\n", l * x + r);
	}
	return 0;
}

作者:7KByte

出处:https://www.cnblogs.com/7KByte/p/16049744.html

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

posted @   7KByte  阅读(892)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
more_horiz
keyboard_arrow_up dark_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示