【题解】CF1830 合集

CF1830A Copil Copac Draws Trees

标签:思维题 \(C^-\)

你考虑对于每一条边打上时间标记,然后在树上 DFS 的时候维护一下以 \(u\) 为根的答案即可,然后将答案合并,反正很简单,看代码就懂。

code:

#include<bits/stdc++.h>
using namespace std;
const int NN = 2e5 + 8;
int t,n;
struct Edge{
	int to,next,val;
}edge[NN << 1];
int head[NN],cnt;
void init(){
	for(int i = 1; i <= n; ++i) head[i] = -1;
	cnt = 1;
}
void add_edge(int u,int v,int w){
	edge[++cnt] = {v,head[u],w};
	head[u] = cnt;
}
int dfs(int u,int fa,int pre){
	int ans = 0;
	for(int i = head[u]; i != -1; i = edge[i].next){
		int v = edge[i].to,val = edge[i].val;
		if(v == fa) continue;
		ans = max(ans,dfs(v,u,val) + (val < pre));
	}
	return ans;
}
int main(){
	scanf("%d",&t);
	while(t--){
		scanf("%d",&n);init();
		for(int i = 1,u,v; i < n; ++i) scanf("%d%d",&u,&v),add_edge(u,v,i),add_edge(v,u,i);
		printf("%d\n",dfs(1,1,0)+1);
	}
}

CF1830B The BOSS Can Count Pairs

标签:思维题 \(B^-\)

你考虑,我们观察数据范围,发现可以是 \(O(n\sqrt n) / O(n\log n)\) 的,我们又看到乘法,便有几个大概的想法:

  • 数论分块
  • \(O(\sqrt n)\) 枚举其中一个乘数
  • 还有什么……(笔者学识浅陋,读者可以帮忙补充)

我们可以找到两种 \(O(n^2)\) 做法:

  • \(O(n^2)\) 枚举数对 \((i,j)\) 然后进行判断。(这个很好想,就是平常的暴力)
  • \(O(n^2)\) 一个 \(n\) 枚举 \(a_i\) 的值,并将 \(b_i\) 记录在桶中,另一个 \(n\) 枚举 \(j\) 并在桶中查找 \(a_i \times a_i - b_j\) (有时候换一种枚举方式 确实不失为一种打开新思路的好方法)

我们可以发现 \(a_i \times a_j\) 是不大于 \(2n\) 的,所以里面最小的数是不大于 \(\sqrt {2n}\) 的,然后我们就可以将第一维从 \(O(n)\) 变为 \(O(\sqrt n)\)

当然细节上还是需要处理一下,因为每个数对只能出现一次,所以我们让 \(a\) 小的在前面,\(a\) 大的在后面,\(a\) 相同再是按下标(说得有点玄乎?看不懂可以直接看代码,代码还是很好懂的)。

然后我们就做完了这道题。

code:

#include<bits/stdc++.h>
using namespace std;
const int NN = 2e5 + 8; 
typedef long long ll;
int T;
int n;
int a[NN],b[NN];
int cnt[NN << 1];
int main(){
	scanf("%d",&T);
	while(T--){
		ll ans = 0;
		scanf("%d",&n);
		for(int i = 1; i <= n; ++i) scanf("%d",&a[i]);
		for(int i = 1; i <= n; ++i) scanf("%d",&b[i]);
		int c = sqrt(2 * n) + 1;
		for(int j = 1; j <= c; ++j){
			for(int i = 0; i <= n; ++i) cnt[i] = 0;
			for(int i = 1; i <= n; ++i) if(a[i] == j){
				if(j*j - b[i] >= 0 && j * j - b[i] <= n) ans += cnt[j*j-b[i]];
				++cnt[b[i]];
			} 
			for(int i = 1; i <= n; ++i){
				if(a[i]*j-b[i] >= 0 && a[i] > j && a[i] * j - b[i] <= n) ans += cnt[a[i]*j-b[i]];
			}
		}
		printf("%lld\n",ans);
	}
}

CF1830C Hyperregular Bracket Strings

标签:思维题 \(B\) | 杂项 \(B^-\)

