2021CCPC上海省赛题解ABCDEGHIJK

2021CCPC上海省赛题解ABCDEGHIJK

A. 小 A 的点面论

题意

给定两相异的非零向量 ( x 1 , y 1 , z 1 ) , ( x 2 , y 2 , z 2 )    ( 0 ≤ x i , y i , z i ≤ 10 ) (x_1,y_1,z_1),(x_2,y_2,z_2)\ \ (0\leq x_i,y_i,z_i\leq 10) (x1,y1,z1),(x2,y2,z2)  (0xi,yi,zi10),求一个向量 ( x , y , z )    ( − 200 ≤ x , y , z ≤ 200 ) (x,y,z)\ \ (-200\leq x,y,z\leq 200) (x,y,z)  (200x,y,z200)垂直于这两个向量.上述 x i , y i , z i , x , y , z ∈ Z x_i,y_i,z_i,x,y,z\in\mathbb{Z} xi,yi,zi,x,y,zZ.

思路I

输出 ( x 1 , y 1 , z 1 ) × ( x 2 , y 2 , z 2 ) = ( ∣ y 1 z 1 y 2 z 2 ∣ , − ∣ x 1 z 1 x 2 z 2 ∣ , ∣ x 1 y 1 x 2 y 2 ∣ ) (x_1,y_1,z_1)\times (x_2,y_2,z_2)=\left(\begin{vmatrix}y_1 & z_1 \\ y_2&z_2\end{vmatrix},-\begin{vmatrix}x_1 & z_1 \\ x_2&z_2\end{vmatrix},\begin{vmatrix}x_1 & y_1 \\ x_2&y_2\end{vmatrix}\right) (x1,y1,z1)×(x2,y2,z2)=( y1y2z1z2 , x1x2z1z2 , x1x2y1y2 )即可.

代码I -> 2021CCPC上海省赛-A(计算几何+叉积)

void solve() {
	int x1, y1, z1, x2, y2, z2; cin >> x1 >> y1 >> z1 >> x2 >> y2 >> z2;
	
	int ans1 = y1 * z2 - y2 * z1;
	int ans2 = x2 * z1 - x1 * z2;
	int ans3 = x1 * y2 - x2 * y1;
	cout << ans1 << ' ' << ans2 << ' ' << ans3;
}

int main() {
	solve();
}

思路II

因欲求向量坐标范围不超过 200 200 200,可 O ( n 3 ) O(n^3) O(n3)暴力枚举 ( x , y , z ) (x,y,z) (x,y,z),检查它与 ( x i , y i , z i )    ( i = 1 , 2 ) (x_i,y_i,z_i)\ \ (i=1,2) (xi,yi,zi)  (i=1,2)的点积是否为 0 0 0即可.

代码II -> 2021CCPC上海省赛-A(计算几何+暴力+点积)

void solve() {
	int x1, y1, z1, x2, y2, z2; cin >> x1 >> y1 >> z1 >> x2 >> y2 >> z2;
	
	for (int x = -200; x <= 200; x++) {
		for (int y = -200; y <= 200; y++) {
			for (int z = -200; z <= 200; z++) {
				if (x1 * x + y1 * y + z1 * z == 0 && x2 * x + y2 * y + z2 * z == 0) {
					cout << x << ' ' << y << ' ' << z;
					return;
				}
			}
		}
	}
}

int main() {
	solve();
}


C. 小 A 的期末考试

题意

给定 n    ( 1 ≤ n ≤ 100 ) n\ \ (1\leq n\leq 100) n  (1n100)个同学的学号 s    ( 1 ≤ s ≤ n ) s\ \ (1\leq s\leq n) s  (1sn)和分数 a    ( 0 ≤ a ≤ 100 ) a\ \ (0\leq a\leq 100) a  (0a100)和小A的学号 m    ( 1 ≤ m ≤ n ) m\ \ (1\leq m\leq n) m  (1mn).设所有同学初始平均分为 a v g avg avg.现有操作:①若小A成绩低于 60 60 60分,将其改为 60 60 60分;②若小A外的其他同学分数 ≥ a v g \geq avg avg,将其分数 − = 2 -=2 =2,但分数不低于 0 0 0分.按学号升序输出每个同学最后的分数.

代码 -> 2021CCPC上海省赛-C(模拟)

void solve() {
	int n, m; cin >> n >> m;
	vii stus;
	double avg = 0;  // 平均分
	for (int i = 0; i < n; i++) {
		int s, a; cin >> s >> a;
		stus.push_back({ s,a });
		avg += a;
	}
	avg /= n;

	sort(all(stus));
	for (auto& [s, a] : stus) {
		if (s == m) a = max(a, 60);
		else a = max(0, a - (cmp(a, avg) >= 0 ? 2 : 0));
	}

	for (auto& [s, a] : stus) cout << a << ' ';
}

int main() {
	solve();
}


E. Zztrans 的庄园

题意

5 5 5种等级的鱼,分别用符号 D , C , B , A , S D,C,B,A,S D,C,B,A,S表示,售价分别为 16 , 24 , 54 , 80 , 10000 16,24,54,80,10000 16,24,54,80,10000元.每次钓鱼需要花 23 23 23元购买鱼饵.现给定 n    ( 1 < n ≤ 100 ) n\ \ (1<n\leq 100) n  (1<n100)种鱼的等级和调到的概率,求钓 k    ( 1 ≤ k ≤ 100 ) k\ \ (1\leq k\leq 100) k  (1k100)次收益的期望,误差不超过 1 e − 4 1\mathrm{e}-4 1e4.

