CF11E Forward, march! 详细题解
题目描述
给定一个含 L
, R
, X
的字符串,你可以添加若干 X
,使得对改变后的新字符串无限循环时保证:
- 相同的非
X
字符不能相邻(包括循环前的首尾字符) - 与
LR
循环串的匹配率(同位置字符相同率)最大
求出这个最大匹配率。
详细解析
由于输入的字符串可能出现 LL
或者 RR
的情况,而最后的答案必须要在它们之间加上一个 X
,因此我们不妨在输入后就预处理好,给它们加上 X
。而对于首尾相邻的情况,这就需要考虑到最后的匹配率情况来进行 X
的添加。如果首尾是 LL
,那么将 X
加在最后是最好的;如果是 RR
,那么 X
加在开头是最好的。
为什么暂时不需要考虑中间的呢?这是因为无论如何它们之间必须要加,而如何在别的地方加 X
来控制它们的位置,这就是我们后面需要做的事了。
而为什么后面的处理无法考虑好前面的问题呢?这就和我们选择的二分判断策略有关了,我们到后面再回答。
在进行我们的算法之前,我们需要知道一个事实:改变后的新字符串长度为偶数最佳。
如果长度为奇数最佳,那么我们将其倍长后会发现,前后串对应的 LR
是错开的,那么得到的最大匹配率就会等于 \(\dfrac{cnt_L+cnt_R}{2Len}\)。如图所示:
我们假设之前的奇数串倍长后的两串贡献分别是 \(x_1,x_2\),那么原来的匹配率就可以写成 \(\dfrac{x_1+x_2}{2Len}\)。
如果此时有 \(x_1>x_2\),那么我们可以在原来这个奇数串后面加上 X
,倍长后得到新的匹配率就是 \(\dfrac{2x_1}{2Len+2}\)。后者减去前者,只看分子部分有:\(2x_1Len-x_1Len-x_1-x_2Len-x_2=(x_1-x_2)Len-(x_1+x_2)\)。
再者,对于奇数串,必然有 \(x_1+x_2<Len\)。如果 \(x_1+x_2=Len\),要么它会发生首尾相接的情况,要么其内部必然会出现 LL
或 RR
,这与我们已经处理好的前提相悖。这样我们就证明了这种情况得到的最优情况必然是偶数串。
那如果是 \(x_1=x_2\) 呢?我们可以看到上面的图就是这种情况,我们可以试着在中间加上 X
。我们发现,如果在某个位置插入一个 X
,那么后面的所有贡献就会反过来,我们只需要找到某个位置,满足插入 X
后会导致 \(x_1>x_2\) 即可。那如果导致 \(x_1<x_2\) 的位置行不行呢?答案是否定的,因为变成偶数串后,必须是按照 LRLR...
的顺序来匹配的。
然而,的确存在一类情况,使得没有任何位置能只插入一个 X
使得 \(x_1>x_2\)。举个例子:RXL
:
我们会发现,无论如何插入一个 X
,得到的匹配字符个数不会超过 \(1\)。对于此类情况,我们可以进行如下操作:首先在字符串首插入一个 X
,在第一个非 X
字符后面插入一个 X
,在第二个非 X
字符后面插入一个 X
……直到插入完这 \(2x_1+1\) 个 X
。此时串长变成了 \(Len+2x_1+1\),\(x'_1=2x_1\)。那么我们再计算一下匹配率之差:这里我们全用变量 \(x_1\) 代替,原来的匹配率为 \(\dfrac{x_1}{Len}\),现在为 \(\dfrac{2x_1}{Len+2x_1+1}\),作差并观察分子:\(2x_1Len-x_1Len-2x^2_1-x_1=x_1(Len-1)-2x^2_1=x_1(Len-2x_1-1)\)。
我们不难知道,奇数串中,要想满足 \(x_1=x_2\) 情况,必然有 \(2x_1+1\leq Len\),故而上述作差大于等于零,所以此情况下偶数串不劣于奇数串。
综上所述,改变后的新字符串长度为偶数最佳。
为什么我们要费尽周折来证明这样一个东西呢?因为我们采用的算法过程需要。这里就可以开始介绍一下这个算法步骤了:首先答案的这个百分比进行二分,在二分的判断过程,我们采用 dp 方式进行判断。当我们枚举到某一位置 \(i\) 的时候,如果其对应的无穷 LR
串的字符是 L
,那么它的状态就是 \(0\),如果是 R
,那么状态就是 \(1\)。如果不摆放 X
,那么上一位转移到这一位必然是 \(0\to1,1\to0\),对应的状态值需要加上是否有 L,R
,并减去百分比(因为是平均数,所以每个位置都要减去)。那如果前面放了 X
,那么我们就需要再一次转移:\(0\to 1,1\to 0\),同时由于添加了一个 X
,需要减去百分比,只不过此时必须对答案取 \(\max\)。写成转移公式就是:
\(f[0][0]\) 初始化为 \(-\bar{x}\) 的原因是 \(i=1\) 应当紧接 L
匹配,那么 \(i=0\) 就应当是 R
匹配,而这里是 L
匹配,那么必然会添加一个 X
。
前者转移不需要取,是因为其是必然发生的转移,后者取是需要保证答案更优。
而为什么至多只进行一次 X
的添加,这是因为前面我们已经处理好了相邻相同的情况,而证明中提到的更优解或者等价解都是在每个可能的字符前面至多添加一个 X
产生的,因此,多进行一次添加 X
的转移不会使答案更优。
而注意到我们提到在预处理过程中的首尾相接相同的情况的处理方式,其解释如下:
我们讨论 L
相邻的情况,对 R
同理。如果在最前面加上 X
,根据特殊串 L
,发现变成 XL
是不合理的,可以否定这个预操作(如果后期 dp 发现可以那是后期的事)。而可以直接放在最后而不影响解的最优化,原因如此:
如果发生我们提到过的情况,对于 \(x_1>x_2\) 的情况显然这么做是可以的;但是对于 \(x_1<x_2\) 的情况,我们前面仅提到在最前方加上一个 X
更优,但它不是最优,如图例:
其实我们可以类似证明中提到的最后一种情况的操作,将所有的 L
和 R
都对应上,假设操作了 \(v\) 次,那么此时的匹配率为:\(\dfrac{x_1+x_2}{Len+v}\)。减去最初的匹配率,看分子得到:\((x_1+x_2)(Len-v)\)。由于我们至少可以保持第一个 L
前面不增加 X
,那么一定会有 \(Len\geq v\)。而这种一一对应的情况必然有第一个 L
前面不添加 X
,那么必须就有 X
添加在最后一个 L
之后。
对于 \(x_1=x_2\) 的情况,其中的第一类可找到插入点的情况可以类似前面的方法使得可以添加一个 X
在最后,第二类不可找的情况就如同证明中所述的步骤,也能在最后添加一个 X
。这些都能保证答案最优化不变。
至于奇偶串的情况,我们可以对放或不放这个 X
均做如此讨论,两种讨论奇偶不同,结果是一样的。
于是我们就不难写出代码了:
#include <bits/stdc++.h>
using namespace std;
typedef double db;
const db eps = 1e-9;
const int maxn = 1e6+5;
db f[maxn<<1][2];
int n, tot;
char s[maxn], cg[maxn<<1];
bool check(db avg)//转移方程判断
{
f[0][0] = -avg, f[0][1] = 0;
for(int i = 1; i <= tot; i += 1)
{
f[i][0] = f[i-1][1] + (cg[i]=='L') - avg;
f[i][1] = f[i-1][0] + (cg[i]=='R') - avg;
f[i][0] = max(f[i][0], f[i][1]-avg);
f[i][1] = max(f[i][1], f[i][0]-avg);
}
return f[tot][1]>=0;
}
int main()
{
scanf("%s", s+1);
n = strlen(s+1);
if(s[1] == s[n] && s[1] == 'R')//首尾 R 相连,在前面放 X
cg[++tot] = 'X';
for(int i = 1; i <= n; i += 1)
{
if(s[i] == s[i-1] && s[i] != 'X')
cg[++tot] = 'X';//相邻相同,中间插入 X
cg[++tot] = s[i];
}
if(s[1] == s[n] && s[1] == 'L')//首尾 L 相连,在后面放 X
cg[++tot] = 'X';
db l = 0, r = 100;
while(fabs(l-r)>eps)//二分百分比
{
db mid = (l+r)/2;
if(check(mid/100))
l = mid;
else
r = mid;
}
printf("%.6lf", (int)(r*1e6)/(1e6));//特殊处理,需与答案一致
return 0;
}
该算法复杂度是 \(O(n\log (eps)^{-1})\)。不过这里精度判断没有 spj,所以需要一些特殊处理。
新思路
这个思路是 CF 上一位红名大佬 \(\text{mkirsche}\) 在评论提到的,具体评论点我。下面是我的理解与阐释:
我们可以添加最少的 X
,使得所有 L
和 R
到达能产生贡献的位置,并且是偶数串,如果此时的匹配率大于 \(50\%\),那么我们就不断删掉两个 X
,这些 X
满足其中夹着刚好一个匹配的 L
和 R
,并且删去后不会发生 LL
相连或者 RR
相连的情况。最后得到的是一个最优串。
其实我们发现每次删去两个 X
,它们之间的那个字符失配,但是也有两个失配字符被删去,其它字符匹配性不变。我们设原来匹配的字符个数为 \(x\),那么原来的匹配率就是 \(\dfrac{x}{Len}\),现在是 \(\dfrac{x-1}{Len-2}\),后者减去前者,看分子有:\(2x-Len\)。也就是说,只有匹配率大于 \(50\%\),我们这么操作才能增加匹配率。
关于这个思路正确性的讨论,其实可以类似于上面我们做的,对比前后的匹配率,讨论好所有情况即可。
这个算法复杂度是 \(O(n)\) 的,相对更优,实现如下:
#include <bits/stdc++.h>
using namespace std;
typedef double db;
const db eps = 1e-9;
const int maxn = 1e6+5;
db f[maxn<<1][2];
int n, tot, lst, mt, Xpos[maxn];
char s[maxn], cg[maxn<<1];
int main()
{
scanf("%s", s+1);
n = strlen(s+1);
if(s[1] == s[n] && s[1] == 'R')
cg[++tot] = 'X';
mt = 0;//看有目前处于第几个非 X 的字符
for(int i = 1; i <= n; i += 1)
{
if(s[i] == s[i-1] && s[i] != 'X')
cg[++tot] = 'X';//必须要加的 X 后面不可删
if(tot+1&1 && s[i] == 'R' || !(tot+1&1) && s[i] == 'L')
cg[++tot] = 'X', Xpos[++lst] = mt;//可以删的 X 位置需要记录
cg[++tot] = s[i];
if(s[i] != 'X')
mt += 1;
}
if(s[1] == s[n] && s[1] == 'L')
cg[++tot] = 'X';
if(tot & 1)//变成偶数序列,与我们前面证明的一致
{
cg[++tot] = 'X';
if(s[1] != s[n])//包括 XL,XR 相邻的情况也要记录
Xpos[++lst] = mt;
}
int matched = 0, fail = 0;
for(int i = 1; i <= tot; i += 1)//统计匹配与失配字符
{
if(cg[i] != 'X')
matched += 1;
else
fail += 1;
}
for(int i = 1; i <= lst; i += 1)
{
if(matched <= fail)
break;
if(Xpos[i] + 1 == Xpos[i+1])//中间只间隔一个 L 或者 R,就可以删
matched -= 1, fail -= 1, i += 1;
}
long long rat = (long long)matched*100000000/(matched+fail);
printf("%.6lf", rat/1e6);
return 0;
}
总结
两个解答方式对比,第一个其实是像 \(\text{YLWang}\) 的题解所说的,类似于 \(01\) 规划的方式进行二分答案求解。但是其约束就在于需要证明偶数串可行。然而在证明的过程中我们可以发现这其中具有可以贪心的性质,也就是先将其可匹配的部分匹配上,然后再去进行修正操作。而这样一来,我们的复杂度就降低了一个 \(\log\),是更优的解法。我觉得 codeforces 向来不太卡常,这题 1s+1e6 估计也是想用这种方法来考验我们的,所以正解应该是后者。
通过写此题题解,让我对算法又深入理解与体会,也希望能帮助大家深入感受一下这道题的优秀之处。