尺取法的巧解字符串平衡
方法概述
尺取法,又称双指针法。正如其名,这种方法的功能主要体现于双指针上。在对一个数据序列操作时,维持两个模拟指针(使用哨兵FLAG)和相应的数组下标变化来模拟指针的指向移动,能够解决数组存储的数据结构中区域类型的问题。方便后面题目的理解,举出一道经典的尺取法板子题来详解这种基础算法。
基础题目
给定一个大小为n数组和一个判断值m,在满足有合法解的条件下,求解出满足区间[a,b]之间的整数和>m的最小区间长度。
算法分析
很明显解决这种问题,只通过遍历数组并且记录是无法完成的。因为在遍历过程中受制于遍历下标只有一个,我们无法控制区间变大变小,由此尺取法被引入。使用指针L&R来记录两个下标,找到满足条件的解,便是尺取法的核心思想。(因为它的这个性质我总是觉得称之为双指针法更为容易记忆)
回到题目,我们可以总结出如下的具体算法:
维护双指针L&R,并初始化为1,维护sum值,初始化为a[0]
当sum<m时,代表双指针符合要求,要进行条件的松弛,也就是R++
当sum>=m时,代表双指针状态符合要求,使用(R-L+1)更新最小区间大小;为了找出最小区间,缩小区间,L++,并注意更新sum值;
==注意:==在R<L时,要进行R=L。(可能这里你会奇怪为什么会出现这种情况哎?)仔细阅读源码就可以找到答案。
问题源码(c++)
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
int a[1000];
int main()
{
int number=0;
cin>>number;
for(int i=0;i<number;i++)
{
cin>>a[i];
}
int sum_value=0;
cin>>sum_value;
int L=0;
int R=0;
int ans=10000;
int sum_number=a[0];
//整体思路是每次while先移动R到一个解,再移动L,缩小区间长度
while(R<number-1)//这种条件能够保证在R=number-1,也就是最后一个元素的位置,就直接被筛掉,而不会访问没有元素的区间部分
{
//sum_number<sum_value
while(R<number-1 && sum_number<sum_value)//这里的松弛条件要和上面的松弛条件保持一致
{
R++;
sum_number+=a[R];
}
//出循环的条件是sum_number>=sum_value 或者 R已经达到了数组的边界值下标number-1,再++就会越界
ans=min(ans,R-L+1);
while(sum_number>=sum_value && L<=R)
{
sum_number-=a[L];
L++;
if(sum_number>=sum_value) //这里如果仍然满足,一定有R>=L,所以R-L+1>=1一定成立
ans=min(ans,R-L+1);
}
//出循环的条件是sum_number<sum_value 或者 L > R
if(L>R) //如果在某个位置a[i]>sum_value,这时会出现L++后大于R的情况,要把R上移一位,并且初始化sum_value为a[i+1]的值
{
R=L;
sum_number=a[L];
}
}
cout<<ans<<endl;
}
题目概述
如果你是大佬,可以直接来看这里;但如果你还不了解尺取法,请仔细阅读上面的讲解,你会对这种方法有一个大致的认知和概念。下面来看实战:
题目叙述
一个长度为 n 的字符串 s,其中仅包含 ‘Q’, ‘W’, ‘E’, ‘R’ 四种字符。
如果四种字符在字符串中出现次数均为 n/4,则其为一个平衡字符串。
现可以将 s 中连续的一段子串替换成相同长度的只包含那四个字符的任意字符串,使其变为一个平衡字符串,问替换子串的最小长度?
如果 s 已经平衡则输出0
INPUT及输入样例
一行字符表示给定的字符串s
输入样例:
QQWE
OUTPUT及输出样例
一个整数表示答案
输出样例:
1
题目重述
题面十分简单,甚至不需要进行重述,可以明显的看出是道板子题,但最难的是怎样看出来这个板子是尺取法。
为了在下面的算法讲解中你能更好的理解,简化一下题意:
平衡字符串指的是一个字符串含有的4种字母数字相等。
题目需求:给定义一个任意的字符串,字符个数是4的整数(一定可以平衡)。规定一个平衡区间,在平衡区间内你可以随意的修改字符串,目的是让整个字符串是一个平衡字符串,找到最小的平衡区间。
思路解析
通过重述部分的解析,可以明确地知道题意是找出最小区间,保证条件成立,且根据题意该寻找区间的范围是全局。这两个条件让我们不免会想起基础板子的情景,但如何才能将它和基础板子联系起来呢?下面仔细剖析下:
按照平衡区间的定义,区间内部的字符可以随意变化,但外部是不可变的。由此我们可以先考虑区间外,再对区间内松弛即可。即可以将求解思路转换为
调整后的区间外满足平衡+调整后的区间内满足平衡=整个序列满足平衡
理解了上面的思路转换,一切就显得很简单了:
1、对区间外的字符部分对4种字符分别计数sum1\sum2\sum3\sum4,并选出出现次数最多的作为基准MAX_sum。
2、从区间外分出部分字符填充,区间内的字符个数减少
(MAX_sum-sum1)+(MAX_sum-sum2)+(MAX_sum-sum3)+(MAX_sum-sum4)
3、此时调整后区间外已经满足平衡,如果区间内剩余的个数仍满足整除4,则证明该状态满足平衡。
到这里,你会发现状态的判断已经明了,自己上手实现吧!
总结
尺取法主要应用于在一个数值序列的全局范围内寻找符合条件的最小区间类问题。其核心思想是维持两个指针L&R,利用指针对数组访问并找到满足条件的最优解。
题目源码(c++)
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
int a[1000001];
int number=0;
int sum1=0;
int sum2=0;
int sum3=0;
int sum4=0;
int max_one(int a,int b,int c,int d)
{
int maxone=0;
if(a>=maxone)
maxone=a;
if(b>=maxone)
maxone=b;
if(c>=maxone)
maxone=c;
if(d>=maxone)
maxone=d;
return maxone;
}
bool balance_criticize(int x,int y)
{
int maxx=max_one(sum1,sum2,sum3,sum4);
int need_fill_number=maxx*4-sum1-sum2-sum3-sum4;
int free_number=y-x+1-need_fill_number;
if(free_number%4==0 && free_number>=0)
return true;
else
return false;
}
int main()
{
string s;
cin>>s;
int flag1=0;
int flag2=0;
int flag3=0;
int flag4=0;
for(int i=0;i<s.length();i++)
{
if(s.at(i)=='Q')
a[i]=1,flag1++;
else if(s.at(i)=='W')
a[i]=2,flag2++;
else if(s.at(i)=='E')
a[i]=3,flag3++;
else if(s.at(i)=='R')
a[i]=4,flag4++;
}
if(flag1==flag2 &&flag2==flag3 && flag3==flag4)
{
printf("0\n");
}
else
{
number=s.length();
int L=0;
int R=0;
for(int i=1;i<number;i++)
{
if(a[i]==1)
sum1++;
else if(a[i]==2)
sum2++;
else if(a[i]==3)
sum3++;
else if(a[i]==4)
sum4++;
}
int ans=100001;
while(R<number-1)
{
while(R<number-1 && balance_criticize(L,R)==false)
{
R++;
if(a[R]==1)
sum1--;
else if(a[R]==2)
sum2--;
else if(a[R]==3)
sum3--;
else if(a[R]==4)
sum4--;
}
ans=min(ans,R-L+1);
while(balance_criticize(L,R)==true && L<=R)
{
L++;
if(a[L-1]==1)
sum1++;
else if(a[L-1]==2)
sum2++;
else if(a[L-1]==3)
sum3++;
else if(a[L-1]==4)
sum4++;
if(balance_criticize(L,R)==true && R>=L)
ans=min(ans,R-L+1);
}
if(L>R)
{
R=L;
if(a[R]==1)
sum1--;
else if(a[R]==2)
sum2--;
else if(a[R]==3)
sum3--;
else if(a[R]==4)
sum4--;
}
}
printf("%d\n",ans);
}
return 0;
}