思路

先求钓一次的净收益的期望,再乘 k k k.

代码 -> 2021CCPC上海省赛-E(期望)

map<char, int> prices = { {'D',16},{'C',24},{'B',54},{'A',80},{'S',10000} };

void solve() {
	int n, k; cin >> n >> k;
	double ans = 0;
	while (n--) {
		char t; double p; cin >> t >> p;
		ans += (prices[t] - 23) * p;
	}
	cout << fixed << setprecision(8) << ans * k;
}

int main() {
	solve();
}


G. 鸡哥的雕像

题意

给定一个长度为 n    ( 2 ≤ n ≤ 1 e 5 ) n\ \ (2\leq n\leq 1\mathrm{e}5) n  (2n1e5)的序列 a 1 , ⋯   , a n    ( 1 ≤ a i ≤ 1 e 9 ) a_1,\cdots,a_n\ \ (1\leq a_i\leq 1\mathrm{e}9) a1,,an  (1ai1e9).对每个 i ∈ [ 1 , n ] i\in[1,n] i[1,n],输出除了 a i a_i ai外的其他元素之积,答案对 998244353 998244353 998244353取模.

思路

注意到 998244353 < 1 e 9 998244353<1\mathrm{e}9 998244353<1e9,而 998244353 998244353 998244353在模 998244353 998244353 998244353下不存在逆元,故不能用求逆元的方式解决.

维护前缀积 p r e [ ] pre[] pre[]和后缀积 s u f [ ] suf[] suf[],则对每个 i , a n s = p r e [ i − 1 ] ∗ s u f [ i + 1 ] i,ans=pre[i-1]*suf[i+1] i,ans=pre[i1]suf[i+1].

代码 -> 2021CCPC上海省赛-G(前缀积+后缀积)

const int MAXN = 1e5 + 5;
const int MOD = 998244353;
int n;
int a[MAXN];
int pre[MAXN], suf[MAXN];  // 前缀积、后缀积

void solve() {
	cin >> n;
	pre[0] = 1;
	for (int i = 1; i <= n; i++) {
		cin >> a[i];
		pre[i] = (ll)pre[i - 1] * a[i] % MOD;
	}

	suf[n + 1] = 1;
	for (int i = n; i >= 1; i--) suf[i] = (ll)suf[i + 1] * a[i] % MOD;

	for (int i = 1; i <= n; i++) cout << (ll)pre[i - 1] * suf[i + 1] % MOD << ' ';
}

int main() {
	solve();
}


J. Alice and Bob-1

题意

n    ( 1 ≤ n ≤ 5000 ) n\ \ (1\leq n\leq 5000) n  (1n5000)个元素 a 1 , ⋯   , a n    ( − 1 e 9 ≤ a i ≤ 1 e 9 ) a_1,\cdots,a_n\ \ (-1\mathrm{e}9\leq a_i\leq 1\mathrm{e}9) a1,,an  (1e9ai1e9),Alice和Bob轮流取走一个元素,Alice先手.取完所有元素后,两人拥有的价值定义为各自取的元素之和的绝对值.设Alice和Bob拥有的价值分别为 A A A B B B,Alice希望 A − B A-B AB尽量大,Bob希望 A − B A-B AB尽量小,两人都采取最优策略,求 A − B A-B AB.

思路

贪心策略:两人轮流取当前的最大元素.

[] Alice希望 A − B A-B AB尽量大,Bob希望 A − B A-B AB尽量小,都等价于两人希望自己拥有的价值尽量大.

设Alice和Bob拥有的价值分别为 ∣ A ∣ |A| A ∣ B ∣ |B| B.

因价值有绝对值,故所有数取反不影响答案,不妨设 S = ∑ i = 1 n a i ≥ 0 \displaystyle S=\sum_{i=1}^n a_i\geq 0 S=i=1nai0

