Codeforces Round 898 (Div. 4)
title: Codeforces Round 898 (Div. 4)
categories:
- ICPC
tags:
- null
abbrlink: e2f7c3b1
date: 2024-03-03 00:00:00
由于题目补完才来写总结,导致前面的有的题目都需要重新再看一遍,只好当作复习了。
但考虑到趁热打铁,先看H.
#H题:
- 从小白视角来看乍一看是博弈论,仔细思考以后发现是图论。本题给的是基环树,意识到这一点很重要,这个条件是让本题不是很复杂的关键。n个点n条边且没有重边保证这个联通图中只有一个环。由于瓦能够预测马去哪,当双方在环上时,瓦永远不会被抓到。若开始时瓦不在环上,则瓦的最优策略是找到去环上的最短路,而马的策略尽可能先到环去拦截瓦。
梳理一下逻辑顺序,在环上找到离b最近的一点u,a如果能提前到u,则b就会被拦截。只需比较a到u的最短路和b到u的最短路大小就可以。
以上为算法思路,下面考虑用什么去实现上面这个思路?
首先dfs找到环,再bfs得到瓦到环上哪个点最近,确定u点后,再bfs找到a到u的最短距离。
bfs找最短路过程朴素,注意力放到dfs过程我学习到的点:
1.记录par父节点数组是为了能遍历环的时候找回去,找到环中距离b点最近的点。
2.对于只有一个环,利用dep深度数组去找,如果当前点x可拓展的点深度和dep[x]大小差2还能连在一起,则起码构成一个最小的三角环,建议自己画图理解dfs深度和环的关系。
#include <bits/stdc++.h>
using i64 = long long;
void solve() {
int n, a, b;
std::cin >> n >> a >> b;
a--, b--;
std::vector<std::vector<int>> adj(n);
for (int i = 0; i < n; i++) {
int u, v;
std::cin >> u >> v;
u--, v--;
adj[u].push_back(v);
adj[v].push_back(u);
}
//在连通图中找到所有点离某个点的最短距离,返回的是一个vector
//内部lambda函数的好处是可以不用预处理,随时执行,并可以将函数定义为变量
auto bfs = [&](int s) {
std::vector<int> dis(n, -1);
dis[s] = 0;
std::queue<int> q;
q.push(s);
while (!q.empty()) {
int x = q.front();
q.pop();
for (auto y : adj[x]) {
if (dis[y] == -1) {
dis[y] = dis[x] + 1;
q.push(y);
}
}
}
return dis;
};
auto dis = bfs(b);
int u = -1;
std::vector<bool> vis(n);
std::vector<int> par(n), dep(n);
auto dfs = [&](auto self, int x) -> void {
vis[x] = true;
for (auto y : adj[x]) {
if (!vis[y]) {
par[y] = x;
dep[y] = dep[x] + 1;
self(self, y);
} else if (dep[y] < dep[x] - 1) {//利用深度关系找到环
for (int i = x; ; i = par[i]) {
if (u == -1 || dis[i] < dis[u]) {
u = i;//dijstra的模式找最短路,把第一次的情况u==-1的情况也考虑进去,可以一行特判我们应该固化下来
}//找到环中距离b点最近的点
if (i == y) {
break;
//环走完了,该及时退出循环
}
}
}
}
};
dfs(dfs, 0);
if (bfs(u)[a] > dis[u]) {
std::cout << "YES\n";
} else {
std::cout << "NO\n";
}
}
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int t;
std::cin >> t;
while (t--) {
solve();
}
return 0;
}
G题
个人认为官方题解这题写得不错,故搬运一下。
序言
在开始编辑之前,我想先说明一下,这类问题并不是第 4 单元的传统问题,它比通常的问题更具有临时性。我的目的是制作一个 “入门级 “的临时问题,希望大家喜欢。这篇社论有点长,主要是因为我想详细介绍如何处理这类临时问题,因为通常大多数 Div.4 问题都不像这样,而是侧重于注意和执行正确的算法。我不认为任何人在解题时真的会明确地去看这些细节,但我想为不知道如何入手的人提供一个非常明确的攻略。
但我认为临时技能对 Codeforces 尤为重要,所以才有了这个问题。也许你很快会看到更多。)
分析形势
让我们先想想如何重复使用运算。如果我们有一个字符串$\texttt{AAAB}$,那么就可以进行运算$\texttt{AAAB} \to \texttt{AABC} \to \texttt{ABCC} \to \texttt{BCCC}$。同样,如果我们有一个字符串$\texttt{BAAA}$,那么就可以进行操作$\texttt{BAAA} \to \texttt{CBAA} \to \texttt{CCBA} \to \texttt{CCCB}$。
从某种意义上说,把$\texttt{B}$看作是 “吃掉”$\texttt{A}$是很有用的:只要$\texttt{B}$挨着$\texttt{A}$,它就会吃掉$\texttt{A}$,吃掉它,然后继续前进。注意,$\texttt{B}$不能吃掉$\texttt{C}$。
我们可以这样替换这些字符:用$\texttt{.}$替换$\texttt{A}$,用$\texttt{_}$替换$\texttt{C}$。这样就很清楚是怎么回事了。下面是对字符串$\texttt{BAAABA}$的一系列移动,在替换之后,字符串$\texttt{BAAABA}$变成了$\texttt{B…B.}$:
$$
\texttt{B…B.} \to \texttt{_B..B.} \to \texttt{__B.B.} \to \texttt{___BB.} \to \texttt{___B_B}
$$
现在您可以看到每一个$\texttt{B}$是如何在一个方向上吃掉所有的点(它无法在$\texttt{_} = \texttt{C}$所代表的空白处移动)。
解决问题
好了,我们有了这种直觉,那么现在该如何解决问题呢?我们需要吃掉最大数量的点(原始字符串中的$\texttt{A}$),因为$\texttt{B}$不能吃掉其他任何东西。注意,对于每个$\texttt{B}$来说,它可以吃掉它左边或右边的所有$\texttt{A}$,但***不能同时吃掉;一旦它离开了原来的位置,就会被困在那一边:
$$
\texttt{..B.} \to \texttt{.B_.} \to \texttt{B__.}
$$
但$\texttt{B}$永远无法到达最右边的点。这个小例子实际上为我们提供了一条线索:对于每一个$\texttt{B}$,让我们数一数两边的$\texttt{A}$个数,然后选取最大的一个。这可能行得通,但要注意的是,多个$\texttt{B}$可能会到达同一个点。
这就是我们的原始字符串发挥作用的地方。假设我们的原始字符串以 $\texttt{B}$ 开始,那么它的形式必须是
$$
\texttt{B} \underbrace{\texttt{A} \dots \texttt{A}}{\text{some number, possibly } 0} \texttt{B} \underbrace{\texttt{A} \dots \texttt{A}}{\text{some number, possibly } 0} \texttt{B} \underbrace{\texttt{A} \dots \texttt{A}}{\text{some number, possibly } 0} \dots
$$
那么每一个$\texttt{B}$都可以简单地吃掉它后面的所有$\texttt{A}$,因此我们的答案就是$\texttt{A}$的总数。同样,如果我们的字符串以 $\texttt{B}$ 结尾,那么它的形式一定是
$$
\dots\underbrace{\texttt{A} \dots \texttt{A}}{\text{some number, possibly } 0} \texttt{B} \underbrace{\texttt{A} \dots \texttt{A}}{\text{some number, possibly } 0} \texttt{B} \underbrace{\texttt{A} \dots \texttt{A}}{\text{some number, possibly } 0} \texttt{B}
$$
那么每个$\texttt{B}$都可以简单地吃掉它前面的所有$\texttt{A}$,因此我们的答案就是$\texttt{A}$的总数。
所以我们只需要考虑字符串以$\texttt{A}$开始和结束的情况,也就是我们的字符串看起来像这样:
$$
\underbrace{\texttt{A} \dots \texttt{A}}{\text{some number, possibly } 0} \texttt{B} \underbrace{\texttt{A} \dots \texttt{A}}{\text{some number, possibly } 0} \dots ,,,,,,,, \texttt{B} \underbrace{\texttt{A} \dots \texttt{A}}{\text{some number, possibly } 0}
$$
如果任何一个 $\texttt{B}$ 相邻,例如
$$
\underbrace{\texttt{A} \dots \texttt{A}}{\text{some number, possibly } 0} \texttt{B} \texttt{B} \underbrace{\texttt{A} \dots \texttt{A}}_{\text{some number, possibly } 0}
$$
那么请注意,我们可以将字符串拆分成两个字符串,这两个字符串要么以 $\texttt{B}$ 开始,要么以 $\texttt{B}$ 结束,因此我们可以吃掉所有的 $\texttt{A}$。因此,在这种情况下,我们也可以得到所有的$\texttt{A}$。
还有一种情况是什么?起点和终点都是$\texttt{A}$,而且没有两个$\texttt{B}$相邻。在这种情况下,你可以看到$\texttt{A}$的 “组 “比$\texttt{B}$多一个,但是每个$\texttt{B}$只能得到一组$\texttt{A}$。因此,我们无法得到所有的$\texttt{A}$。
我们能做得最好的是什么?请注意,每个$\texttt{B}$只能得到一组,无论它是向左还是向右,因此我们可以得到除了一组之外的所有$\texttt{A}$。现在的答案很简单:答案就是$\texttt{A}$的总数减去最小的一组(因为我们想得到最多的硬币,所以我们会尽可能取大的组)。
你也可以把它想象成一种贪心:每个$\texttt{B}$取最大的一组,一旦没有多余的$\texttt{B}$,我们就停止。
找组的时间复杂度为$\mathcal{O}(n)$,因此整个解法也是在这个时间内完成的。
// Problem: G. ABBC or BACB
// Contest: Codeforces - Codeforces Round 898 (Div. 4)
// URL: https://codeforces.com/contest/1873/problem/G
// Memory Limit: 256 MB
// Time Limit: 1000 ms
//
// Powered by CP Editor (https://cpeditor.org)
#include <bits/stdc++.h>
using namespace std;
#define ll long long
//# define int long long
#define ull unsigned long long
#define pii pair<int,int>
#define double long double
#define baoliu(x, y) cout << fixed << setprecision(y) << x
#define endl "\n"
const int N = 2e5 + 10;
const int M = 1e6 + 10;
const int inf = 0x3f3f3f3f;
const int mod = 998244353;
const double eps = 1e-8;
int n, m;
int a[N];
void solve(){
string s;
cin>>s;
int n=s.size();
bool flag=false;
if(s[0]=='B'||s[n-1]=='B')flag=true;
for(int i=0;i<n-1;i++){
if(s[i]=='B'&&s[i]==s[i+1])flag=true;
}
vector<int>ans;
int cnt=0;
for(int i=0;i<n;i++){
if(s[i]=='A')cnt++;
else{
ans.push_back(cnt);
cnt=0;
}
}
if(cnt)ans.push_back(cnt);//对于结尾没有B进行判断
//对于1个B没有的情况进行特判
if(ans.empty()){
cout<<0<<endl;
return ;
}
sort(ans.begin(),ans.end());
for(auto x:ans)cerr<<x<<endl;
int res=0;
if(flag)res+=ans[0];
for(int i=1;i<ans.size();i++){
res+=ans[i];
}
cout<<res<<endl;
}
int main() {
cin.tie(0);
cout.tie(0);
ios::sync_with_stdio(false);
int t;
cin>>t;
while (t--) {
solve();
}
return 0;
}
g题首先需要自己多试样例发现重要规律,就是根据变换规则,b能处理它紧靠左边所有的a或者右边,也就收只能选一边,从最简单情况考虑,就是b的数量足够能够把所有a消灭,那答案就是字符串中a的个数。那怎么衡量b的数量有多足够呢?
当b开头或者b结尾或者既在头又在头又在尾,我们思考发现可以把所有a消灭掉。
在开始思考这题的时候,我们会觉得很杂很乱,脑子里觉得有很多情况。我们需要多看样例,尽力去划分问题,从一种情况看起,找到一种解法,然后再考虑其他情况,看看能不能划归。
对于aaaaabbbbbbbaaaaa的情况,我们会发现中间的b可以删到只剩两个,然后以2个b中间为分界线,拆成两个串,左边串以b结尾,右边串以b开头,问题划归到上面所述情况。
上面探讨的都是b数量足够的情况,如果不够也只是会少处理一段a,那我们要做的就是选出少处理哪一段,题目中要求最大值,我们只需要放弃连续a数量最少的一段即可。
E和F都是很明显能二分,但发现F可以不用二分用双指针优化到$O(n)$,具体可以看jls代码,我还没来及仔细琢磨。
知乎借鉴:双指针扫一遍,满足前面的树高度能被后面的树高度整除时,移动右指针,不满足时左指针直接跳到右指针位置,然后移动右指针,当摘的果子总数超过k时移动左指针,但左指针不能大于右指针
D题就是很朴素的贪心加指针跳跃。
C题官方题解给的也是打表,只不过我是用函数封装进行的数学找规律打表,虽然都是(不正经
正经解法好像是曼哈顿距离类似去判断和中心的距离,不过最终也还是找到数学规律.