【香蕉OI】 chy2003 Contest 1

竞赛编程策略与技巧
本文深入探讨了三道编程竞赛题目:T1night涉及状压动态规划,T2dawn讲解了树上路径计数问题,T3light则是一道经典的区间覆盖最小费用问题。文章不仅提供了详细的解题思路,还分享了作者的实战经验和教训。

chy2003 大爷出的题。

本来这套题一眼看上去对我这种蒟蒻挺友好的,但是,反正每次只要是我自我感觉蛮良好的时候,最后成绩都挺惨的。

期望得分 240 ,实际得分 175 。

T1 night

题意

有一些 m ( m ≤ 20 ) m(m\le 20) m(m20) 位二进制数 a i a_i ai 和一个 b b b ,假如 b ∗ 2 k b*2^k b2k 在二进制下是 1 的位 a i a_i ai 都是 1,那么 a i a_i ai 可以减去 b b b

有多组询问,每次问有多少个 a i a_i ai 能够通过一些减 b ∗ 2 k b*2^k b2k 的操作得到 x x x

思路

首先确定这题是一个状压 DP。 d p [ i ] dp[i] dp[i] 表示 i i i 能由多少个数操作得到。

那么现在问题在于去重,因为有的 i i i 可能可以由 j j j 通过多次减操作得到,但是 j j j 只能对 i i i 有 1 的贡献。

先讲我的 O ( m 2 ∗ 2 m ) O(m^2*2^m) O(m22m) 的做法,非常 naive。记 d p [ i ] [ j ] dp[i][j] dp[i][j] ,在 d p [ i ] dp[i] dp[i] 的基础上多记一位 j j j 表示这个 i i i 是由 i + b ∗ 2 j i+b*2^j i+b2j 减去 b ∗ 2 j b*2^j b2j 得到的。然后卡常能卡到 2 秒 真是可惜

然后是 O ( m ∗ 2 m ) O(m*2^m) O(m2m) 做法。只需要记 d p [ i ] dp[i] dp[i] ,然后改变循环的顺序。先枚举 b ∗ 2 k b*2^k b2k 中的 k k k ,也就是强行规定先做 k k k 小的操作。那么操作的顺序问题显然就迎刃而解了。

代码

#include<bits/stdc++.h>
using namespace std;
const int M = 21, S = 1<<20;
int T, n, m, c, b, q;
int f[S];

template<class T>inline void read(T &x){
	x = 0; bool fl = 0; char c = getchar();
	while (!isdigit(c)){if (c == '-') fl = 1; c = getchar();}
	while (isdigit(c)){x = (x<<3)+(x<<1)+c-'0'; c = getchar();}
	if (fl) x = -x;
}

int main()
{
	for (read(T); T--; ){
		read(n); read(m);
		memset(f, 0, sizeof(f));
		for (int i = 1; i <= n; ++ i){
			int x; read(x);
			f[x]++;
		}
		b = 1;
		for (read(c); c--; ){
			int x; read(x);
			b |= 1<<x;
		}
		for (; b < (1<<m); b <<= 1)
			for (int i = (1<<m)-1; i >= 1; -- i)
				if ((i&b) == b)
					f[i^b] += f[i];
		for (read(q); q--; ){
			int x; read(x);
			printf("%d\n", f[x]);
		}
	}
	return 0;
}

T2 dawn

题意

有一棵树,求 k k k 条路径的排列,使这 k k k 条路径的交为 x x x y y y 的简单路径。

注意:假如选两条路径,那么 ( 1 , 2 ) , ( 1 , 3 ) (1,2),(1,3) (1,2),(1,3) ( 1 , 3 ) , ( 1 , 2 ) (1,3),(1,2) (1,3),(1,2) 是两种不同的方案,但是 ( 1 , 2 ) , ( 1 , 3 ) (1,2),(1,3) (1,2),(1,3) ( 2 , 1 ) , ( 3 , 1 ) (2,1),(3,1) (2,1),(3,1) 是两种相同的方案。

思路

相当于在两棵子树各选 k k k 个点,两两对应形成路径,答案为 ( s i z [ x ] ∗ s i z [ y ] ) k (siz[x]*siz[y])^k (siz[x]siz[y])k s i z [ x ] siz[x] siz[x] x x x 的子树大小。

