2021ICPC欧洲东南部区域赛题解ACFGJKLN

2021ICPC欧洲东南部区域赛题解ACFGJKLN

A. King of String Comparison

题意

给定两长度为 n    ( 1 ≤ n ≤ 2 e 5 ) n\ \ (1\leq n\leq 2\mathrm{e}5) n  (1n2e5)的字符串 s s s t t t,求使得子串 s [ l ⋯ r ] s[l\cdots r] s[lr]的字典序 < < <子串 t [ l ⋯ r ] t[l\cdots r] t[lr]的字典序的 ( l , r )    ( 1 ≤ l ≤ r ≤ n ) (l,r)\ \ (1\leq l\leq r\leq n) (l,r)  (1lrn)的对数.

思路

从左往右扫一遍,用双指针维护子串的左右端点.

代码 -> 2021ICPC欧洲东南部区域赛-A(双指针)
const int MAXN = 2e5 + 5;
int n;
string s, t;

void solve() {
	cin >> n >> s >> t;
	ll ans = 0;
	int l = 0, r = 0;
	while (max(l, r) < n) {
		r = max(r, l);

		if (s[r] == t[r]) r++;
		else if (s[r] < t[r]) ans += n - r, l++;
		else l = r + 1, r = l;
	}
	cout << ans;
}

int main() {
	solve();
}


N. A-series

题意

( n + 1 ) (n+1) (n+1)种不同大小的纸 A 0 , A 1 , ⋯   , A n A_0,A_1,\cdots,A_n A0,A1,,An,其中前者的大小是后者的两倍.现有 a 0 a_0 a0张大小为 A 0 A_0 A0的纸, a 1 a_1 a1张大小为 A 1 A_1 A1的纸, ⋯   , a n \cdots,a_n ,an张大小为 A n A_n An的纸,现要获得 b 0 b_0 b0张大小为 A 0 A_0 A0的纸, b 1 b_1 b1张大小为 A 1 A_1 A1的纸, ⋯   , b n \cdots,b_n ,bn张大小为 A n A_n An的纸.每次操作可将一张大的纸对折并切成两半,得到两张小的纸.求至少需切多少次才能获得足够数量的纸.

第一行输入整数 n    ( 1 ≤ n ≤ 2 e 5 ) n\ \ (1\leq n\leq 2\mathrm{e}5) n  (1n2e5).第二行输入 ( n + 1 ) (n+1) (n+1)个数 a 0 , a 1 , ⋯   , a n    ( 0 ≤ a i ≤ 1 e 9 ) a_0,a_1,\cdots,a_n\ \ (0\leq a_i\leq 1\mathrm{e}9) a0,a1,,an  (0ai1e9).第三行输入 ( n + 1 ) (n+1) (n+1)个数 b 0 , b 1 , ⋯   , b n    ( 0 ≤ b i ≤ 1 e 9 ) b_0,b_1,\cdots,b_n\ \ (0\leq b_i\leq 1\mathrm{e}9) b0,b1,,bn  (0bi1e9).

若能通过若干次切割获得足够数量的纸,输出最小切割次数;否则输出 − 1 -1 1.

思路I

从前往后看数量是 2 n 2^n 2n的增长,不妨考虑从后往前看.

考察最小尺寸 A n A_n An.①若 b n ≤ a n b_n\leq a_n bnan,则已有的尺寸为 A n A_n An的纸已满足需求,再继续切割显然是不优的.

​ ②若 b n > a n b_n>a_n bn>an,则至少需切割尺寸为 A n − 1 A_{n-1} An1的纸 ⌈ b n − a n 2 ⌉ \left\lceil\dfrac{b_n-a_n}{2}\right\rceil 2bnan次.

​ 先不考虑尺寸为 A n − 1 A_{n-1} An1的纸存量是否够,只令 a n − 1 − = ⌈ b n − a n 2 ⌉ , a n s + = ⌈ b n − a n 2 ⌉ a_{n-1}-=\left\lceil\dfrac{b_n-a_n}{2}\right\rceil,ans+=\left\lceil\dfrac{b_n-a_n}{2}\right\rceil an1=2bnan,ans+=2bnan.

从后往前重复上述过程,最后检查 a 0 a_0 a0是否 ≥ b 0 \geq b_0 b0即可.总时间复杂度 O ( n ) O(n) O(n).

代码 -> 2021ICPC欧洲东南部区域赛-N(思维I)
void solve() {
	int n; cin >> n;
	vi a(n + 1), b(n + 1);
	for (int i = 0; i <= n; i++) cin >> a[i];
	for (int i = 0; i <= n; i++) cin >> b[i];

	ll ans = 0;
	for (int i = n; i >= 1; i--) {
		if (a[i] >= b[i]) continue;  // 存量够

		int tmp = (b[i] - a[i] + 1) / 2;
		a[i - 1] -= tmp, ans += tmp;
	}

	cout << (a[0] >= b[0] ? ans : -1);
}

int main() {
	solve();
}

思路II By : HeartFireY

将剪纸的过程倒过来,倒序考察合并纸张.

代码 -> 2021ICPC欧洲东南部区域赛-N(思维II)
void solve() {
	int n; cin >> n;
	vi a(n + 1), b(n + 2);
	for (int i = 0; i <= n; i++) cin >> a[i];
	for (int i = 0; i <= n; i++) cin >> b[i];

	ll ans = 0;
	for (int i = n; i >= 1; i--) {
		if (a[i] >= b[i]) continue;  // 存量够

		int tmp = (b[i] - a[i] + 1) >> 1;
		b[i] -= tmp << 1, b[i - 1] += tmp;
		ans += tmp;
	}

	for (int i = 0; i < n; i++) {
		if (b[i] > a[i]) {
			cout << -1;
			return;
		}
	}
	cout << ans;
}

