CF660C Hard Process
原题链接 https://www.luogu.com.cn/problem/CF660C
题解
如果直接枚举左右端点,再统计区间内 \(0\) 的数量是否 \(<=k?O(n^3)\)
考虑对于区间 \([l,r]\) 和区间 \([l,r+1]\),\(0\) 的数量差仅取决于 \(a[r+1]\),所以枚举右端点时 \(0\) 的数量可以直接转移。\(o(n^2)\)。
因为实际上我们在贪心地选取一个最长的 \(0\) 的个数不多于 \(k\) 的区间,所以当右端点取得的 \(0\) 的个数大于 \(k\) 的位置时就可以不用继续向右枚举了,而我们右移左端点时也可以用之前统计的 \(0\) 的数量直接转移,从区间 \([l,r]\) 转移到区间 \([l+1,r]\) 仅取决于 \(a[l]\)。
这样我们就得到了一个“不停把右端点向右移,\(0\) 数量 \(>k\) 时就右移左端点直到 \(0\) 数量 \(<=k\)” 的方法,时间复杂度 \(O(n)\)。
这种做法有一堆名字,“\(Two-Pointers\)”“尺取法”“滑动窗口”\(......\)
\(Code:\)
#include<iostream>
#include<cstdio>
using namespace std;
const int N=3e5+5;
int n,m,l,r,L,R,sum,ans;
int a[N];
int main()
{
scanf("%d %d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
if(m==0) //注意特判m=0的情况,此时的问题转化为求最长连续子序列
{
int now=0; //记录目前的连续子序列的长度
for(int i=1;i<=n;i++)
{
if(a[i]) now++;
else now=0;
ans=max(ans,now); //连续子序列断了,now要清空
}
printf("%d\n",ans);
for(int i=1;i<=n;i++) printf("%d ",a[i]);
return 0;
}
l=r=1;sum=a[1]==0; //sum表示[l,r]内有多少个0
while(l<=r&&r<=n)
{
if(sum<=m) //如果[l,r]内0的个数小于sum,那么我们更新一下答案,并继续将右端点右移
{
if(r-l+1>ans)
{
L=l;R=r; //记录将哪个答案区间进行修改
ans=r-l+1;
}
r++;
sum+=a[r]==0; //维护新区间的信息
}
else //否则右移左端点
{
sum-=a[l]==0; //维护新区间的信息
l++;
}
}
printf("%d\n",ans);
for(int i=1;i<=n;i++)
{
if(L<=i&&i<=R) printf("1 "); //将更改后的区间0变为1
else printf("%d ",a[i]); //其余的按原来的输出即可
}
return 0;
}
\(Another\) \(Solution:\)
由上面的贪心思路可知,对于每一个区间左端点 \(l\),我们只关心区间内 \(0\) 的个数 \(<=k\) 的最靠右的右端点 \(r\),而由于 \(r\) 越靠右,区间内 \(0\) 的个数不会减少,所以最优的 \(r\) 肯定只有 \(1\) 个,我们可以通过预处理来求:
\(S[i]:\) 表示前 \(i\) 个数中有多少个 \(0\)(包含 \(i\)),那么对于区间 \([l,r]\) 内的 \(0\) 的个数,我们可以利用前缀和思想通过 \(S[r]-S[l-1]\) 来 \(O(1)\) 算出。
那么如何 \(O(1)\) 地求出最优的 \(r\) 呢?
我们再设 \(r[i]\) 记录在一些前缀和为 \(i\) 的数中最大的那个数是多少,注意前缀和是记录的 \(0\) 的个数。
这样的话,对于一个 \(l\),它的最优右端点一定是 \(r[S[l-1]+k]\)。
解释一下:\(S[l-1]\) 是 \(l-1\) 前面有多少个 \(0\),\(k\) 是最优情况下区间 \([l,r]\) 内 \(0\) 的数量,相加就是 \(r\) 前面有多少个 \(0\),及 \(r\) 的前缀和我们就可以确定出来了,再通过上面的数组就可以求得这个最优的 \(r\)。
注意一个细节:当整个序列的 \(0\) 的数量 \(<=k\) 时,我们是找不到 \(r[S[l-1]+k]\) 的,所以如果 \(r[i]==0\) 我们将其赋值为 \(r[i]=n\),表示区间的右端点最大是 \(n\)。
时间复杂度 \(O(n)\)。
\(Code:\)
#include<iostream>
#include<cstdio>
using namespace std;
const int N=3e5+5;
int n,m,L,R,ans; //[L,R]是最优的修改区间
int a[N],S[N],r[N]; //S[i]表示i前面有多少个0,r[i]记录在一些前缀和为i的数中最大的那个数是多少
int main()
{
scanf("%d %d",&n,&m);
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
if(!a[i]) S[i]=S[i-1]+1; //算前缀和
else S[i]=S[i-1];
r[S[i]]=max(r[S[i]],i); //更新一下r数组
}
for(int i=1;i<=n;i++)
if(!r[i]) r[i]=n; //将r[i]=0的部分赋值为r[i]=n
for(int i=1;i<=n;i++) //枚举左端点
{
if(r[S[i-1]+m]-i+1>ans) //贪心地找出了右端点r[S[i-1]+m]
{
L=i;R=r[S[i-1]+m]; //如果更优就更新答案
ans=r[S[i-1]+m]-i+1;
}
}
printf("%d\n",ans);
for(int i=1;i<=n;i++)
{
if(L<=i&&i<=R) printf("1 "); //注意将修改区间全部输出1
else printf("%d ",a[i]); //其余的按原来的输出即可
}
return 0;
}