然后发现假如 k k k 个点都在 x x x 的同一个儿子的子树里,那么路径的交会比 ( x , y ) (x,y) (x,y) 要长,那就把这些答案减掉。重复减的容斥一下。

最后只需要对每个点记录子树大小的 k k k 次方,所有儿子子树大小的 k k k 次方和,然后还要记录子树外的那棵子树的信息,用来特判 x , y x,y x,y 某一个是另一个的祖先。

注意

检查取模!!!

检查取模!!!

检查取模!!!

不检查 CSP 爆零活该。

代码

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1e5 + 10, M = N<<1, E = 20, K = 100 + 5, mod = 998244353;
int T, n, q;
int h[N], ecnt, nxt[M], v[M]; 
int f[N][E], dep[N];
int siz[N][K], sum[N][K], tsiz[N][K];

template<class T>inline void add(T &x, T y){x += y; if (x >= mod) x -= mod;}

template<class T>inline void read(T &x){
	x = 0; bool fl = 0; char c = getchar();
	while (!isdigit(c)){if (c == '-') fl = 1; c = getchar();}
	while (isdigit(c)){x = (x<<3)+(x<<1)+c-'0'; c = getchar();}
	if (fl) x = -x;
}

void _add(int x, int y){
	nxt[++ecnt] = h[x]; v[ecnt] = y;
	h[x] = ecnt;
}

void dfs(int u, int fa){
	dep[u] = dep[fa]+1;
	f[u][0] = fa;
	for (int i = 1; i < E; ++ i)
		f[u][i] = f[f[u][i-1]][i-1];
	siz[u][1] = 1;
	for (int i = 1; i < K; ++ i)
		sum[u][i] = 0;
	for (int i = h[u]; i; i = nxt[i])
		if (v[i] != fa){
			dfs(v[i], u);
			add(siz[u][1], siz[v[i]][1]); // 没有取模!!!
			for (int j = 1; j < K; ++ j)
				add(sum[u][j], siz[v[i]][j]); // 没有取模!!!
		}
	for (int i = 2; i < K; ++ i)
		siz[u][i] = 1LL * siz[u][i-1] * siz[u][1] % mod;
	if (u == 1) tsiz[u][1] = 0;
	else tsiz[u][1] = n-siz[u][1];
	for (int i = 2; i < K; ++ i)
		tsiz[u][i] = 1LL * tsiz[u][i-1] * tsiz[u][1] % mod;
}

int lca(int x, int y){
	if (dep[x] < dep[y]) swap(x, y);
	for (int i = E-1; i >= 0; -- i)
		if (dep[f[x][i]] >= dep[y])
			x = f[x][i];
	if (x == y) return x;
	for (int i = E-1; i >= 0; -- i)
		if (f[x][i] != f[y][i])
			x = f[x][i], y = f[y][i];
	return f[x][0];
}

int lca_son(int x, int y){
	if (dep[x] < dep[y]) swap(x, y);
	for (int i = E-1; i >= 0; -- i)
		if (dep[f[x][i]] > dep[y])
			x = f[x][i];
	return x;
}

int solve1(int x, int y, int k){
	LL ans = 0;
	add(ans, 1LL * siz[x][k] * siz[y][k] % mod);
	add(ans, mod - 1LL * sum[x][k] * siz[y][k] % mod);
	add(ans, mod - 1LL * sum[y][k] * siz[x][k] % mod);
	add(ans, 1LL * sum[x][k] * sum[y][k] % mod);
	return ans;
}

int solve2(int x, int y, int k){
	if (dep[x] > dep[y]) swap(x, y);
	LL ans = 0;
	int z = lca_son(x, y);
	int tmp = ((sum[x][k]-siz[z][k]+tsiz[x][k])%mod+mod)%mod;
	add(ans, 1LL * siz[y][k] * tsiz[z][k] % mod);
	add(ans, mod - 1LL * tmp * siz[y][k] % mod);
	add(ans, mod - 1LL * sum[y][k] * tsiz[z][k] % mod);
	add(ans, 1LL * tmp * sum[y][k] % mod);
	return ans;
}