int main() {
	solve();
}


J. ABC Legacy

题意

给定一个长度为 2 n    ( 1 ≤ n ≤ 1 e 5 ) 2n\ \ (1\leq n\leq 1\mathrm{e}5) 2n  (1n1e5)的且只包含字符’A’、‘B’、'C’的字符串 s s s.判断是否能将 s s s分割为 n n n个不相交的子串,每个子串是"AB"、”AC“、“BC"之一.若能,输出"YES"并输出所有子串包含的两个字符的下标;否则输出"NO”.

思路I

s s s能分割为 n n n个不相交的子串,其中"AB"、“AC”、"BC"各 x , y , z x,y,z x,y,z个.设 s s s中’A’、‘B’、'C’分别出现 c n t A , c n t B , c n t C cnt_A,cnt_B,cnt_C cntA,cntB,cntC次,则 { c n t A = x + y c n t B = x + z c n t C = y + z \begin{cases}cnt_A=x+y \\ cnt_B=x+z \\ cnt_C=y+z\end{cases} cntA=x+ycntB=x+zcntC=y+z,解得 { x = c n t A + c n t B − c n t C 2 y = c n t A − c n t B + c n t C 2 z = − c n t A + c n t B + c n t C 2 \begin{cases}x=\dfrac{cnt_A+cnt_B-cnt_C}{2} \\ y=\dfrac{cnt_A-cnt_B+cnt_C}{2} \\ z=\dfrac{-cnt_A+cnt_B+cnt_C}{2} \end{cases} x=2cntA+cntBcntCy=2cntAcntB+cntCz=2cntA+cntB+cntC,则有解的必要条件是 x , y , z ∈ N x,y,z\in\mathbb{N} x,y,zN.

考察’B’出现的位置,它将作为子串"AB"的第二个字母出现 x x x次,作为子串"BC"的第一个字母出现 z z z次.显然最优解中’B’出现的前 z z z个位置用于产生子串"BC",出现的后 x x x个位置用于产生子串"AB",同时使得子串"AB"中’A’尽量靠右,子串"BC"中的’C’尽量靠左,在中间产生子串"AC".

v i s [ i ] vis[i] vis[i]表示字符 s [ i ] s[i] s[i]是否已匹配.先正着扫一遍 s s s,将出现位置靠前的 z z z个’B’与其右边最靠前的未匹配的’C’匹配(若存在,下同).再倒着扫一遍 s s s,将出现位置靠后的 x x x个’B’与其左边靠后的未匹配的’A’匹配.对剩下的字符,检查其是否是"ACACAC ⋯ \cdots "即可.时间复杂度 O ( n ) O(n) O(n).

代码 : set实现(思路清晰,但TLE)
void solve() {
	int n; cin >> n;
	n <<= 1;
	string s; cin >> s;
	s = " " + s;

	set<int> A, B, C;  // 分别记录字符'A'、'B'、'C'出现的下标
	for (int i = 1; i <= n; i++) {
		if (s[i] == 'A') A.insert(i);
		else if (s[i] == 'B') B.insert(i);
		else C.insert(i);
	}
	;
	int tmpa = (int)A.size() + (int)B.size() - (int)C.size(), 
		tmpb = (int)A.size() - (int)B.size() + (int)C.size(), 
		tmpc = -(int)A.size() + (int)B.size() + (int)C.size();
	if (tmpa < 0 || tmpb < 0 || tmpc < 0 || (tmpa & 1) || (tmpb & 1) || (tmpc & 1)) {
		cout << "NO";
		return;
	}
	int x = tmpa >> 1, y = tmpb >> 1, z = tmpc >> 1;

	vii ans;
	int cnt = 0;  // 当前匹配的B的个数
	while (true) {
		if (B.empty() || cnt == z) break;

		int i = *B.begin();  // B中最小值即当前字符'B'最早出现的下标
		auto tmp = upper_bound(all(C), i);  // 找到i右边第一个字符'C'出现的下标
		if (tmp != C.end()) {
			ans.push_back({ i,*tmp });
			B.erase(i), C.erase(*tmp);
			cnt++;
		}
		else break;
	}

	cnt = 0;
	while (true) {
		if (B.empty() || cnt == x) break;

		int i = *B.rbegin();  // B中最大值即当前字符'B'最晚出现的下标
		auto tmp = upper_bound(rall(A), i, greater<int>());  // 找到i左边第一个字符'A'出现的下标
		if (tmp != A.rend()) {
			ans.push_back({ *tmp,i });
			B.erase(i), A.erase(*tmp);
			cnt++;
		}
		else break;
	}

	if (B.size() || A.size() != C.size()) {
		cout << "NO";
		return;
	}

	while (A.size()) {
		int tmpa = *A.begin(), tmpc = *C.begin();
		if (tmpa > tmpc) {
			cout << "NO";
			return;
		}
		else {
			ans.push_back({ tmpa,tmpc });
			A.erase(tmpa), C.erase(tmpc);
		}
	}
	
	cout << "YES" << endl;
	for (auto [l, r] : ans) cout << l << ' ' << r << endl;
}

int main() {
	solve();
}

