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;
}
posted @ 2020-08-03 09:24  暗い之殇  阅读(105)  评论(0编辑  收藏  举报