int main()
{
	for (read(T); T--; ){
		read(n); read(q);
		ecnt = 1; memset(h, 0, sizeof(h));
		for (int i = 1; i < n; ++ i){
			int x, y;
			read(x); read(y);
			_add(x, y); _add(y, x);
		}
		dfs(1, 0);
		for (int i = 1; i <= q; ++ i){
			int k, x, y, z;
			read(k); read(x); read(y);
			z = lca(x, y);
			if (z == x || z == y) printf("%d\n", solve2(x, y, k));
			else printf("%d\n", solve1(x, y, k));
		}
	}
	return 0;
}

T3 light

题意

n n n 个格子,标号 1 1 1 n n n ,可以花费 i i i 的代价得到 i i i 号格子。有一些格子,在你花钱得到他的时候同时也会得到 [ i − k , i + k ] [i-k,i+k] [ik,i+k] 的所有格子。

现在给定 k k k 和这些特殊格子,求最小的代价得到所有 n n n 个格子。

思路

先写出 DP 方程: d p [ i ] = m i n ( d p [ j ] + w ) dp[i]=min(dp[j]+w) dp[i]=min(dp[j]+w) ,其中 w w w 号格子是特殊格子,并且满足
j ≥ w − k − 1 , i ≤ w + k j \ge w-k-1,i\le w+k jwk1,iw+k

贪心一波,对于某个 i i i ,显然会选 w + k ≤ i w+k\le i w+ki 的最小的 w w w 。然后用单调队列维护一个区间最小的 d p [ j ] dp[j] dp[j] 就完事了。

然后讲一下我的错误思路:对于每一个位置 i i i ,假如他是特殊位置,就拿他和
m i n ( d p [ j ] ) , ( i − k − 1 ≤ j < i ) min(dp[j]),(i-k-1\le j < i) min(dp[j]),(ik1j<i) 来更新 d p [ i + k ] dp[i+k] dp[i+k] 。显然这个思路漏了 d p [ j ] ( i ≤ j < i + k ) dp[j](i \le j< i+k) dp[j](ij<i+k) j j j ,也就是没有考虑两个选取的特殊位置相互覆盖的情况。

代码

#include<bits/stdc++.h>
using namespace std;
#define int long long
typedef long long LL;
const int N = 5e6 + 10;
const LL inf = 1e18 + 7;
int T, n, k;
char s[N];

namespace Solver3
{
	LL f[N];
	int que[N], h, t;
	int que1[N], h1, t1;
	void main(){
		f[0] = 0;
		que[h = t = 1] = 0;
		h1 = 1; t1 = 0;
		for (int i = 1; i <= n; ++ i){
			f[i] = f[i-1] + i;
			if (s[i] == '1') que1[++t1] = i;
			while (h1 <= t1 && que1[h1]+k < i) h1++;
			if (h1 <= t1){
				while (h <= t && que[h] < que1[h1]-k-1) h++;
				if (h <= t) f[i] = min(f[i], f[que[h]] + que1[h1]);
			}
			while (h <= t && f[que[t]] >= f[i]) t--;
			que[++t] = i;
		}
		printf("%lld\n", f[n]);
	}
}