代码I -> 2021ICPC欧洲东南部区域赛-J(贪心+模拟)
void solve() {
	int n; cin >> n;
	n <<= 1;
	string s; cin >> s;
	s = " " + s;

	int cnta = 0, cntb = 0, cntc = 0;
	for (int i = 1; i <= n; i++) {
		if (s[i] == 'A') cnta++;
		else if (s[i] == 'B') cntb++;
		else cntc++;
	}
	;
	int tmpa = cnta + cntb - cntc, tmpb = cnta - cntb + cntc, tmpc = -cnta + cntb + cntc;
	if (tmpa < 0 || tmpb < 0 || tmpc < 0 || (tmpa & 1) || (tmpb & 1) || (tmpc & 1)) {
		cout << "NO";
		return;
	}
	int x = tmpa >> 1, y = tmpb >> 1, z = tmpc >> 1;

	vii ans;
	vb vis(n + 1);
	deque<int> posb;  // 字符'B'出现的下标
	int cnt = 0;  // 当前匹配的'B'的个数
	for (int i = 1; i <= n && cnt < z; i++) {  // 正着扫一遍s,匹配"BC"
		if (s[i] == 'B') posb.push_back(i);
		else if (s[i] == 'C') {
			if (posb.size()) {
				ans.push_back({ posb.front(),i });
				vis[posb.front()] = vis[i] = true;
				posb.pop_front();
				cnt++, cntc--;
			}
		}
	}

	cntb-=cnt, cnt = 0;  // 更新未匹配的'B'的个数,并清空cnt
	posb.clear();
	for (int i = n; i >= 1 && cnt < x; i--) {  // 倒着扫一遍s,匹配"AB"
		if (vis[i]) continue;  // 防止一个字符重复被使用

		if (s[i] == 'B') posb.push_back(i);
		else if (s[i] == 'A') {
			if (posb.size()) {
				ans.push_back({ i,posb.front() });
				vis[i] = vis[posb.front()] = true;
				posb.pop_front();
				cnt++, cnta--;
			}
		}
	}
	cntb -= cnt;  // 更新未匹配的'B'的个数

	if (cntb || cnta != cntc) {  // 还有未匹配的'B'则无解
		cout << "NO";
		return;
	}

	deque<int> posa, posc;
	for (int i = 1; i <= n; i++) {
		if (!vis[i]) {
			if (s[i] == 'A') posa.push_back(i);
			else posc.push_back(i);
		}
	}
	
	while (posa.size()) {
		int tmpa = posa.front(), tmpc = posc.front();
		if (tmpa > tmpc) {
			cout << "NO";
			return;
		}
		else {
			ans.push_back({ tmpa,tmpc });
			posa.pop_front(), posc.pop_front();
		}
	}
	
	cout << "YES" << endl;
	for (auto [l, r] : ans) cout << l << ' ' << r << endl;
}

int main() {
	solve();
}

思路II By : HeartFireY

将’A’、'C’分别视为左括号、右括号,而’B’可视为左括号与’C’匹配,也可视为右括号与’A’匹配,转化为括号匹配问题.

合法的括号匹配共 n n n个左括号和 n n n个右括号,则有解的必要条件是 max ⁡ { c n t a , c n t c } ≤ n \max\{cnta,cntc\}\leq n max{cnta,cntc}n.

显然’B’中有 c n t b c = n − c n t a cntbc=n-cnta cntbc=ncnta个需作为左括号,将它们加入队列中,它们将产生"BC".其余的’B’作为右括号与’A’匹配.

代码II -> 2021ICPC欧洲东南部区域赛-J(贪心+思维)
deque<int> que[3];  // posa,posb,posc

void solve() {
	int n; cin >> n;
	string s; cin >> s;

	int cnta = 0, cntb = 0, cntc = 0;
	for (auto ch : s) {
		if (ch == 'A') cnta++;
		else if (ch == 'B') cntb++;
		else cntc++;
	}
	;
	int cntbc = n - cnta;  // 还需补cntbc个左括号
	if (cntbc < 0) {  // 括号不足
		cout << "NO" << endl;
		return;
	}

	n <<= 1, s = " " + s;
	vii ans;
	for (int i = 1; i <= n; i++) {
		if (s[i] == 'A') que[0].push_back(i);
		else if (s[i] == 'B') {
			if (cntbc) {  // 'B'作为左括号
				que[1].push_back(i);
				cntbc--;
			}
			else {  // 匹配"AB"
				if (que[0].empty()) {  // 无'A'
					cout << "NO" << endl;
					return;
				}

				ans.push_back({ que[0].back(),i});
				que[0].pop_back();
			}
		}
		else {  // 'B'作为左括号,匹配"AC"、"BC"
			if (que[0].empty() && que[1].empty()) {
				cout << "NO" << endl;
				return;
			}

			if (que[1].size()) {  // 优先匹配'B'
				ans.push_back({ que[1].back(),i });
				que[1].pop_back();
			}
			else {
				ans.push_back({ que[0].back(),i });
				que[0].pop_back();
			}
		}
	}

	cout << "YES" << endl;
	for (auto [l, r] : ans) cout << l << ' ' << r << endl;
}

int main() {
	solve();
}


F. to Pay Respects

题意

打BOSS,它不会攻击你,但它会念再生咒语.

游戏共 N N N轮,每轮按顺序发生如下事件:①BOSS可以选择念一次再生咒语;②若你还有能量,你可以选择念一次中毒咒语;③你攻击BOSS,造成 X X X点伤害;④本轮的负面效果生效.

有两种状态:再生和中毒.当前BOSS的状态可用三个整数描述:当前血量 h p hp hp、当前中毒等级 p p p、当前再生等级 r r r.初始时BOSS无中毒和再生等级,即 p = r = 0 p=r=0 p=r=0.每一级中毒会造成 P P P点伤害,每一级再生会恢复 R R R点血量.再生咒语会令 r + + r++ r++.中毒咒语会令 p + + p++ p++,若此时 r > 0 r>0 r>0,则同时 r − − r-- r.每轮结束后 h p − = x + P ⋅ p − R ⋅ r hp-=x+P\cdot p-R\cdot r hp=x+PpRr(该值可能为负,如BOSS回的血比收到的攻击更多时).