a n s = ∣ A ∣ − ∣ B ∣ = ∣ A ∣ − ∣ S − A ∣ = { S , A ≥ S 2 A − S , 0 < A < S − S , A ≤ 0 ans=|A|-|B|=|A|-|S-A|=\begin{cases}S,A\geq S \\ 2A-S,0<A<S \\ -S,A\leq 0\end{cases} ans=AB=ASA= S,AS2AS,0<A<SS,A0.作图知:该分段函数单调增,故证.

先将 a [ ] a[] a[]升序排列.因集合元素可能全正、全负、有正有负,但答案都能归结为两种情况:①最大值被Alice取走,使得 a n s ans ans去掉绝对值后正得更多;②最大值被Bob取走,使得 a n s ans ans去掉绝对值后负得更多,两种情况取 max ⁡ \max max即可.

代码 -> 2021CCPC上海省赛-J(贪心)

void solve() {
	int n; cin >> n;
	vi a(n + 1);
	ll sum = 0;
	for (int i = 1; i <= n; i++) {
		cin >> a[i];
		sum += a[i];
	}

	sort(a.begin() + 1, a.end());

	ll sum1 = 0, sum2 = 0;
	for (int i = 1; i <= n; i += 2) sum1 += a[i];
	for (int i = n; i >= 1; i -= 2) sum2 += a[i];

	cout << max(abs(sum1) - abs(sum - sum1), abs(sum2) - abs(sum - sum2));
}

int main() {
	solve();
}


D. Zztrans 的班级合照

题意 ( 3   s 3\ \mathrm{s} 3 s)

n n n(偶数)个人按如下要求排队:排成人数相同的两排,每排从左往右身高不减,且第二排同学身高不低于第一排对应位置的同学的身高.现给定将同学按身高升序排列后每个同学的排名(身高相同的同学排名相同),求排队方案数,答案对 998244353 998244353 998244353取模.

第一行输入一个偶数 n    ( 2 ≤ n ≤ 5000 ) n\ \ (2\leq n\leq 5000) n  (2n5000).第二行输入 n n n个整数 a 1 , ⋯   , a n    ( 1 ≤ a i ≤ n ) a_1,\cdots,a_n\ \ (1\leq a_i\leq n) a1,,an  (1ain),分别表示每个同学的身高排名.

思路

记录每个身高 i i i的人数 c n t [ i ] cnt[i] cnt[i]后将原数组去重,则相同的身高的人任意排,答案乘上人数的阶乘即可.

考虑所有身高都不同时的情况.注意到任意时刻第二排的人数不超过第一排的人数, d p [ i ] [ j ] dp[i][j] dp[i][j]表示排完前 i i i个人,且第一排比第二排多 j j j个人的方案数,则最终答案为 d p [ n ] [ n / 2 ] dp[n][n/2] dp[n][n/2].

s u m sum sum记录当前排完的人数.对每个身高的人数 i i i,枚举第一排比第二排多的人数 j j j,显然它不超过 min ⁡ { n 2 , s u m } \min\left\{\dfrac{n}{2},sum\right\} min{2n,sum}.因第二排的人数不超过第一排的人数,故还需满足 j ≥ s u m − j j\geq sum-j jsumj.按上一个状态 d p [ s u m − i ] [ ] dp[sum-i][] dp[sumi][]中第一排比第二排多的人数分类,不妨设 d p [ s u m ] [ ] dp[sum][] dp[sum][]中第一排还需补 k k k个人,则可从 d p [ s u m − i ] [ j − k ] dp[sum-i][j-k] dp[sumi][jk]转移到 d p [ s u m ] [ j ] dp[sum][j] dp[sum][j],枚举 k ∈ [ 0 , min ⁡ { i , j } ] k\in[0,\min\{i,j\}] k[0,min{i,j}]即可.

代码 -> 2021CCPC上海省赛-D(DP+组合计数)

const int MAXN = 5005;
const int MOD = 998244353;
int n;
int cnt[MAXN];  // cnt[i]表示身高为i的人数
int dp[MAXN][MAXN];  // dp[i][j]表示排完前i个人,且第一排比第二排多j个人的方案数
int fac[MAXN], ifac[MAXN];

void init() {  // 预处理阶乘
	fac[0] = 1;
	for (int i = 1; i < MAXN; i++) fac[i] = (ll)fac[i - 1] * i % MOD;
}

void solve() {
	init();

	cin >> n;
	for (int i = 0; i < n; i++) {
		int x; cin >> x;
		cnt[x]++;
	}

	vi h;  // 去重后的身高
	int ans = 1;
	for (int i = 1; i <= n; i++) {
		if (cnt[i]) {
			h.push_back(cnt[i]);
			ans = (ll)ans * fac[cnt[i]] % MOD;  // 相同身高的人任意排
		}
	}

	int sum = 0;  // 当前排完的人数
	dp[0][0] = 1;  // i=0时只有j=0是合法方案
	for (auto i : h) {  // 枚举每个身高的人数
		sum += i;

		// 第一排的人数比第二排多的人数不超过min{当前排完的人数,总人数的一半}
		for (int j = min(n / 2, sum); j >= sum - j; j--) {
			for (int k = 0; k <= min(i, j); k++)  // 枚举上一个状态站第一排的人数
				dp[sum][j] = ((ll)dp[sum][j] + dp[sum - i][j - k]) % MOD;
		}
	}

	ans = (ll)ans * dp[n][n / 2] % MOD;
	cout << ans;
}

int main() {
	solve();
}


H. 鸡哥的 AI 驾驶

题意 ( 4   s 4\ \mathrm{s} 4 s)

数轴上有若干辆车,每辆车有三个参数:位置、速度、型号.型号相同的两车在同一位置时不会发生事故,不同型号的辆车在同一位置时会发生事故.求一个时间 t   s . t .   [ 0 , t ] t\ s.t.\ [0,t] t s.t. [0,t]时间内未发生事故, ( t , t + 1 ] (t,t+1] (t,t+1]时间内发生了事故.

第一行输入两个整数 n , k    ( 1 ≤ k ≤ n ≤ 1 e 5 ) n,k\ \ (1\leq k\leq n\leq 1\mathrm{e}5) n,k  (1kn1e5),分别表示车数、型号数.接下来 n n n行每行输入三个整数 p , v , t    ( − 1 e 9 ≤ p , v ≤ 1 e 9 , 1 ≤ t ≤ k ) p,v,t\ \ (-1\mathrm{e}9\leq p,v\leq 1\mathrm{e}9,1\leq t\leq k) p,v,t  (1e9p,v1e9,1tk).数据保证初始时任意两车不在同一位置.

输出时间 t t t,若不会发生事故,输出 − 1 -1 1.

思路

若会发生事故,则时间越久越可能发生事故,故是否发生事故的性质具有二段性,可二分出其分界点.

考虑如何check.显然两不同型号的车发生事故的充要条件是它们的相互位置发生改变,即直观上它们互相穿过了对方.注意到每辆车不发生事故的移动范围是数轴上该型号的车最左边与最右边的位置之间的线段,则某型号的车离开该范围也会发生事故.

考察二分时间的范围.显然耗时最久的是从 x = − 1 e 9 x=-1\mathrm{e}9 x=1e9以速度 v = 1 v=1 v=1走到 x = 1 e 9 x=1\mathrm{e}9 x=1e9,耗时 t = 2 e 9 t=2\mathrm{e}9 t=2e9,则边界可取 [ 0 , 2 e 9 + 1 ] [0,2\mathrm{e}9+1] [0,2e9+1],其中 + 1 +1 +1是为了退出循环后断定 l = 2 e 9 l=2\mathrm{e}9 l=2e9是否有解.

代码 -> 2021CCPC上海省赛-H(二分)

const int MAXN = 1e5 + 5;
int n, k;
struct Car {
	int p, v, t;  // 位置、速度、型号
	
	bool operator<(const Car& B)const { return p < B.p; }
}cars[MAXN];
pii segs[MAXN];  // 每一段型号相同的车两端点的车的编号
pair<ll, int> pos[MAXN];  // 车移动后的位置、编号

bool check(int ti) {
	for (int i = 1; i <= n; i++) pos[i] = { cars[i].p + (ll)cars[i].v * ti,i };  // 末位置

	sort(pos + 1, pos + n + 1);

	for (int i = 1; i <= n; i++) {
		if (i != 1 && pos[i].first == pos[i - 1].first && cars[pos[i].second].t != cars[pos[i - 1].second].t)
			return false;  // 型号不同的两车在同一位置

		if (i < segs[pos[i].second].first || i > segs[pos[i].second].second) return false;  // 超出最大移动范围
	}

	return true;
}

void solve() {
	cin >> n >> k;
	for (int i = 1; i <= n; i++) cin >> cars[i].p >> cars[i].v >> cars[i].t;

	if (k == 1) {  // 只有一种型号不会发生事故
		cout << -1;
		return;
	}

	sort(cars + 1, cars + n + 1);  // 按位置升序排列

	// 预处理segs[]
	for (int i = 1; i <= n; i++) {  // 左端点
		if (cars[i].t == cars[i - 1].t) segs[i].first = segs[i - 1].first;
		else segs[i].first = i;
	}
	for (int i = n; i >= 1; i--) {  // 右端点
		if (cars[i].t == cars[i + 1].t) segs[i].second = segs[i + 1].second;
		else segs[i].second = i;
	}

	int l = 0, r = 2e9 + 1;  // 注意+1,否则l=2e9时无法判断是否有解
	while (l < r) {
		int mid = (ll)l + r + 1 >> 1;  // 注意这里会爆int
		if (check(mid)) l = mid;
		else r = mid - 1;
	}
	cout << (l == 2e9 + 1 ? -1 : l);
}

int main() {
	solve();
}


B. 小 A 的卡牌游戏

题意 ( 2   s 2\ \mathrm{s} 2 s)

一副 n n n张卡的卡组恰包含 a a a张A卡、 b b b张B卡、 c c c张C卡.现给出 n n n次三选一的机会,三张卡分别来自三个种类,玩家需从三张卡中选一张加入自己的卡组,使得卡组强度尽量大.每张卡有一个强度值,卡组的强度是所有卡的强度之和.求卡组强度的最大值.

第一行输入四个整数 n , a , b , c    ( 1 ≤ a , b , c ≤ n ≤ 5000 , a + b + c = n ) n,a,b,c\ \ (1\leq a,b,c\leq n\leq 5000,a+b+c=n) n,a,b,c  (1a,b,cn5000,a+b+c=n).接下来 n n n行每行输入三个整数描述一个三选一的机会,其中第 i i i行输入三个整数 a i , b i , c i    ( 1 ≤ a i , b i , c i ≤ 1 e 9 ) a_i,b_i,c_i\ \ (1\leq a_i,b_i,c_i\leq 1\mathrm{e}9) ai,bi,ci  (1ai,bi,ci1e9),分别表示该次选择中A卡、B卡、C卡的强度.

思路I

先考虑只有A卡和B卡的情况.注意不能贪心地选择强度前 a a a大的A卡和强度前 b b b大的B卡,因为选择间不独立.考虑先确定B卡的选择,剩下的选A卡.对两次三选一的机会 ( a i , b i ) (a_i,b_i) (ai,bi) ( a j , b j ) (a_j,b_j) (aj,bj),若选 b i b_i bi,则只能再选 a j a_j aj,同理选 b j b_j bj只能再选 a i a_i ai,则选前者更优的充要条件是: b i + a j > b j + a i b_i+a_j>b_j+a_i bi+aj>bj+ai,即 b i − a i > b j − a j b_i-a_i>b_j-a_j biai>bjaj.故将三选一的机会按 b i − a i b_i-a_i biai降序排列后贪心地选前几个即可.

考虑有A、B、C卡的情况. d p [ i ] [ j ] dp[i][j] dp[i][j]表示表示前 i i i次选择中有 j j j次选择C卡的最大强度,其中的 ( i − j ) (i-j) (ij)次选A卡或B卡按照上述贪心策略选即可.总时间复杂度 O ( n 2 ) O(n^2) O(n2).

代码I -> 2021CCPC上海省赛-B(贪心+DP)

const int MAXN = 5005;
int n, A, B, C;
ll dp[MAXN][MAXN];  // dp[i][j]表示前i次选择中有j次选择C卡的最大强度

struct Card {
	int a, b, c;

	bool operator<(const Card& B)const {
		if (b - a != B.b - B.a) return b - a > B.b - B.a;
		else return c > B.c;
	}
}cards[MAXN];

void solve() {
	cin >> n >> A >> B >> C;

	for (int i = 1; i <= n; i++) cin >> cards[i].a >> cards[i].b >> cards[i].c;

	sort(cards + 1, cards + n + 1);

	for (int i = 0; i <= n; i++)
		for (int j = i + 1; j <= C; j++) dp[i][j] = -INFF;  // 初始化非法状态

	for (int i = 1; i <= n; i++) {
		for (int j = 0; j <= min(i, C); j++) {  // 枚举选C卡的次数
			if (j) dp[i][j] = max(dp[i][j], dp[i - 1][j - 1] + cards[i].c);  // 选C卡

			if (i - j <= B) dp[i][j] = max(dp[i][j], dp[i - 1][j] + cards[i].b);  // 选B卡
			else dp[i][j] = max(dp[i][j], dp[i - 1][j] + cards[i].a);  // 选A卡
		}
	}
	cout << dp[n][C];
}

int main() {
	solve();
}

在这里插入图片描述


思路II

各边容量都为 1 1 1,费用为卡牌强度的负值,转化为求最小费用流,最终答案为最小费用的负值.

代码II -> 2021CCPC上海省赛-B(费用流)

namespace SPFA_Cost_Flow {
	static const int MAXN = 5005, MAXM = 1e5 + 10;  // 边开两倍
	int n, m, s, t;  // 点数、边数、源点、汇点
	int head[MAXN], edge[MAXM], capa[MAXM], cost[MAXM], nxt[MAXM], idx;  // capa[i]表示边i的容量,cost[i]表示边i的费用
	int min_capa[MAXN];  // min_capa[i]表示到节点i的所有边的容量的最小值
	int dis[MAXN];  // dis[i]表示源点到节点i的最短路
	int pre[MAXN];  // pre[i]表示节点i的前驱边的编号
	bool state[MAXN];  // SPFA中记录每个节点是否在队列中

	void add(int a, int b, int c, int d) {  // 建边a->b,容量为c,费用为d
		edge[idx] = b, capa[idx] = c, cost[idx] = d, nxt[idx] = head[a], head[a] = idx++;  // 正向边
		edge[idx] = a, capa[idx] = 0, cost[idx] = -d, nxt[idx] = head[b], head[b] = idx++;  // 反向边,流量初始为0,费用为正向边的相反数
	}

	bool spfa() {  // 返回是否找到增广路
		memset(dis, INF, so(dis));
		memset(min_capa, 0, so(min_capa));

		qi que;
		que.push(s);
		dis[s] = 0, min_capa[s] = INF;  // 源点处的流量无限制
		
		while (que.size()) {
			int u = que.front(); que.pop();
			state[u] = false;
			
			for (int i = head[u]; ~i; i = nxt[i]) {
				int v = edge[i];
				if (capa[i] && dis[v] > dis[u] + cost[i]) {  // 边还有容量
					dis[v] = dis[u] + cost[i];
					pre[v] = i;  // 记录前驱边
					min_capa[v] = min(min_capa[u], capa[i]);

					if (!state[v]) {
						que.push(v);
						state[v] = true;
					}
				}
			}
		}
		return min_capa[t];  // 汇点的流量非零即可以到达汇点,亦即存在增广路
	}

	pll EK() {  // first为最大流、second为最小费用
		pll res(0, 0);
		while (spfa()) {  // 当前还有增广路
			int tmp = min_capa[t];
			res.first += tmp, res.second += (ll)tmp * dis[t];
			for (int i = t; i != s; i = edge[pre[i] ^ 1])
				capa[pre[i]] -= tmp, capa[pre[i] ^ 1] += tmp;  // 正向边减,反向边加
		}
		return res;
	}
}
using namespace SPFA_Cost_Flow;

int A, B, C;

void solve() {
	memset(head, -1, so(head));
	s = 0, t = MAXN - 1;  // 超级源点、超级汇点

	cin >> n >> A >> B >> C;
	
	// 所有汇点向超级汇点连边,容量为每种卡的数量,费用为0
	add(n + 1, t, A, 0), add(n + 2, t, B, 0), add(n + 3, t, C, 0);
	
	for (int i = 1; i <= n; i++) {
		int a, b, c; cin >> a >> b >> c;
		add(s, i, 1, 0);  // 超级源点向源点连边,容量为1,费用为0
		// 各源点向对应的汇点连边,容量为1,费用为强度的负值
		add(i, n + 1, 1, -a), add(i, n + 2, 1, -b), add(i, n + 3, 1, -c);
	}

	cout << -EK().second;
}

int main() {
	solve();
}

在这里插入图片描述



K. Alice and Bob-2

题意 ( 15   s 15\ \mathrm{s} 15 s)

给定一些只包含小写字母的字符串,Alice和Bob两人轮流取字符,Alice先手,不能操作者败.每次有两种操作:①选择一个非空的字符串,取走其中任一个字符;②选择一个非空的字符串,取走其中任两个相异的字符.

t    ( 1 ≤ t ≤ 10 ) t\ \ (1\leq t\leq 10) t  (1t10)组测试数据.每组测试数据第一行输入一个整数 n    ( 1 ≤ n ≤ 10 ) n\ \ (1\leq n\leq 10) n  (1n10),表示字符串个数.接下来 n n n行每行输入一个长度不超过 40 40 40且只包含小写字母的字符串 s s s.

对每组测试数据,输出最后的胜利者.

思路

显然SG函数.注意到字符串"aabb"和字符串"ccdd"对答案的贡献是相同的,即本问题中两字符串本质不同当且仅当它们所含的字符种类数不同或所含的字符种类数相同且相同字符的数目不同.考虑记搜,将本质相同的字符串用哈希值表示.

事实上,长度为 l e n len len的本质不同的字符串的个数为 P ( l e n ) P(len) P(len),其中 P ( i ) P(i) P(i)表示 i i i的无序分拆数,故要求的SG函数只有 ∑ i = 1 n P ( i ) = 215308 \displaystyle\sum_{i=1}^n P(i)=215308 i=1nP(i)=215308个.

代码 -> 2021CCPC上海省赛-K(SG函数+记搜+哈希)

namespace Hash {
	const int Base = 131, MOD = 1e9 + 7;
	umap<int, int> mp;  // 记录哈希值对应的下标
	int idx = 0;

	int get_hash(vi& a) {
		int res = 0;
		for (auto i : a) {
			if (i) res = ((ll)res * Base + i) % MOD;
			else break;
		}
		return res;
	}

	int get_idx(int a) {
		if (mp.count(a)) return mp[a];
		else return mp[a] = idx++;
	}
};
using namespace Hash;

int cnt[30];  // 每个字母出现的次数
umap<int, int> SG;

int get_mex(set<int>& s) {
	int mex = 0;
	for (auto i : s) {
		if (i == mex) mex++;
		else break;
	}
	return mex;
}

int get_SG(vi a) {
	sort(all(a), greater<int>());  // 注意排序
	int ha = get_hash(a);
	if (SG.count(ha)) return SG[ha];  // 搜过

	set<int> tmpSG;  // 存已求出的SG函数值

	// 删除一个字符
	for (int i = 0; i < a.size(); i++) {  // 枚举要删除的字符
		if (a[i]) {  // 还有这种字符
			a[i]--;  // 删除一个字符
			tmpSG.insert(get_SG(a));
			a[i]++;  // 恢复现场
		}
		else break;  // 没有这种字符
	}

	// 删除两个相异的字符
	for (int i = 0; i < a.size(); i++) {  // 枚举第一个要删除的字符
		if (!a[i]) break;  // 没有这种字符
		for (int j = i + 1; j < a.size(); j++) {  // 枚举第二个要删除的字符
			if (!a[j]) break;

			a[i]--, a[j]--;  // 删除两个字符
			tmpSG.insert(get_SG(a));
			a[i]++, a[j]++;  // 恢复现场
		}
	}
	
	return SG[ha] = get_mex(tmpSG);
}

void solve() {
	int ans = 0;  // 各SG函数的异或和
	CaseT{
		for (int i = 0; i < 26; i++) cnt[i] = 0;  // 清空
	
		string s; cin >> s;
		for (auto ch : s) cnt[ch - 'a']++;

		vi tmp;
		for (int i = 0; i < 26; i++)
			if (cnt[i]) tmp.push_back(cnt[i]);
		ans ^= get_SG(tmp);
	}

	cout << (ans ? "Alice" : "Bob") << endl;
}

int main() {
	solve();
}

在这里插入图片描述



I. 对线

题意 ( 12   s 12\ \mathrm{s} 12 s)

有三排长度为 n n n的兵线,每排兵线从左往右编号 1 ∼ n 1\sim n 1n,同排同编号位置对齐.现有如下四个操作:

0   x   l   r    ( x ∈ { 1 , 2 , 3 } , 1 ≤ l ≤ r ≤ n ) 0\ x\ l\ r\ \ (x\in\{1,2,3\},1\leq l\leq r\leq n) 0 x l r  (x{1,2,3},1lrn),表示询问第 x x x排从 l l l位置到 r r r位置间的士兵数,答案对 998244353 998244353 998244353取模.

1   x   l   r   y    ( x ∈ { 1 , 2 , 3 } , 1 ≤ l ≤ r ≤ n , 1 ≤ y ≤ 1 e 9 ) 1\ x\ l\ r\ y\ \ (x\in\{1,2,3\},1\leq l\leq r\leq n,1\leq y\leq 1\mathrm{e}9) 1 x l r y  (x{1,2,3},1lrn,1y1e9),表示第 x x x排从 l l l位置到 r r r位置都增加 y y y个士兵.

2   x   y   l   r    ( x , y ∈ { 1 , 2 , 3 } , 1 ≤ l ≤ r ≤ n ) 2\ x\ y\ l\ r\ \ (x,y\in\{1,2,3\},1\leq l\leq r\leq n) 2 x y l r  (x,y{1,2,3},1lrn),表示交换第 x x x排和第 y y y排区间 [ l , r ] [l,r] [l,r]上的士兵.

3   x   y   l   r    ( x , y ∈ { 1 , 2 , 3 } , 1 ≤ l ≤ r ≤ n ) 3\ x\ y\ l\ r\ \ (x,y\in\{1,2,3\},1\leq l\leq r\leq n) 3 x y l r  (x,y{1,2,3},1lrn),表示将第 x x x排区间 [ l , r ] [l,r] [l,r]上的士兵复制一份加到第 y y y排对应位置.

思路

线段树节点维护:① 1 × 4 1\times 4 1×4的矩阵 s u m [ ] [ ] sum[][] sum[][],其中 s u m [ 1 ] [ x ] sum[1][x] sum[1][x]表示第 x x x排当前区间的区间和;② 4 × 4 4\times 4 4×4的矩阵 l a z y [ ] [ ] lazy[][] lazy[][],表示矩阵乘法的懒标记,初始化为单位矩阵.整体思路:每个 4 × 4 4\times 4 4×4的矩阵左上角的 3 × 3 3\times 3 3×3矩阵的每一列维护兵线的每一排, 4 × 4 4\times 4 4×4的矩阵的第 4 4 4行维护操作.

①操作,答案为 [ l , r ] [l,r] [l,r]的区间和的 a [ 1 ] [ x ] a[1][x] a[1][x]元素.

②操作,根据Gauss消元解线性方程组中的"一行的若干倍加到另一行上",不妨取矩阵的第 4 4 4行都为 1 1 1,则该操作等价于将第 4 4 4行的 y y y倍加到第 x x x行.如 x = 1 x=1 x=1时,转移矩阵 [ 1 0 0 0 0 1 0 0 0 0 1 0 y 0 0 1 ] \begin{bmatrix}1&0&0&0 \\ 0&1&0&0 \\ 0&0&1&0 \\ y&0&0&1\end{bmatrix} 100y010000100001 ; x = 2 x=2 x=2时,转移矩阵 [ 1 0 0 0 0 1 0 0 0 0 1 0 0 y 0 1 ] \begin{bmatrix}1&0&0&0 \\ 0&1&0&0 \\ 0&0&1&0 \\ 0&y&0&1\end{bmatrix} 1000010y00100001 ; x = 3 x=3 x=3时,转移矩阵. [ 1 0 0 0 0 1 0 0 0 0 1 0 0 0 y 1 ] \begin{bmatrix}1&0&0&0 \\ 0&1&0&0 \\ 0&0&1&0 \\ 0&0&y&1\end{bmatrix} 10000100001y0001 .

③操作,根据Gauss消元解线性方程组中的"交换两行",易知转移矩阵即单位矩阵交换第 x x x行和第 y y y行的结果.如 x = 1 , y = 3 x=1,y=3 x=1,y=3时,转移矩阵 [ 0 0 1 0 0 1 0 0 1 0 0 0 0 0 0 1 ] \begin{bmatrix}0&0&1&0 \\ 0&1&0&0 \\ 1&0&0&0 \\ 0&0&0&1\end{bmatrix} 0010010010000001 .

④操作,根据Gauss消元解线性方程组中的"一行的若干倍加到另一行上",易知转移矩阵即单位矩阵的 a [ x ] [ y ] a[x][y] a[x][y]元素 + 1 +1 +1.如 x = 1 , y = 3 x=1,y=3 x=1,y=3时,转移矩阵 [ 1 0 1 0 0 1 0 0 0 0 1 0 0 0 0 1 ] \begin{bmatrix}1&0&1&0 \\ 0&1&0&0 \\ 0&0&1&0 \\ 0&0&0&1\end{bmatrix} 1000010010100001 .

注意输入输出量大.

代码 -> 2021CCPC上海省赛-I(线段树维护矩阵)

namespace FastIO {
#define gc() (p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 1 << 21, stdin), p1 == p2) ? EOF : *p1++)  // 重写getchar()
#define pc(ch) (p - buf2 == SIZE ? fwrite(buf2, 1, SIZE, stdout), p = buf2, *p++ = ch : *p++ = ch)  // 重写putchar()
	char buf[1 << 23], * p1 = buf, * p2 = buf;

	template<typename T>
	void read(T& x) {  // 数字快读
		x = 0;
		T sgn = 1;
		char ch = gc();
		while (ch < '0' || ch > '9') {
			if (ch == '-') sgn = -1;
			ch = gc();
		}
		while (ch >= '0' && ch <= '9') {
			x = (((x << 2) + x) << 1) + (ch & 15);
			ch = gc();
		}
		x *= sgn;
	}

	const int SIZE = 1 << 21;
	int stk[40], top;
	char buf1[SIZE], buf2[SIZE], * p = buf2, * s = buf1, * t = buf1;

	template<typename T>
	void print_number(T x) {
		p = buf2;  // 复位指针p

		if (!x) {
			pc('0');
			return;
		}

		top = 0;  // 栈顶指针
		if (x < 0) {
			pc('-');
			x = ~x + 1;  // 取相反数
		}

		do {
			stk[top++] = x % 10;
			x /= 10;
		} while (x);
		while (top) pc(stk[--top] + 48);
	}

	template<typename T>
	void write(T x) {  // 数字快写
		print_number(x);
		fwrite(buf2, 1, p - buf2, stdout);
	}
};
using namespace FastIO;

const int MAXN = 3e5 + 5;
const int MOD = 998244353;

template<typename T>
struct Matrix {
	static const int MAXSIZE = 5;
	int n, m;  // 行数、列数
	T a[MAXSIZE][MAXSIZE];  // 下标从1开始

	Matrix() :n(0), m(0) { memset(a, 0, so(a)); }
	Matrix(int _n, int _m) :n(_n), m(_m) { memset(a, 0, so(a)); }
	
	void init_identity() {  // 初始化为单位矩阵
		assert(n == m);

		memset(a, 0, so(a));
		for (int i = 1; i <= n; i++) a[i][i] = 1;
	}

	Matrix<T> operator+(const Matrix<T>& B)const {
		assert(n == B.n), assert(m == B.m);

		Matrix<T> res(n, n);
		for (int i = 1; i <= n; i++) {
			for (int j = 1; j <= m; j++)
				res.a[i][j] = ((ll)a[i][j] + B.a[i][j]) % MOD;
		}
		return res;
	}

	Matrix<T> operator-(const Matrix<T>& B)const {
		assert(n == B.n), assert(m == B.m);

		Matrix<T> res(n, n);
		for (int i = 1; i <= n; i++) {
			for (int j = 1; j <= m; j++)
				res.a[i][j] = ((a[i][j] - B.a[i][j]) % MOD + MOD) % MOD;
		}
		return res;
	}

	Matrix<T> operator*(const Matrix<T>& B)const {
		assert(m == B.n);

		Matrix<T> res(n, B.m);
		for (int i = 1; i <= n; i++) {
			for (int j = 1; j <= B.m; j++) {
				for (int k = 1; k <= m; k++)
					res.a[i][j] = ((ll)res.a[i][j] + (ll)a[i][k] * B.a[k][j]) % MOD;
			}
		}
		return res;
	}

	Matrix<T> operator^(int k)const {  // 快速幂
		assert(n == m);

		Matrix<T> res(n, n);
		res.init_identity();  // 单位矩阵
		Matrix<T> tmpa(n, n);  // 存放矩阵a[][]的乘方
		memcpy(tmpa.a, a, so(a));

		while (k) {
			if (k & 1) res = res * tmpa;
			k >>= 1;
			tmpa = tmpa * tmpa;
		}
		return res;
	}

	Matrix<T>& operator=(const Matrix<T>& B) {
		memset(a, 0, so(a));
		n = B.n, m = B.m;
		for (int i = 1; i <= n; i++)
			for (int j = 1; j <= m; j++) a[i][j] = B.a[i][j];
		return *this;
	}

	bool operator==(const Matrix<T>& B)const {
		if (n != B.n || m != B.m) return false;

		for (int i = 1; i <= n; i++) {
			for (int j = 1; j <= m; j++)
				if (a[i][j] != B.a[i][j]) return false;
		}
		return true;
	}

	void print() {
		for (int i = 1; i <= n; i++)
			for (int j = 1; j <= m; j++) cout << a[i][j] << " \n"[j == m];
	}
};

Matrix<ll> Imatrix = Matrix<ll>(4, 4);  // 单位矩阵

struct Node {
	int l, r;
	Matrix<ll> sum;  // 区间和
	Matrix<ll> lazy;  // 矩阵乘法标记
}SegT[MAXN << 2];

void push_up(int u) {
	SegT[u].sum = SegT[u << 1].sum + SegT[u << 1 | 1].sum;
}

void build(int u, int l, int r) {
	SegT[u].l = l, SegT[u].r = r;
	SegT[u].sum = Matrix<ll>(4, 4);
	SegT[u].lazy = Matrix<ll>(4, 4);
	SegT[u].lazy.init_identity();  // 初始化为单位矩阵,表示无修改
	if (l == r) {
		SegT[u].sum.a[1][4] = 1;
		return;
	}

	int mid = l + r >> 1;
	build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
	push_up(u);
}

void push_down(int u) {
	if (SegT[u].lazy == Imatrix) return;  // 无懒标记

	SegT[u << 1].sum = SegT[u << 1].sum * SegT[u].lazy;
	SegT[u << 1].lazy = SegT[u << 1].lazy * SegT[u].lazy;
	SegT[u << 1 | 1].sum = SegT[u << 1 | 1].sum * SegT[u].lazy;
	SegT[u << 1 | 1].lazy = SegT[u << 1 | 1].lazy * SegT[u].lazy;
	SegT[u].lazy.init_identity();  // 清空为单位矩阵
}

void modify(int u, int l, int r, Matrix<ll>& mul) {
	if (l <= SegT[u].l && SegT[u].r <= r) {
		SegT[u].sum = SegT[u].sum * mul;
		SegT[u].lazy = SegT[u].lazy * mul;
		return;
	}
	
	push_down(u);
	int mid = SegT[u].l + SegT[u].r >> 1;
	if (l <= mid) modify(u << 1, l, r, mul);
	if (r > mid) modify(u << 1 | 1, l, r, mul);
	push_up(u);
}

Matrix<ll> query(int u, int l, int r) {
	if (l <= SegT[u].l && SegT[u].r <= r) return SegT[u].sum;

	push_down(u);
	int mid = SegT[u].l + SegT[u].r >> 1;
	Matrix<ll> res(4, 4);
	if (l <= mid) res = res + query(u << 1, l, r);
	if (r > mid) res = res + query(u << 1 | 1, l, r);
	return res;
}

void solve() {
	Imatrix.init_identity();

	int n, q; read(n), read(q);
	build(1, 1, n);

	while (q--) {
		int op; read(op);
		if (op == 0) {  // 询问第x排[l,r]的区间和
			int x, l, r; read(x), read(l), read(r);
			write(query(1, l, r).a[1][x]), putchar('\n');
		}
		else if (op == 1) {  // 第x排[l,r]+=y
			int x, l, r, y; read(x), read(l), read(r), read(y);
			Matrix<ll> mul(4, 4);
			mul.init_identity();
			mul.a[4][x] = y;
			modify(1, l, r, mul);
		}
		else if (op == 2) {  // 交换第x排和第y排的[l,r]
			int x, y, l, r; read(x), read(y), read(l), read(r);
			Matrix<ll> mul(4, 4);
			mul.init_identity();
			mul.a[x][x] = mul.a[y][y] = 0;
			mul.a[x][y] = mul.a[y][x] = 1;
			modify(1, l, r, mul);
		}
		else {  // 第y排[l,r]+=第x排[l,r]
			int x, y, l, r; read(x), read(y), read(l), read(r);
			Matrix<ll> mul(4, 4);
			mul.init_identity();
			mul.a[x][y]++;
			modify(1, l, r, mul);
		}
	}
}

int main() {
	solve();
}

在这里插入图片描述



  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值