为了能到远方,脚下的每一步都不能少。|

larryyu_blog

园龄:2年5个月粉丝:5关注:17

2023-09-26 13:48阅读: 26评论: 0推荐: 0

P9566 [SDCPC2023] K-Difficult Constructive Problem 题解

Description

有一个长度为 n01 字符串 s,其中部分位置已给出,在 ? 的位置处需填入一个 10

一个填充方案是好的,当且仅当存在 m 个不同的 i 满足 1i<nsisi+1

求所有好的填充方案中字典序最小的一个,如果无解输出 Impossible

对于两个长度为 n 的字符串 a,b,若存在一个整数 k(1kn),使得所有 1i<kai=biak<bk,则称 a 的字典序小于 b 的字典序。

Solution

设当前 有 wi 满足 1i<nsisi+1

引理:对于 1<i<n,将 si 取反,w 的奇偶性不变,分类讨论证明:

  • 101111,w=w2
  • 111101,w=w+2
  • 010000,w=w2
  • 000010,w=w+2
  • 100110,w=w
  • 110100,w=w
  • 011001,w=w
  • 001011,w=w

Sol1

只有将 s1sn 取反才能将 w 改变奇偶性。

所以将 s1sn 固定后就不用考虑 w 的奇偶性。

对于整个字符串,一定存在 w 能取到的最大值和最小值,设分别为 w1,w2

由引理得 2w1w2,所以当 2mw2 时,合法填充一定无法做到 w=m,输出 Impossible

如何求出 w1w2 呢?

我们用 e 数组记录下一段连续的 ? 的左端点 l 和右端点 r,这里的端点指两边第一个不是 ? 的字符。

对于一段 ?,我们分类讨论,其中 len 表示这一段 ? 的个数:

  • l=r,在全都填与左右端点相同时取到 w2=0,当 2len 时,w1=len,否则 w1=len+1

  • lr,在全都填与左右端点其中一个时取到 w2=1,当 2len 时,w1=len+1,否则 w1=len

可以在纸上举几个例子找规律。

我们用一个 b 数组记下字符串中为 ? 的下标。

为了使答案字典序最小,我们先将所有 ? 填上 0,算出此时的 w

w=m,直接输出当前字符串。

否则说明我们需要把某些 0(在 b 里面的)改为 1

因为是改为 1,所以字典序会增大,所以从后往前修改保证字典序更优。

设当前要修改第 i 位。

对于修改前 w<m 的情况,分三种情况讨论:

  • si1=si=si+1 ,此时修改 siw 会加二,所以修改。

  • si1=si+1si,此时修改 siw 会减二,不仅离答案越远,字典序也变大,所以不修改。

  • si1si+1,此时修改 siw 不变,但字典序变大,所以不修改。

对于修改前 w>m 的情况,只有一种情况。因为能修改的改完后只能是 1,所以只有当 si1=si+1=1 时,修改后 w 才会减二。

在修改前,我们已经记录了每一个连续 ? 的段的左端点和右端点,若一段的左右端点都是 1,则将这一整段 ? 都改为 1

Code1

#include<bits/stdc++.h>
using namespace std;
int t,n,m;
int num[1000010];
int a[1000010];
int b[1000010];
int tot,cnt;
bool f=0;
int maxn,minn;
struct node{
	int l,r;
}e[1000010];
void clean(){  //多测清空
	tot=cnt=f=0;
}
void print(){
	for(int i=1;i<=n;i++){
		cout<<a[i];
	}
	cout<<endl;
}
void solve(int x,int y,int z,int w){
	maxn=0,minn=0;
	for(int i=1;i<=n;i++){
		a[i]=num[i];
	}
	if(x){
		a[x]=y;
	} 
	if(z){
		a[z]=w;
	}
	for(int i=1;i<n;i++){
		if(a[i]!=a[i+1]&&a[i]!=2&&a[i+1]!=2){  //不是?的连续段,w1和w2是确定的
			minn++;
			maxn++;
		}
	}
	for(int i=1;i<=cnt;i++){
		int len=e[i].r-e[i].l-1;  
		maxn+=len;
		if(a[e[i].l]==1&&a[e[i].r]==1) maxn+=(len%2==1)?1:0;
		else if(a[e[i].l]==0&&a[e[i].r]==0) maxn+=(len%2==1)?1:0;
		else maxn+=(len%2==1)?0:1,minn+=1;
	}
	if(m<minn||m>maxn||(m-minn)%2!=0){
		return;
	}
	int now=0;
	for(int i=1;i<=tot;i++){
		a[b[i]]=0;
	}
	for(int i=1;i<n;i++){
		if(a[i]!=a[i+1]) now++;
	}
	if(now==m){
		print();
		f=1;
		return ;
	}
	if(now<m){
		for(int i=tot;i>=1;i--){
			if(now==m){
				print();
				f=1;
				return ;
			}
			if(a[b[i]+1]==0&&a[b[i]-1]==0){
				a[b[i]]=1;
				now+=2;
			}
		}
		if(now==m){
			print();
			f=1;
		}
		return ;
	}
	if(now>m){
		for(int i=cnt;i>=1;i--){
			if(now==m){
				print();
				f=1;
				return ;
			}
			if(a[e[i].l]==1&&a[e[i].r]==1){
				for(int j=e[i].l+1;j<e[i].r;j++){
					a[j]=1;
				}
				now-=2;
			}
		}
		if(now==m){
			print();
			f=1;
		}
		return ;
	}
}
void sol(){
	clean();
	cin>>n>>m;
	string s;
	cin>>s;
	for(int i=1;i<=n;i++){
		if(s[i-1]=='?') num[i]=2;
		else num[i]=s[i-1]-'0';
	}
	bool ff=0;
	int last=1;
	for(int i=2;i<n;i++){	
		if(num[i]==2){  //将?的下标存起来
			b[++tot]=i;
			ff=1;
		}else{
			if(ff){
				e[++cnt].l=last;  //记录?连续段的左端点和右端点
				e[cnt].r=i;  //注意,这里的端点实际上并不是?,是?外的第一个点
			}
			last=i;
			ff=0;
		}
	}
	if(ff){
		e[++cnt].l=last;
		e[cnt].r=n;
	}
	if(num[1]==2&&num[n]==2){  //枚举字符串两端的情况
		solve(1,0,n,0);
		if(f) return ;  //一旦满足条件,直接输出,此时字典序最小
		solve(1,0,n,1);
		if(f) return ;
		solve(1,1,n,0);
		if(f) return ;
		solve(1,1,n,1);
		if(f) return ;
		printf("Impossible\n");
		return ;
	}
	if(num[1]==2){
		solve(1,0,0,0);
		if(f) return ;
		solve(1,1,0,0);
		if(f) return ;
		printf("Impossible\n");
		return ;
	}
	if(num[n]==2){
		solve(0,0,n,0);
		if(f) return ;
		solve(0,0,n,1);
		if(f) return ;
		printf("Impossible\n");
		return ;
	}
	solve(0,0,0,0);
	if(f) return ;
	printf("Impossible\n");
}
int main(){
	cin>>t;
	while(t--){
		sol();
	}
	return 0;
}