每轮开始时你都知道BOSS是否选择念再生咒语.你有 K K K次机会念中毒咒语.若BOSS的初始血量足够高,即 N N N轮内你无法击败BOSS,求你最大能对BOSS造成多少伤害,即求 h p s t a r t − h p e n d hp_{start}-hp_{end} hpstarthpend的最大值.注意BOSS的血量可以大于其初始血量.

第一行输入五个整数 N , X , R , P , K    ( 1 ≤ N , X , R , P , K ≤ 1 e 6 , 0 ≤ K ≤ N ) N,X,R,P,K\ \ (1\leq N,X,R,P,K\leq 1\mathrm{e}6,0\leq K\leq N) N,X,R,P,K  (1N,X,R,P,K1e6,0KN).第二行输入一个长度为 N N N 01 01 01串描述BOSS是否念再生咒语,其中’1’表示BOSS在对应的轮念重生咒语.

思路I

中毒咒语对再生咒语的影响可视为增加了攻击力.

因每轮的伤害分开计算,可先忽略念中毒咒语的次数限制,分别统计每轮念中毒咒语时本轮能造成的伤害 d a m a g e damage damage,最后降序排列,取前 k k k大即可. a n s ans ans n x nx nx开始,从左往右扫一遍时间线.若第 i    ( 1 ≤ i ≤ N ) i\ \ (1\leq i\leq N) i  (1iN)轮BOSS不念再生咒语,则本轮造成的伤害为 ( N − i + 1 ) ⋅ P (N-i+1)\cdot P (Ni+1)P;否则本轮造成的伤害为 ( N − i + 1 ) ⋅ ( P + R ) (N-i+1)\cdot (P+R) (Ni+1)(P+R),并 a n s − = L ( N − i + 1 ) ⋅ R ans-=L(N-i+1)\cdot R ans=L(Ni+1)R.总时间复杂度 O ( n log ⁡ n ) O(n\log n) O(nlogn).

代码I -> 2021ICPC欧洲东南部区域赛-F(贪心+思维)
void solve() {
	int n, x, r, p, k; cin >> n >> x >> r >> p >> k;
	string s; cin >> s; s = " " + s;

	ll ans = (ll)n * x;
	vl damage;
	for (int i = 1; i <= n; i++) {
		if (s[i] == '1') {
			// 中毒咒语对再生咒语的影响视为增加攻击力
			damage.push_back((ll)(n - i + 1) * (p + r));
			ans -= (ll)(n - i + 1) * r;
		}
		else damage.push_back((ll)(n - i + 1) * p);
	}
	
	sort(rall(damage));
	
	for (int i = 0; i < k; i++) ans += damage[i];
	cout << ans;
}

int main() {
	solve();
}

在这里插入图片描述


思路II

因再生和中毒的等级叠加后产生的效果更强,显然应尽量早地念中毒咒语.注意到BOSS念再生咒语时,可念一次中毒咒语消除再生咒语的影响,但当BOSS在靠后的轮次念再生咒语,此时再念中毒咒语可能不优.故最优解中尽量在时间线的某个前缀念中毒咒语.

念中毒咒语有两种效果:①令 p + + p++ p++;②令 p + + , r − − p++,r-- p++,r.先在第 1 ∼ k 1\sim k 1k轮念中毒咒语.每次考察最靠后一个①类型的中毒咒语位置换为第一个②类型的中毒咒语位置是否会让答案更优.过程用双指针维护(下面的代码未明显写出双指针,但本质相同),总时间复杂度 O ( n ) O(n) O(n).

代码II -> 2021ICPC欧洲东南部区域赛-F(贪心+思维+双指针)
void solve() {
	int n, x, r, p, k; cin >> n >> x >> r >> p >> k;
	string s; cin >> s; s = " " + s;

	deque<int> que1, que2;  // 念①②类型咒语的位置
	ll res = (ll)n * x;
	int curp = 0, curr = 0;  // 当前的中毒等级、再生等级
	for (int i = 1; i <= n; i++) {
		if (i > k) {
			if (s[i] == '1') {
				curr++;
				que2.push_back(i);
			}

			res += (ll)p * curp - (ll)r * curr;
			continue;
		}

		// 第1~k轮念中毒咒语,curr保持为0
		curp++;
		res += (ll)p * curp;
		if (s[i] != '1') que1.push_back(i);
	}

	ll ans = res;
	while (que1.size() && que2.size()) {
		int tmp1 = que1.back(), tmp2 = que2.front();  // 最靠后的一个①类型咒语、最靠前的一个②类型咒语
		res -= (ll)(tmp2 - tmp1) * p - (ll)(n - tmp2 + 1) * r;  // 将tmp1处的念中毒咒语换到tmp2处
		if (res > ans) {
			ans = res;
			que1.pop_back();
		}
		else res = ans;
		que2.pop_front();
	}
	cout << ans << endl;
}

int main() {
	solve();
}

在这里插入图片描述



G. Max Pair Matching

题意

给定 2 n 2n 2n个数对 ( a i , b i )    ( a i , b i ≥ 1 , 1 ≤ i ≤ 2 n ) (a_i,b_i)\ \ (a_i,b_i\geq 1,1\leq i\leq 2n) (ai,bi)  (ai,bi1,1i2n),考察一张由 2 n 2n 2n个节点构成的完全图,其中 e d g e < i , j >    ( 1 ≤ i , j ≤ 2 n , i ≠ j ) edge<i,j>\ \ (1\leq i,j\leq 2n,i\neq j) edge<i,j>  (1i,j2n,i=j)的边权 w i j = max ⁡ { ∣ a i − a j ∣ , ∣ a i − b j ∣ , ∣ b i − a j ∣ , ∣ b i − b j ∣ } w_{ij}=\max\{|a_i-a_j|,|a_i-b_j|,|b_i-a_j|,|b_i-b_j|\} wij=max{aiaj,aibj,biaj,bibj}.从中选出 n n n条边,使得任意两条选中的边无公共顶点,求选出的边的边权之和的最大值.