signed main()
{
	for (scanf("%lld", &T); T--; ){
		scanf("%lld%lld%s", &n, &k, s+1);
		Solver3::main();
	}
	return 0;
}
<think>嗯,用户想解决BalticOI 2003的P1892团伙问题,需要Python的实现方法。首先,我得先理解这个问题到底要求什么。题目名称是“团伙”,可能和并查集有关,因为很多关于分组的问题都会用到并查集结构。比如朋友关系或者敌人关系的处理。 根据常见的团伙问题,通常是给定一些人的关系,比如朋友或敌人,然后确定有多少个独立的团伙。朋友的朋友是朋友,敌人的敌人可能也是朋友,或者可能有不同的规则。比如,如果A和B是朋友,B和C是朋友,那么A和C自然在一个团伙里。但如果是敌人关系,可能需要不同的处理方式,比如敌人的敌人是朋友,或者每个节点可能有多个敌人,需要维护不同的集合。 首先,我需要确认这个问题的具体描述。但由于用户没有给出详细的问题说明,我需要回忆或查找BalticOI 2003 P1892的具体内容。可能的问题结构是:每个人可能有朋友和敌人,而确定团伙的数量需要处理这两种关系。例如,朋友关系是合并集合,而敌人关系可能需要特殊的处理,比如记录敌人的集合,或者使用扩展的并查集结构,如带权并查集,其中权重表示与父节点的关系类型(朋友或敌人)。 例如,在带权并查集中,每个节点维护一个到父节点的关系权重,0表示朋友,1表示敌人。当合并两个集合时,根据不同的关系类型调整权重。但具体如何应用需要看题目规则。 另一种可能是,当两个人是敌人时,他们的敌人可能被视为朋友。例如,如果A和B是敌人,那么A的敌人(B)的敌人可能被视为A的朋友。这种情况下,可能需要将敌人的敌人合并到同一集合中。这种情况下,并查集的结构可能需要处理两倍的节点,比如每个人有两个节点,分别代表“朋友”和“敌人”的身份,然后通过合并操作来处理关系。这种方法常用于处理具有对立关系的并查集问题,比如食物链问题。 假设问题中的规则是:朋友的朋友是朋友,敌人的敌人是朋友,敌人的朋友是敌人,那么可能需要使用扩展的并查集方法,每个元素拆分为两个节点,表示同一元素的不同状态。例如,对于每个元素x,x表示其本身,x + n表示其敌人组。当x和y是朋友时,合并x和y,同时合并x+n和y+n。当x和y是敌人时,合并x和y+n,以及y和x+n。这样,敌人的敌人就会合并到同一组中。这种方法可以处理复杂的敌人和朋友关系。 现在,我需要考虑如何用Python实现这样的并查集结构。并查集的基本操作包括查找父节点和合并集合。每个节点初始时父节点是自己。查找时需要路径压缩,合并时需要按秩合并。对于扩展的并查集,每个元素需要处理两个节点,所以总共有2n的节点数。 例如,如果有n个人,那么总共有2n的节点。对于每个操作,如果是朋友关系,合并x和y,以及x+n和y+n。如果是敌人关系,合并x和y+n,以及y和x+n。这样,当查询两个节点是否属于同一团伙时,需要检查他们的根节点是否相同。但具体团伙的数量计算可能需要遍历所有原始节点(前n个),统计不同的根的数量。 现在,我需要根据这个思路来写出Python代码的结构。首先定义并查集的类,包含父节点数组和秩数组。初始化时父节点是自己,秩为0。然后实现查找和合并操作。接着处理输入的关系,根据朋友或敌人进行合并。最后统计根的数量。 但需要注意,当处理敌人的时候,可能需要进行两次合并操作,比如合并x和y的敌人组,以及y和x的敌人组。这样才能保证敌人的敌人被正确合并到同一组。 测试一下这个逻辑是否正确。例如,当x和y是敌人,那么x的敌人组(x+n)会和y合并,而y的敌人组(y+n)会和x合并。假设之后有一个z是y的敌人,那么z应该和y+n合并,也就是和x合并。这样x和z就会在同一组,满足敌人的敌人是朋友的规则。 然后,统计团伙数量的时候,只需要遍历前n个节点,找到它们的根,并统计不同的根的数量即可。因为每个元素的根可能属于同一个团伙,或者不同的,取决于合并的情况。 现在,针对具体的输入格式,假设输入第一行是n和m,n是人数,m是关系数量。接下来的m行每行是p x y,其中p是操作类型,可能是F(朋友)或E(敌人)。需要处理这些操作,然后输出团伙的数量。 例如,输入示例可能是: 5 3 E 1 2 F 4 5 E 1 3 处理这些操作后,需要确定有多少个独立的团伙。 那么,Python代码的大致步骤是: 1. 读取n和m。 2. 初始化并查集,大小为2n。 3. 对于每个关系,处理F或E的情况。 4. 最后统计前n个节点的根,使用集合去重,统计数量。 接下来编写具体的代码。例如,并查集的类: class UnionFind: def __init__(self, size): self.parent = list(range(size)) self.rank = [0] * size def find(self, x): if self.parent[x] != x: self.parent[x] = self.find(self.parent[x]) return self.parent[x] def union(self, x, y): x_root = self.find(x) y_root = self.find(y) if x_root == y_root: return if self.rank[x_root] < self.rank[y_root]: self.parent[x_root] = y_root else: self.parent[y_root] = x_root if self.rank[x_root] == self.rank[y_root]: self.rank[x_root] += 1 然后处理输入: n = int(input().split()[0]) # 假设输入第一行是n和m m = ... uf = UnionFind(2 * n) for _ in range(m): p, x, y = input().split() x = int(x) - 1 # 假设输入的编号是1-based,转换为0-based y = int(y) - 1 if p == 'F': uf.union(x, y) uf.union(x + n, y + n) else: # 'E' uf.union(x, y + n) uf.union(y, x + n) 最后统计: roots = set() for i in range(n): roots.add(uf.find(i)) print(len(roots)) 这样应该就能得到正确的结果。 现在需要验证这个逻辑是否正确。例如,对于样例输入: 比如,假设n=5,三个关系: E 1 2 → 合并1-1和2+5,合并2-11+5 → 这里可能需要注意,x和y的转换是否正确。假设n=5,那么每个元素的范围是0-4,所以敌人组是5-9。 例如,当处理E 1 2时,原来的x是0,y是1。合并0和1+5=6,合并1和0+5=5。这样,当之后遇到E 1 3时,合并0和3+5=8,同时合并3和0+5=5。这时,0的根可能连接到6,而6的根可能连接到其他节点。这可能比较复杂,但最终统计每个原始节点的根,如果他们的根被合并到同一个,则属于同一团伙。 例如,当处理E 1 2,那么1的敌人会被连接到2的敌人组,或者类似的结构。可能需要更多的测试样例来验证。 总的来说,这样的代码结构应该能够正确处理朋友和敌人的关系,并正确计算团伙的数量。因此,这个实现应该适用于BalticOI 2003的P1892问题。</think>### BalticOI 2003 P1892 团伙问题 Python 实现 #### 问题描述 在一个有 $n$ 人的群体中,关系分为两种: - `F x y`:$x$ 和 $y$ 是朋友(朋友关系具有传递性) - `E x y`:$x$ 和 $y$ 是敌人(敌人的敌人是朋友) 需要计算最终形成的独立团伙数量[^1]。 --- #### 算法思路 使用**扩展并查集**处理复杂关系: 1. **朋友关系**:直接合并 $x$ 和 $y$ 的集合。 2. **敌人关系**:将 $x$ 的敌人组与 $y$ 合并,$y$ 的敌人组与 $x$ 合并,实现“敌人的敌人是朋友”的规则。 3. **并查集设计**:为每个元素 $i$ 创建两个节点,分别表示其**自身**($i$)和**敌人组**($i+n$)。 #### 时间复杂度 - 单次并查操作:$O(\alpha(n))$(近似常数) - 总体复杂度:$O(m \alpha(n))$,$m$ 为操作次数。 --- #### Python 代码实现 ```python class UnionFind: def __init__(self, size): self.parent = list(range(size)) self.rank = [0] * size def find(self, x): if self.parent[x] != x: self.parent[x] = self.find(self.parent[x]) # 路径压缩 return self.parent[x] def union(self, x, y): x_root = self.find(x) y_root = self.find(y) if x_root == y_root: return # 按秩合并 if self.rank[x_root] < self.rank[y_root]: self.parent[x_root] = y_root else: self.parent[y_root] = x_root if self.rank[x_root] == self.rank[y_root]: self.rank[x_root] += 1 def main(): import sys input = sys.stdin.read().split() ptr = 0 n = int(input[ptr]) ptr += 1 m = int(input[ptr]) ptr += 1 uf = UnionFind(2 * n) # 前n表示自身,后n表示敌人组 for _ in range(m): p = input[ptr] x = int(input[ptr+1]) - 1 # 转换为0-based索引 y = int(input[ptr+2]) - 1 ptr += 3 if p == 'F': uf.union(x, y) uf.union(x + n, y + n) # 合并敌人组 else: # 'E' 敌人关系 uf.union(x, y + n) # x的敌人组与y合并 uf.union(y, x + n) # y的敌人组与x合并 # 统计独立团伙数量 roots = set() for i in range(n): roots.add(uf.find(i)) print(len(roots)) if __name__ == "__main__": main() ``` --- #### 示例说明 **输入**: ``` 6 4 E 1 2 F 1 3 E 3 4 F 2 5 ``` **输出**:`3` **解释**:通过合并操作,最终形成 $\{1,3,5\},\{2,4\},\{6\}$ 三个团伙。 --- ###
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值