我们知道,一个长度为 \(n\) 的合法括号序列的种数就是第 \(\frac n 2\) 个卡特兰数(当然 \(n\) 是奇数答案肯定就是 \(0\)

我们可以发现一件事情,如果两个区间相互包含,那么就可以将大区间分为中间被包含的小区间的部分和外面没有被小区间覆盖的部分。

如果两个区间相交,那么就可以分为三个部分,左半部分、中间相交部分、右半部分。

以上的所有 部分 都要满足是一个合法的括号序列才能满足条件。

但是很显然,题目给出的数据肯定会有一堆相交和包含的关系,我们需要将其理清。

我们可以发现两个位置在同一个 部分 当且仅当覆盖其上的区间完全相同,那么我们可以对每个区间一个特征值,然后使用 Xor-Hash 做到将所有关系理清,然后用 \(Map/Set\) 存储每一个 Hash 值的数量,最后将所有的卡特兰数相乘即可。

code:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int NN = 6e5 + 8, MOD = 998244353;
int n,k;
int t;
ll a[NN];
ll fac[NN],inv[NN],cat[NN];
map<ll,ll> vis;

ll ksm(ll x,ll k){
	ll res = 1;
	while(k){
		if(k & 1) res = res * x % MOD;
		x = x * x % MOD;
		k >>= 1;
	}
	return res;
}

ll binom(int n,int m){
	if(n < m) return 0;
	return fac[n] * inv[m] % MOD * inv[n-m] % MOD;
}

int main(){
	mt19937 rnd(time(0));
	scanf("%d",&t);
	fac[0] = inv[0] = 1;
	for(int i = 1; i <= NN - 2; ++i) fac[i] = fac[i-1] * i % MOD;
	inv[NN - 2] = ksm(fac[NN - 2], MOD - 2);
	for(int i = NN - 2; i >= 1; --i) inv[i-1] = inv[i] * i % MOD;
	for(int i = 0; i <= NN - 2; ++i) cat[i] = binom(2 * i,i) * ksm(i + 1,MOD - 2) % MOD;
	
	while(t--){
		vis.clear();
		scanf("%d%d",&n,&k);
		for(int i = 1; i <= n; i++) a[i] = 0;
		for(ll i = 1,l, r; i <= k; i++) {
			scanf("%lld%lld",&l,&r);
			ll t = 1ll * rnd() * rnd() ^ rnd();
			a[l] ^= t, a[r + 1] ^= t; 
		}
		ll t = 1ll * rnd() * rnd() ^ rnd();
		a[1] ^= t, a[n + 1] ^= t; 
		for(int i = 1; i <= n; i++)	a[i] = a[i] ^ a[i - 1], ++vis[a[i]];
		ll ans = 1;
		for(auto i : vis) {
			int cnt = i.second;
			if (cnt % 2 == 1) ans = 0;
			else ans = ans * cat[cnt / 2] % MOD;
		}
		printf("%lld\n",ans);
	}
} 

CF1830D Mex Tree

标签:思维题 \(A^-\) | DP \(B\)

我们考虑这道题一看题就特别难受,所有路径?\(mex\) 之和?这是什么东西?

我们考虑 \(mex\) 之和其实是有一点诈骗的感觉,毕竟是 \(0\)\(1\),还比较简单。就是路径上全都是 \(1\) 的时候是 \(0\),全都是 \(0\) 的时候是 \(1\),有 \(0\)\(1\) 的时候是 \(2\)

\(n \leq 2 \times 10^5\) 显然是有点 DP 的影子,但是我们想设出一个状态还是很难的,毕竟很难规避它的后效性的问题,子树内的路径即使在子树内不优,但是有可能在外面更优。

如果真的记下当前的 只含 \(0\)、只含 \(1\)、含 \(0\)\(1\) 的路径的个数,显然空间爆炸。

你考虑正难则反,我们考虑什么时候会损失 \(mex\) 之和,显然是链全为 \(1\) 或者全为 \(0\)

我们正常 DP,设 \(f_{u,j,0/1}\) 表示以 \(u\) 为根的子树,当前点放的是 \(0/1\),和点 \(u\) 相同颜色的和 \(u\) 直接相连的点的个数为 \(j\),当前最少的损失是多少。

转移式子如下:

\[\begin{cases} f_{u,j,0}=\min (f_{u,j,0}+\min\limits_{k}\{f_{v,k,1}\},\min\limits_{k}\{f_{u,k,0}+f_{v,j-k,0}+j(j-k)\})\\ f_{u,j,1}=\min (f_{u,j,1}+\min\limits_{k}\{f_{v,k,0}\},\min\limits_{k}\{f_{u,k,1}+f_{v,j-k,1}+2j(j-k)\})\\ \end{cases} \]

  • 为什么是 \(j(j-k)\):考虑显然被影响的链的数量为 \(j(j-k)\)
  • 为什么加上了 \(\min\limits_{k}\{f_{v,k,0/1}\}\):因为左半部分计算的是当前儿子染异色,右半部分是染同色。
  • 时间复杂度?:因为显然我们通过贪心之类的 算法可以构造出一个较优的解,就是进行黑白染色,然后可以发现所有长度大于等于 \(2\) 的链都是 \(2\) 的贡献,所以最多缺少的是 \(2n\),所以实际上 \(j\) 这一维最多只需要开到 \(\sqrt {2n}\),这个时候只需要代码优秀再加上使用优秀的 c++17 即可通过,好像 luogu 上有 \(j\) 这一维只需要开到 \(300\) 的证明,有兴趣可以自己去看一看。

code:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll NN = 2e5 + 8,B = 300,INF = 1e18;
vector<ll> f[NN][2];

int n;
struct Edge{
	int to,next;
}edge[NN << 1];
int head[NN],cnt;
void init(){
	for(int i = 1; i <= n; ++i) head[i] = -1;
	cnt = 1;
}
void add_edge(int u,int v){
	edge[++cnt] = {v,head[u]};
	head[u] = cnt;
}

ll siz[NN];
ll g[2][300];
void dfs(int u,int fa){
	siz[u] = 1;
	f[u][0].resize(2);f[u][1].resize(2);
	f[u][0][0] = f[u][1][0] = INF;f[u][0][1] = 1;f[u][1][1] = 2;
	for(int i = head[u]; i != -1; i = edge[i].next){
		int v = edge[i].to;
		if(fa == v) continue;
		dfs(v,u);
		siz[u] += siz[v];
		
		ll min0 = INF,min1 = INF;
		for(int j = 1; j <= min(B,siz[v]); ++j) min0 = min(min0,f[v][0][j]),min1 = min(min1,f[v][1][j]);
		for(int j = 1; j <= min(B,siz[u]-siz[v]); ++j) g[1][j] = f[u][1][j],g[0][j] = f[u][0][j];
		f[u][0].resize(min(B,siz[u])+1);f[u][1].resize(min(B,siz[u])+1);
		
		for(int j = 1; j <= min(B,siz[u]); ++j) f[u][0][j] = f[u][1][j] = INF;
		for(ll j = 1; j <= min(B,siz[u]); ++j){
			for(int k = max(1ll,j-min(B,siz[u]-siz[v])); k <= min(j-1,min(B,siz[v])); ++k){
				f[u][0][j] = min(f[u][0][j], g[0][j-k] + f[v][0][k] + k * (j-k));
				f[u][1][j] = min(f[u][1][j], g[1][j-k] + f[v][1][k] + 2 * k * (j-k));
			}
		}
		for(int j = 1; j <= min(B,siz[u]-siz[v]); ++j){
			f[u][0][j] = min(g[0][j]+min1,f[u][0][j]);
			f[u][1][j] = min(g[1][j]+min0,f[u][1][j]);
		}
		f[v][0].clear(); f[v][0].shrink_to_fit(); f[v][1].clear(); f[v][1].shrink_to_fit();
	}
} 
int t;
int main(){
	scanf("%d",&t);
	while(t--){
		scanf("%d",&n);init();
		for(int i = 1,u,v; i < n; ++i){
			f[i][0].clear();f[i][1].clear();
			scanf("%d%d",&u,&v);
			add_edge(u,v);add_edge(v,u);
		}
		f[n][0].clear();f[n][1].clear();
		dfs(1,1);
		ll ans = INF;
		for(int i = 1; i <= min(B,siz[1]); ++i) ans = min(ans,min(f[1][0][i],f[1][1][i]));
		printf("%lld\n",1ll * n * (n+1) - ans);
	}
}

CF1830E Bully Sort

标签:思维题 \(S\) | 杂项 \(B^+\)

考虑一次交换,我们发现,被选出来的 \([i,j]\) 的区间里 \(p_i\) 一定是最大的,\(p_j\) 一定是最小的。

然后我们会发现,我们原序列的逆序对数量会减少 \(2(j-i) - 1\),而 \(\sum|p_i-i|\) 会减少 \(2(j-i)\)

那么答案就是原序列的两部分相减(神奇的性质又增加了!)。

至于我们的后半部分显然是很好维护的,而逆序对数量只需要使用三位偏序求解即可。

yes,搞定!

code:

#include<bits/stdc++.h>
using namespace std;
const int NN = 5e5 + 8;
typedef long long ll;
int n,m,tot;
int p[NN];
ll res[NN];
ll ans[NN];
struct Node{
	int t,x,y,val;
	bool operator < (const Node &A){
		return x < A.x;
	}
}q[NN << 1],Q[NN << 1];
int a[NN];
inline lowbit(int x){return x & (-x);}
void add(int x,int num){
	while(x <= n){
		a[x] += num;
		x += lowbit(x);
	}
}
int ask(int x){
	int res = 0;
	while(x > 0){
		res += a[x];
		x -= lowbit(x);
	}
	return res;
}
void solve(int l,int r){
	if(l == r) return;
	int mid = (l + r) / 2;
	solve(l,mid);solve(mid+1,r);
	
	int i = l,j = mid + 1,k = l;
	while(i <= mid && j <= r){
		if(q[i].x <= q[j].x) add(q[i].y,q[i].val),Q[k++] = q[i++];
        else ans[q[j].t] += (ask(n) - ask(q[j].y)) * q[j].val,Q[k++] = q[j++];
	}
	while(i <= mid) add(q[i].y,q[i].val),Q[k++] = q[i++];
	while(j <= r) ans[q[j].t] += (ask(n) - ask(q[j].y)) * q[j].val,Q[k++] = q[j++];
	for(int i = l; i <= mid; ++i) add(q[i].y,-q[i].val);
	
	i = mid,j = r;
	while(i >= l && j > mid){
		if(q[i].x >= q[j].x) add(q[i].y,q[i].val),--i;
        else ans[q[j].t] += ask(q[j].y-1) * q[j].val,--j;
	}
	while(i >= l) add(q[i].y,q[i].val),--i;
	while(j > mid) ans[q[j].t] += ask(q[j].y-1) * q[j].val,--j;
	for(int i = l; i <= mid; ++i) add(q[i].y,-q[i].val);
	
	for(int i = l; i <= r; ++i) q[i] = Q[i];
}
int main(){
	scanf("%d%d",&n,&m);
	for(int i = 1; i <= n; ++i){
		scanf("%d",&p[i]);
		q[++tot] = {0,i,p[i],1};
		res[0] += abs(p[i] - i);
	}
	for(int i = 1,x,y; i <= m; ++i){
		scanf("%d%d",&x,&y);
		res[i] = res[i-1];
		res[i] -= abs(p[x]-x) + abs(p[y]-y);
		q[++tot] = {i,x,p[x],-1}, q[++tot] = {i,y,p[y],-1};
        swap(p[x],p[y]);
        res[i] += abs(p[x]-x) + abs(p[y]-y);
        q[++tot]={i,x,p[x],1},q[++tot]={i,y,p[y],1};
	}
	solve(1,tot);
	for(int i = 1; i <= m; ++i){
        ans[i] += ans[i-1];
        printf("%lld\n",res[i] - ans[i]);
    }
}
posted @ 2023-09-08 20:44  ricky_lin  阅读(78)  评论(0)    收藏  举报