第一行输入一个整数 n    ( 1 ≤ n ≤ 1 e 5 ) n\ \ (1\leq n\leq 1\mathrm{e}5) n  (1n1e5).接下来 2 n 2n 2n行每行输入两个整数 a , b    ( 1 ≤ a , b ≤ 1 e 9 ) a,b\ \ (1\leq a,b\leq 1\mathrm{e}9) a,b  (1a,b1e9).

思路

因选择的 n n n条边中任意两条选中的边无公共顶点,则每个数只能被选一次,进而每对 ( a i , b i ) (a_i,b_i) (ai,bi)中都会选出一个数带正号,另一个数带负号,即边权之和的式子中有 n n n个正项和 n n n个负项.不妨设 a i ≤ b i a_i\leq b_i aibi,否则交换 a i a_i ai b i b_i bi,则最优解中在 2 n 2n 2n ( a i , b i ) (a_i,b_i) (ai,bi)中选择 n n n b i b_i bi n n n − a i -a_i ai.

S = ∑ i = 1 2 n b i \displaystyle S=\sum_{i=1}^{2n}b_i S=i=12nbi,考察将其中的 n n n b i b_i bi换为 − a i -a_i ai.注意到每换一个会使得 S S S减少 a i + b i a_i+b_i ai+bi,为使得边权之和最大,应使得 a i + b i a_i+b_i ai+bi尽可能小.故将所有数对 ( a i , b i ) (a_i,b_i) (ai,bi) a i + b i a_i+b_i ai+bi的值升序排列,在前 n n n个中取 − a i -a_i ai,在后 n n n个数中取 b i b_i bi.总时间复杂度 O ( n log ⁡ n ) O(n\log n) O(nlogn).

代码 -> 2021ICPC欧洲东南部区域赛-G(贪心)
void solve() {
	int n; cin >> n;
	n <<= 1;

	vii a(n);
	for (int i = 0; i < n; i++) {
		int x, y; cin >> x >> y;
		if (x > y) swap(x, y);
		a[i] = { x,y };
	}
	
	sort(all(a), [&](const pii& a, const pii& b) {
		return a.first + a.second < b.first + b.second;
		});

	ll ans = 0;
	for (int i = 0; i < n; i++) 
    ans += -a[i].first * (i < n / 2) + a[i].second * (i >= n / 2);
	cout << ans;
}

int main() {
	solve();
}


L. Jason ABC

题意

给定一个长度为 3 n 3n 3n,且只包含’A’、‘B’、‘C’的字符串 s s s.现有操作:选择 s s s一个连续子串,将其中的全部字符替换为’A’、‘B’或’C’.求将 s s s变为恰含’A’、‘B’、'C’各 n n n个所需的最小操作次数,并输出任一方案.

第一行输入一个整数 n    ( 1 ≤ n ≤ 3 e 5 ) n\ \ (1\leq n\leq 3\mathrm{e}5) n  (1n3e5).第二行输入一个长度为 3 n 3n 3n,且只包含’A’、‘B’、'C’的字符串 s s s.

第一行输出一个整数 k k k表示最小操作次数.接下来 k k k行每行输出两个整数 l , r    ( 1 ≤ l ≤ r ≤ 3 n ) l,r\ \ (1\leq l\leq r\leq 3n) l,r  (1lr3n)和一个字符 c ∈ { A , B , C } c\in\{A,B,C\} c{A,B,C},表示将子串 s [ l ⋯ r ] s[l\cdots r] s[lr]中的全部字符换为 c c c.

思路

首先一定有解,最坏可每次改变一个字符.其次最小操作次数不超过 3 3 3次,最坏可先将整个串变为’B’,然后将前 n n n个字符变为’A’,再将后 n n n个字符变为’C’.

进一步地,最小操作次数不超过 2 2 2次.证明:不妨设 s [ 1 ⋯ p ] s[1\cdots p] s[1p] s s s最短的、恰包含 n n n个相同字符(不妨设为’A’)的前缀,设其中字符’B’、‘C’的数量分别为 c n t b , c n t c    ( c n t b , c n t c < n ) cntb,cntc\ \ (cntb,cntc<n) cntb,cntc  (cntb,cntc<n).只需将子串 s [ ( p + 1 ) ⋯ ( p + n − c n t b ) ] s[(p+1)\cdots(p+n-cntb)] s[(p+1)(p+ncntb)]替换为’B’,将子串 s [ ( p + n − c n t b + 1 ) , 3 n ] s[(p+n-cntb+1),3n] s[(p+ncntb+1),3n]替换为’C’即可.

显然最小操作次数为 0 0 0当且仅当初始串已满足条件.下面考察何时 1 1 1次操作即可满足条件.

s s s中’B’、'C’的个数分别为 b b b c c c.考察是否存在一个区间$[l,r]\ s.t.\ 子串 子串 子串s[1\cdots (l-1)]+s[(r+1)\cdots 3n] 中恰有 中恰有 中恰有n 个 ′ B ′ 和 个'B'和 Bn 个 ′ C ′ , 进而可将子串 个'C',进而可将子串 C,进而可将子串s[l\cdots r] 替换 为 ′ A ′ 即可满足条件 . 注意到此时子串 替换为'A'即可满足条件.注意到此时子串 替换A即可满足条件.注意到此时子串s[l\cdots r] 中恰有 中恰有 中恰有(b-n) 个 ′ B ′ 和 个'B'和 B(c-n) 个 ′ C ′ , 可预处理出每个前缀 个'C',可预处理出每个前缀 C,可预处理出每个前缀s[1\cdots i] 中 ′ B ′ 和 ′ C ′ 的个数 中'B'和'C'的个数 BC的个数b_i,c_i , 用双指针检查是否存在一个区间 ,用双指针检查是否存在一个区间 ,用双指针检查是否存在一个区间[l,r]\ s.t.\ b_r-b_{l-1}=b-n,c_r-c_{l-1}=c-n . 总时间复杂度 .总时间复杂度 .总时间复杂度O(n)$.