这一种解法代码量大,细节多,所以接下来介绍一种更简洁的做法。

Sol2

maxni,j 表示第 j 位为 i 时,sjnw1 值, minni,j 表示第 j 位为 i 时,sjnw2 值。

所以若 sj 能填 i,当且仅当 minni,jresmaxni,j2resmisn? 时例外,因为可以改奇偶性)。注意:这里没有考虑 sj 本身是否为 ?,具体原因往下看。

根据定义,得出 maxn,minn 的递推式:

  • maxni,j=max(maxni,j+1,maxn1i,j+1+1)

  • minni,j=min(minni,j+1,minn1i,j+1+1)

递推时从 n 推向 i

sj 不为 ? 时,将 maxn1sj,j=1×108,minn1sj,j=1×108,这样就能保证上面判断 sj 能否填 i 时,填 1sj 肯定不合法。

如果 s1 既不能填 0 也不能填 1,输出 Impossible,因为第一位什么都不能填,肯定不满足条件,但如果第一位可以填,就说明存在合法解,后面就不用再判无解。

然后一位位的判能否填 0,不能就填 1

Code2

#include<bits/stdc++.h>
using namespace std;
int t;
int n,m;
int a[1000010];
int maxn[2][1000010],minn[2][1000010]; //第j位为i时的最大最小值 
bool check(int x,int y){
	int cnt=m;
	if(x>1&&y!=a[x-1]) cnt--; //和前面一位不同时,w加了1,所以这里要减1
	if(cnt<minn[y][x]||cnt>maxn[y][x]) return 0;
	if(a[n]!=2&&(cnt-minn[y][x])%2==1) return 0;
	return 1; 
}
void clean(){
	for(int i=1;i<=n;i++){
		maxn[0][i]=maxn[1][i]=0;  //多测清空
		minn[0][i]=minn[1][i]=0;
	}
}
void solve(){
	clean();
	cin>>n>>m;
	string s;
	cin>>s;
	for(int i=0;i<n;i++){
		if(s[i]=='?') a[i+1]=2;
		else a[i+1]=s[i]-'0';
	}
	for(int i=n;i>=1;i--){
		maxn[0][i]=max(maxn[0][i+1],maxn[1][i+1]+1); 
		minn[0][i]=min(minn[0][i+1],minn[1][i+1]+1);
		maxn[1][i]=max(maxn[1][i+1],maxn[0][i+1]+1);
		minn[1][i]=min(minn[1][i+1],minn[0][i+1]+1);
		if(i==n) maxn[1][i]=minn[1][i]=maxn[0][i]=minn[0][i]=0; //特判i==n
		if(a[i]==0){
			minn[1][i]=100000000;
			maxn[1][i]=-100000000;
		}else if(a[i]==1){
			minn[0][i]=100000000;
			maxn[0][i]=-100000000;
		}
	}
	if(check(1,0)==0&&check(1,1)==0){
		cout<<"Impossible"<<endl;
		return ;
	}
	for(int i=1;i<=n;i++){
		if(check(i,0)) a[i]=0;
		else a[i]=1;
		if(i>1&&a[i]!=a[i-1]) m-=1;
	}
	for(int i=1;i<=n;i++){
		cout<<a[i];
	}
	cout<<endl;
}
int main(){
	cin>>t;
	while(t--){
		solve();
	}
	return 0;
}

本文作者:larryyu

本文链接:https://www.cnblogs.com/larryyu/p/17729931.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   larryyu_blog  阅读(26)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起