2021ICPC欧洲东南部区域赛题解ACFGJKLN
A. King of String Comparison
题意
给定两长度为 n ( 1 ≤ n ≤ 2 e 5 ) n\ \ (1\leq n\leq 2\mathrm{e}5) n (1≤n≤2e5)的字符串 s s s和 t t t,求使得子串 s [ l ⋯ r ] s[l\cdots r] s[l⋯r]的字典序 < < <子串 t [ l ⋯ r ] t[l\cdots r] t[l⋯r]的字典序的 ( l , r ) ( 1 ≤ l ≤ r ≤ n ) (l,r)\ \ (1\leq l\leq r\leq n) (l,r) (1≤l≤r≤n)的对数.
思路
从左往右扫一遍,用双指针维护子串的左右端点.
代码 -> 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 (1≤n≤2e5).第二行输入 ( 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 (0≤ai≤1e9).第三行输入 ( 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 (0≤bi≤1e9).
若能通过若干次切割获得足够数量的纸,输出最小切割次数;否则输出 − 1 -1 −1.
思路I
从前往后看数量是 2 n 2^n 2n的增长,不妨考虑从后往前看.
考察最小尺寸 A n A_n An.①若 b n ≤ a n b_n\leq a_n bn≤an,则已有的尺寸为 A n A_n An的纸已满足需求,再继续切割显然是不优的.
②若 b n > a n b_n>a_n bn>an,则至少需切割尺寸为 A n − 1 A_{n-1} An−1的纸 ⌈ b n − a n 2 ⌉ \left\lceil\dfrac{b_n-a_n}{2}\right\rceil ⌈2bn−an⌉次.
先不考虑尺寸为 A n − 1 A_{n-1} An−1的纸存量是否够,只令 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 an−1−=⌈2bn−an⌉,ans+=⌈2bn−an⌉.
从后往前重复上述过程,最后检查 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 (1≤n≤1e5)的且只包含字符’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+cntB−cntCy=2cntA−cntB+cntCz=2−cntA+cntB+cntC,则有解的必要条件是 x , y , z ∈ N x,y,z\in\mathbb{N} x,y,z∈N.
考察’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=n−cnta个需作为左括号,将它们加入队列中,它们将产生"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+P⋅p−R⋅r(该值可能为负,如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} hpstart−hpend的最大值.注意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 (1≤N,X,R,P,K≤1e6,0≤K≤N).第二行输入一个长度为 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 (1≤i≤N)轮BOSS不念再生咒语,则本轮造成的伤害为 ( N − i + 1 ) ⋅ P (N-i+1)\cdot P (N−i+1)⋅P;否则本轮造成的伤害为 ( N − i + 1 ) ⋅ ( P + R ) (N-i+1)\cdot (P+R) (N−i+1)⋅(P+R),并 a n s − = L ( N − i + 1 ) ⋅ R ans-=L(N-i+1)\cdot R ans−=L(N−i+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 1∼k轮念中毒咒语.每次考察最靠后一个①类型的中毒咒语位置换为第一个②类型的中毒咒语位置是否会让答案更优.过程用双指针维护(下面的代码未明显写出双指针,但本质相同),总时间复杂度 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,bi≥1,1≤i≤2n),考察一张由 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> (1≤i,j≤2n,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{∣ai−aj∣,∣ai−bj∣,∣bi−aj∣,∣bi−bj∣}.从中选出 n n n条边,使得任意两条选中的边无公共顶点,求选出的边的边权之和的最大值.
第一行输入一个整数 n ( 1 ≤ n ≤ 1 e 5 ) n\ \ (1\leq n\leq 1\mathrm{e}5) n (1≤n≤1e5).接下来 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 (1≤a,b≤1e9).
思路
因选择的 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 ai≤bi,否则交换 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=1∑2nbi,考察将其中的 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 (1≤n≤3e5).第二行输入一个长度为 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 (1≤l≤r≤3n)和一个字符 c ∈ { A , B , C } c\in\{A,B,C\} c∈{A,B,C},表示将子串 s [ l ⋯ r ] s[l\cdots r] s[l⋯r]中的全部字符换为 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[1⋯p]是 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+n−cntb)]替换为’B’,将子串 s [ ( p + n − c n t b + 1 ) , 3 n ] s[(p+n-cntb+1),3n] s[(p+n−cntb+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'和 个′B′和n 个 ′ 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'的个数 中′B′和′C′的个数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 1∼n的 n n n个节点的树,其中第 i ( 1 ≤ i ≤ n ) i\ \ (1\leq i\leq n) i (1≤i≤n)个节点的颜色为 c i c_i ci.好的连通子图满足:子图中的节点中有一个颜色的数量严格大于该子图的节点数的一半,称该颜色为该子图的主要颜色.求该树的好连通子图的个数,答案对 998244353 998244353 998244353取模.
第一行输入一个整数 n ( 1 ≤ n ≤ 3000 ) n\ \ (1\leq n\leq 3000) n (1≤n≤3000).第二行输入 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 (1≤ci≤n,1≤i≤n).接下来 ( n − 1 ) (n-1) (n−1)行每行输入两个整数 u , v ( 1 ≤ u , v ≤ n , u ≠ v ) u,v\ \ (1\leq u,v\leq n,u\neq v) u,v (1≤u,v≤n,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<sum≤cntc的状态,因为无法得到更大或更小的点权和.设当前子树大小为 s i z siz siz.注意到当前状态只能转移到满足 ∣ s u m ∣ ≤ s i z |sum|\leq siz ∣sum∣≤siz的状态,故转移只需考虑满足 ∣ s u m ∣ ≤ min { c n t c , s i z } |sum|\leq\min\{cnt_c,siz\} ∣sum∣≤min{cntc,siz}的情况.计算每个主要颜色的时间复杂度为 O ( n ⋅ c n t c ) O(n\cdot cnt_c) O(n⋅cntc),则总时间复杂度 ∑ 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) c∑O(n⋅cntc)=O(n⋅c∑cntc)=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 (1≤t≤1e5)组测试数据.每组测试数据第一行输入一个整数 n ( 2 ≤ n ≤ 2 e 5 ) n\ \ (2\leq n\leq 2\mathrm{e}5) n (2≤n≤2e5),表示树的节点数.接下来 ( n − 1 ) (n-1) (n−1)行每行输入两个整数 u , v ( 1 ≤ u , v ≤ n , u ≠ v ) u,v\ \ (1\leq u,v\leq n,u\neq v) u,v (1≤u,v≤n,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) (k−1)棵.
①若 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();
}