代码 -> 2021ICPC欧洲东南部区域赛-L(思维+双指针) By : HeartFireY
const int MAXN = 9e5 + 5;  // 注意开3倍空间
int n;
string s;
int cnt[MAXN][3];  // cnt[i][]表示子串s[1...i]中字符'A'(0)、'B'(1)、'C'(2)出现的次数

bool check(char ch) {  // 检查是否存在一个区间[l,r] s.t. b[r]-b[l-1]=b-n,c[r]-c[l-1]=c-n
	int x = (ch - 'A' + 1) % 3, y = (ch - 'A' + 2) % 3;  // 另外两字符
	int l = 1, r = 1;
	while (l <= r) {
		while ((cnt[r][x] - cnt[l - 1][x] < cnt[n][x] - n / 3) || (cnt[r][y] - cnt[l - 1][y] < cnt[n][y] - n / 3)) r++;
		if (cnt[r][x] - cnt[l - 1][x] == cnt[n][x] - n / 3 && cnt[r][y] - cnt[l - 1][y] == cnt[n][y] - n / 3) {
			cout << 1 << endl;
			cout << l << ' ' << r << ' ' << ch;
			return true;
		}
		else l++;
	}
	return false;
}

void complete(int p, char ch) {  // 用两次操作完成
	int x = (ch - 'A' + 1) % 3, y = (ch - 'A' + 2) % 3;  // 另外两字符
	char X = x + 'A', Y = y + 'A';
	cout << 2 << endl;
	cout << p + 1 << ' ' << p + n/3 - cnt[p][x] << ' ' << X << endl;
	cout << p + n / 3 - cnt[p][x] + 1 << ' ' << n << ' ' << Y;
}

void solve() {
	cin >> n;
	n *= 3;
	cin >> s;
	s = " " + s;

	int p = -1; char ch;  // s[1...p]是s最短的、恰包含n个相同字符ch的前缀
	for (int i = 1; i <= n; i++) {
		for (int j = 0; j < 3; j++) cnt[i][j] = cnt[i - 1][j];
		cnt[i][s[i] - 'A']++;

		if (p == -1) {
			if (cnt[i][0] == n / 3) p = i, ch = 'A';
			else if (cnt[i][1] == n / 3) p = i, ch = 'B';
			else if (cnt[i][2] == n / 3) p = i, ch = 'C';
		}
	}

	if (cnt[n][0] == n / 3 && cnt[n][1] == n / 3 && cnt[n][2] == n / 3) {
		cout << 0;
		return;
	}

	if (check('A') || check('B') || check('C')) return;

	complete(p, ch);
}

int main() {
	solve();
}


C. Werewolves

题意

给定一棵包含编号 1 ∼ n 1\sim n 1n n n n个节点的树,其中第 i    ( 1 ≤ i ≤ n ) i\ \ (1\leq i\leq n) i  (1in)个节点的颜色为 c i c_i ci.好的连通子图满足:子图中的节点中有一个颜色的数量严格大于该子图的节点数的一半,称该颜色为该子图的主要颜色.求该树的好连通子图的个数,答案对 998244353 998244353 998244353取模.

第一行输入一个整数 n    ( 1 ≤ n ≤ 3000 ) n\ \ (1\leq n\leq 3000) n  (1n3000).第二行输入 n n n个整数 c 1 , ⋯   , c n    ( 1 ≤ c i ≤ n , 1 ≤ i ≤ n ) c_1,\cdots,c_n\ \ (1\leq c_i\leq n,1\leq i\leq n) c1,,cn  (1cin,1in).接下来 ( n − 1 ) (n-1) (n1)行每行输入两个整数 u , v    ( 1 ≤ u , v ≤ n , u ≠ v ) u,v\ \ (1\leq u,v\leq n,u\neq v) u,v  (1u,vn,u=v),表示节点 u u u v v v间存在边.

思路

一个好的连通子图中的主要颜色唯一.

[] 若不然,设某好的连通子图有两个主要颜色.

因主要颜色的节点数严格大于子图的节点数的一半,则两种主要颜色的节点数之和 > > >该子图的节点数,矛盾.

对每个颜色,统计以它为主要颜色的好的连通子图的个数.

固定一个主要颜色,将与该颜色相同的节点的权值置为 1 1 1,颜色不同的节点的权值置为 − 1 -1 1,问题转化为求点权之和 ≥ 1 \geq 1 1的子树的个数. d p [ u ] [ s u m ] dp[u][sum] dp[u][sum]表示以 u u u为根节点的子树中点权之和为 s u m sum sum的方案数.暴力转移时间复杂度 O ( n 4 ) O(n^4) O(n4),会TLE.

考虑优化.设颜色为 c c c的节点数为 c n t c cnt_c cntc.注意到当前状态只能转移到满足 − c n t c < s u m ≤ c n t c -cnt_c<sum\leq cnt_c cntc<sumcntc的状态,因为无法得到更大或更小的点权和.设当前子树大小为 s i z siz siz.注意到当前状态只能转移到满足 ∣ s u m ∣ ≤ s i z |sum|\leq siz sumsiz的状态,故转移只需考虑满足 ∣ s u m ∣ ≤ min ⁡ { c n t c , s i z } |sum|\leq\min\{cnt_c,siz\} summin{cntc,siz}的情况.计算每个主要颜色的时间复杂度为 O ( n ⋅ c n t c ) O(n\cdot cnt_c) O(ncntc),则总时间复杂度 ∑ c O ( n ⋅ c n t c ) = O ( n ⋅ ∑ c c n t c ) = O ( n 2 ) \displaystyle \sum_c O(n\cdot cnt_c)=O\left(n\cdot \sum_c cnt_c\right)=O(n^2) cO(ncntc)=O(nccntc)=O(n2),可过.

