由于题目补完才来写总结,导致前面的有的题目都需要重新再看一遍,只好当作复习了。
但考虑到趁热打铁,先看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题官方题解给的也是打表,只不过我是用函数封装进行的数学找规律打表,虽然都是(不正经

正经解法好像是曼哈顿距离类似去判断和中心的距离,不过最终也还是找到数学规律.

B题数学公式推导后发现原命题等价于找到数组中的最小值所在位置即可。