代码 -> 2021ICPC欧洲东南部区域赛-C(树上背包) By : TURNINING
const int MAXN = 3005;
const int MOD = 998244353;
int n, m;  // 节点数、每种颜色的节点数
int c[MAXN];  // 颜色
vi edges[MAXN];
bool vis[MAXN];  // 记录每个颜色是否遍历过
int w[MAXN];  // 点权
int dp1[MAXN][MAXN], tmp1[MAXN][MAXN];  // dp1[u][sum]表示以u为根节点的子树中点权之和为sum(sum≥1)的方案数
int dp2[MAXN][MAXN], tmp2[MAXN][MAXN];  // dp2[u][sum]表示以u为根节点的子树中点权之和为-sum(sum≥1)的方案数
int dp3[MAXN], tmp3[MAXN];  // dp3[u]表示以u为根节点的子树中点权之和为0的方案数
int ans;

int dfs(int u, int fa) {  // 当前节点、前驱节点
	int res = 1;  // 子树大小
	if (w[u] == 1) dp1[u][1] = 1;  // 相同颜色
	else dp2[u][1] = 1;  // 不同颜色

	for (auto v : edges[u]) {
		if (v == fa) continue;

		int siz = dfs(v, u);  // 递归求子树的信息

		for (int i = 0; i <= min(res, m); i++)  // 备份当前状态
			tmp1[u][i] = dp1[u][i], tmp2[u][i] = dp2[u][i], tmp3[u] = dp3[u];
		
		for (int j = 1; j <= min(siz, m); j++) {  // 转移不超过当前子树大小与相同颜色数的最小值,下同
			dp1[u][j] = ((ll)dp1[u][j] + (ll)tmp3[u] * dp1[v][j]) % MOD;  // 总和为0,+j转移到总和为j
			dp2[u][j] = ((ll)dp2[u][j] + (ll)tmp3[u] * dp2[v][j]) % MOD;  // 总和为0,-j转移到总和为-j
		}
		dp3[u] = ((ll)dp3[u] + (ll)tmp3[u] * dp3[v]) % MOD;  // 总和为0,+0转移到总和为0

		for (int i = 1; i <= min(res, m); i++) {
			dp1[u][i] = ((ll)dp1[u][i] + (ll)tmp1[u][i] * dp3[v]) % MOD;  // 总和为i,+0转移到总和为i
			dp2[u][i] = ((ll)dp2[u][i] + (ll)tmp2[u][i] * dp3[v]) % MOD;  // 总和为-i,+0转移到总和为-i

			for (int j = 1; j <= min(siz, m); j++) {
				if (i + j <= m) {
					dp1[u][i + j] = ((ll)dp1[u][i + j] + (ll)tmp1[u][i] * dp1[v][j]) % MOD;  // 总和为i,+j转移到总和为i+j
					dp2[u][i + j] = ((ll)dp2[u][i + j] + (ll)tmp2[u][i] * dp2[v][j]) % MOD;  // 总和为-i,-j转移到总和为-(i+j)
				}

				if (i - j >= 1) {
					dp1[u][i - j] = ((ll)dp1[u][i - j] + (ll)tmp1[u][i] * dp2[v][j]) % MOD;  // 总和为i,-j转移到总和为i-j
					dp2[u][i - j] = ((ll)dp2[u][i - j] + (ll)tmp2[u][i] * dp1[v][j]) % MOD;  // 总和为-i,+j转移到总和为-(i-j)
				}

				if (j - i >= 1) {
					dp1[u][j - i] = ((ll)dp1[u][j - i] + (ll)tmp2[u][i] * dp1[v][j]) % MOD;  // 总和为-i,+j转移到总和为j-i
					dp2[u][j - i] = ((ll)dp2[u][j - i] + (ll)tmp1[u][i] * dp2[v][j]) % MOD;  // 总和为i,-j转移到总和为-(j-i)
				}

				if (i == j) {
					dp3[u] = ((ll)dp3[u] + (ll)tmp1[u][i] * dp2[v][j] + (ll)tmp2[u][i] * dp1[v][j]) % MOD;
					// ①总和为i,-j;②总和为-i,+j,转移到总和为0
				}
			}
		}

		res += siz;  // 更新子树大小
	}

	for (int i = 1; i <= min(res, m); i++) 
		ans = ((ll)ans + dp1[u][i]) % MOD;  // 更新使得子树点权和≥1的方案数
	
	return res;
}

void solve() {
	cin >> n;
	for (int i = 1; i <= n; i++) cin >> c[i];
	for (int i = 1; i < n; i++) {
		int u, v; cin >> u >> v;
		edges[u].push_back(v), edges[v].push_back(u);
	}
	
	for (int i = 1; i <= n; i++) {  // 每种颜色做一次树上背包
		if (vis[c[i]]) continue;

		vis[c[i]] = true;
		m = 0;
		for (int j = 1; j <= n; j++) {
			if (c[j] == c[i]) {
				m++;
				w[j] = 1;  // 相同颜色权值为1
			}
			else w[j] = -1;  // 不同颜色权值为-1
		}

		// 初始化
		for (int j = 1; j <= n; j++)
			for (int k = 0; k <= m; k++) dp1[j][k] = dp2[j][k] = dp3[j] = 0;
				
		dfs(1, -1);  // 从根节点开始搜,根节点无前驱节点
	}
	cout << ans;
}

int main() {
	solve();
}


K. Amazing Tree

题意

t    ( 1 ≤ t ≤ 1 e 5 ) t\ \ (1\leq t\leq 1\mathrm{e}5) t  (1t1e5)组测试数据.每组测试数据第一行输入一个整数 n    ( 2 ≤ n ≤ 2 e 5 ) n\ \ (2\leq n\leq 2\mathrm{e}5) n  (2n2e5),表示树的节点数.接下来 ( n − 1 ) (n-1) (n1)行每行输入两个整数 u , v    ( 1 ≤ u , v ≤ n , u ≠ v ) u,v\ \ (1\leq u,v\leq n,u\neq v) u,v  (1u,vn,u=v),表示节点 u u u v v v间存在边.每棵树遍历时可任一选择起点,也可任意选定兄弟节点的遍历顺序.对每棵树,求字典序最小的后序遍历.数据保证所有测试数据的 n n n之和不超过 2 e 5 2\mathrm{e}5 2e5.

思路

在这里插入图片描述

固定树的后序遍历: A   B   D   C   H   M   G   E   F A\ B\ D\ C\ H\ M\ G\ E\ F A B D C H M G E F.

后序遍历的起点是叶子节点,以每个叶子节点为第一个节点可产生一个后序遍历.为使得字典序最小,显然应取编号最小的叶子节点为后序遍历的起点,设为节点 v v v.

设节点 u u u是节点 v v v是唯一邻居.设以 u u u为根节点的子树所含节点的最小编号为 m i n i d x [ u ] minidx[u] minidx[u].为得到以 v v v为起点的后序遍历,应先从根节点出发往下搜到 v v v,且 v v v的前驱为 u u u,遍历完 v v v返回 u u u,此时有两种情况:

(1) u u u是根节点,此时应将子树按 m i n i d x [ ] minidx[] minidx[]升序排列,依次遍历子树,最后将 u u u加入后序遍历.

(2) u u u非根节点,此时先将子树按 m i n i d x [ ] minidx[] minidx[]升序排列,设有 k k k棵子树,则先依次遍历前 ( k − 1 ) (k-1) (k1)棵.

​ ①若 u u u的子树中 m i n i d x [ ] minidx[] minidx[]的最大值(即排序后的第 k k k棵子树的 m i n i d x [ ] minidx[] minidx[])小于 u u u,则先遍历第 k k k棵子树,再将 u u u加入后序遍历.

​ ②若 u u u的子树中 m i n i d x [ ] minidx[] minidx[]的最大值 > u >u >u,则先将 u u u加入后序遍历,再遍历第 k k k棵子树.

实现时以最小编号的节点为根节点,则遍历时都是往下搜.

代码 -> 2021ICPC欧洲东南部区域赛-K(树的后序遍历+贪心) By : HeartFireY
const int MAXN = 2e5 + 5;
int n;
vi edges[MAXN];
int d[MAXN];  // 每个节点的度数
vi ans;  // 后序遍历
int minidx[MAXN];  // minidx[u]表示以u为根节点的子树所包含的节点的编号的最小值

void dfs1(int u, int fa) {  // 预处理minidx[]:当前节点、前驱节点
	minidx[u] = n;  // 初始化为节点的最大编号

	bool is_leaf = true;  // 记录当前节点是否是叶子节点
	for (auto v : edges[u]) {
		if (v == fa) continue;

		is_leaf = false;
		dfs1(v, u);  // 递归求子树的信息
		minidx[u] = min(minidx[u], minidx[v]);
	}
	
	if (is_leaf) minidx[u] = u;  // 叶子节点的minidx是自己的编号
}

void dfs2(int u, int fa, bool is_root) {  // 树形DP:当前节点、前驱节点、u是否是子树根节点
	vii subtree;  // 对子树中的所有节点v,存{minidx[v],v}
	for (auto v : edges[u]) {
		if (v == fa) continue;

		subtree.push_back({ minidx[v], v });
	}

	if (!subtree.size()) {  // 叶子节点直接加入后序遍历
		ans.push_back(u);
		return;
	}

	sort(all(subtree));  // 将子树中的所有节点按minidx[]升序排列

	if (is_root) {  // u是根节点
		for (auto [idx, v] : subtree) dfs2(v, u, true);  // 节点v是子树的根节点
		ans.push_back(u);
	}
	else {  // u非根节点
		for (int i = 0; i < subtree.size() - 1; i++)  // 遍历前(k-1)棵子树
			dfs2(subtree[i].second, u, true);  // 节点subtree[i].second是子树的根节点

		auto [idx, v] = subtree.back();
		if (idx < u) {
			dfs2(v, u, true);  // 节点v是子树的根节点
			ans.push_back(u);
		}
		else {
			ans.push_back(u);
			dfs2(v, u, false);  // 节点v不是子树的根节点
		}
	}
}

void solve() {
	cin >> n;
	ans.clear();
	for (int i = 1; i <= n; i++) d[i] = 0, edges[i].clear();

	for (int i = 0; i < n - 1; i++) {
		int u, v; cin >> u >> v;
		edges[u].push_back(v), edges[v].push_back(u);
		d[u]++, d[v]++;
	}

	int leaf = 0;  // 编号最小的叶子节点作为搜索起点
	for (int i = 1; i <= n; i++) {
		if (d[i] == 1) {  // 叶子节点
			leaf = i;
			break;
		}
	}

	dfs1(leaf, -1);  // 从编号最小的叶子节点开始搜,其无前驱节点
	dfs2(leaf, -1, false);  // 从编号最小的叶子节点开始搜,其无前驱节点,它不是原树中的根节点

	for (int i = 0; i < ans.size(); i++) cout << ans[i] << " \n"[i == ans.size() - 1];
}

int main() {
	CaseT  // 单测时注释掉该行
	solve();
}


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值