CF1463F Max Correct Set

2 篇文章 0 订阅
1 篇文章 0 订阅

暑假时校考的题,某种意义上算是我第一次爆标。

题目大意

规定一个正整数集合 S S S。一个集合是合法的当且仅当:

  • S ⊆ { 1 , 2 , . . . , n } S \subseteq \{1,2,...,n\} S{1,2,...,n}

  • 如果 a ∈ s a \in s as 并且 b ∈ s b \in s bs,那么 ∣ a − b ∣ ≠ x |a - b| \not ={x} ab=x 并且 ∣ a − b ∣ ≠ y |a - b| \not ={y} ab=y

输出最大合法集合的大小。
1 ≤ n ≤ 1 0 9 , 1 ≤ x , y ≤ 22 1 \leq n \leq 10 ^ 9,1 \leq x, y \leq 22 1n109,1x,y22

题解

这题我的做法有点诡异,比较意识流,可能较为难懂,本来不是很想写这个题的,但是转了一圈发现貌似还没见到这个做法的题解,且这应该是目前我找到的做法中代码难度最小的做法,于是就写了。看不懂的话建议跟着题解手玩一下。

解法1

大家拿到题的第一个想法应该和我一样,暴力贪心。遍历整个数组,对于一个位置 a a a,如果 a − x a-x ax a − y a-y ay 都没有取过,就取它。复杂度为 O ( n ) O(n) O(n)。大眼观察一下发现是以 x + y x+y x+y 为周期的,优化一下时间复杂度为 O ( x + y ) O(x+y) O(x+y) 。代码如下:

#include<bits/stdc++.h>
using namespace std;

const int N=50+1;

int n,x,y,ans,a[N],b[N];

int main(){
	scanf("%d%d%d",&n,&x,&y);
	for(int i=1;i<=x+y;i++)
		if(!a[max(i-x,0)]&&!a[max(i-y,0)]) a[i]=true;
	for(int i=1;i<=x+y;i++)
		a[i]+=a[i-1];
	ans=(n/(x+y))*a[x+y]+a[n%(x+y)];
	if(x>y) swap(x,y);
	for(int i=1;i<=y-x;i++)
		for(int j=i;j<=x+y;j+=y-x)
			if(b[j]==0) b[j]=1,b[j+x]=b[j+y]=b[max(j-x,0)]=b[max(j-y,0)]=-1;
	b[0]=0;
	for(int i=1;i<=x+y;i++)
		b[i]=b[i-1]+(b[i]==1);
	ans=max(ans,(n/(x+y))*b[x+y]+b[n%(x+y)]);
	printf("%d",ans);
    return 0;
}

遗憾的是我们很快就发现这个做法假掉了,用这个贪心算出来的周期内所选的数数量不一定最多,打个比方:n=26,x=21,y=5。此时按这个做法所能选出的数的数量为11,然而实际上最大可以选到13:

1 3 5 7 9 11 13 15 17 19 21 23 25

于是考虑一个新做法。

解法2

解法1的思路是,当选了一个数 a a a 后, a + x a+x a+x a + y a+y a+y 就不能再选,我们要使能选的数尽可能多,也就是让不能选的数尽可能少,于是我们试着尽可能多的让不能选的数重合。具体来说,我们希望尽可能多地选择两个数 a a a b b b,使得 a + x = b + y a+x=b+y a+x=b+y a + y = b + x a+y=b+x a+y=b+x
这个问题等价于,对于一个数 a a a ,若是被选,那么一定不选 a − x , a − y , a + x , a + y a-x,a-y,a+x,a+y ax,ay,a+x,a+y;若是不选,那么尽量选 a − x , a − y , a + x , a + y a-x,a-y,a+x,a+y ax,ay,a+x,a+y。容易发现这个做法也是以 x + y x+y x+y 为周期(若选 a a a,那么不选 a + x a+x a+x a + y a+y a+y,那么选 a + x + y a+x+y a+x+y,反之同理),于是写一个bfs,复杂度仍为 O ( x + y ) O(x+y) O(x+y) 。代码如下:

#include<bits/stdc++.h>
using namespace std;

const int N=50+1;

int n,m,x,y,ans,a[N],b[N];
queue<int> q;

int gcd(int x,int y){
	return y==0?x:gcd(y,x%y);
}

int main(){
	scanf("%d%d%d",&n,&x,&y);
	for(int i=1;i<=x+y;i++)
		if(!a[max(i-x,0)]&&!a[max(i-y,0)]) a[i]=true;
	for(int i=1;i<=x+y;i++)
		a[i]+=a[i-1];
	ans=(n/(x+y))*a[x+y]+a[n%(x+y)];
	if(gcd(x,y)!=1||x==1||y==1){
		printf("%d",ans);
		return 0;
	}
	memset(b,-1,sizeof(b));
	if(x>y) swap(x,y);
	b[1]=0;
	q.push(1);
	while(!q.empty()){
		int p=q.front();q.pop();
		int w=b[p]^1;
		if(p+x<=x+y&&b[p+x]<w){
			q.push(p+x);b[p+x]=w;
		}
		if(p+y<=x+y&&b[p+y]<w){
			q.push(p+y);b[p+y]=w;
		}
		if(p-x>=1&&b[p-x]<w){
			q.push(p-x);b[p-x]=w;
		}
		if(p-y>=1&&b[p-y]<w){
			q.push(p-y);b[p-y]=w;
		}
	}
	b[0]=0;
	for(int i=1;i<=x+y;i++)
		b[i]=b[i-1]+(b[i]^1);
	ans=max(ans,(n/(x+y))*b[x+y]+b[n%(x+y)]);
	printf("%d",ans);
    return 0;
}

好消息是,对于 n = x + y n=x+y n=x+y 的数据,这个做法是正确的,但对于其余数据,这个做法假掉了不少。因为这个做法并不能保证选出的集合字典序最小(不知道这么说对不对),举个例子,当 x = 22 , y = 17 x=22,y=17 x=22,y=17 时,周期内取出来的数为:

3 4 8 9 12 13 14 17 18 19 22 23 24 27 28 29 32 33 37 38

然而最优解法为

1 2 3 6 7 8 11 12 13 16 17 21 22 26 27 31 32 36 37

虽然都是19个数,但是这会导致例如当 n = 40 n=40 n=40 时,答案比最优解小1。
于是我试着在代码中加入一些优化,然而并没有什么卵用,若是想保证字典序最小,则会被卡成 O ( 2 x + y ) O(2^{x+y}) O(2x+y)
很难过,这下还得接着找新做法。

解法3(最终版)

其实到这我基本已经决定放弃去打暴力了,但是在途中却意外发现了正解。
我们现在需要的是一个解法,能同时满足选的数尽量多和字典序最小。发现一个很神秘的事情:解法1满足字典序小,但是不满足选的数尽量多;解法2满足选的数尽量多,但是不满足字典序小。那么把他俩结合一下,是不是就是正解呢。
对于解法2,我们先假设 x < y x<y x<y,随后发现,其实周期内的数只与前 x x x 个数有关,因为当一个数 a a a 确定了, a + x a+x a+x 也就确定了,所以我们实际上只要确定了前 x x x 个数,后面的数也可以随之确定了。
以上性质是我在做解法2时发现的,然而并没有用上。一开始我打了个基础暴力,最朴素的想法是暴力枚举周期内的数选或是不选,复杂度为 O ( 2 x + y ) O(2^{x+y}) O(2x+y)。 然后我试着用以上性质去优化他,当时我甚至没发现这个解法是正解……
由于只需要确定前 x x x 个数,于是把暴力枚举前 x + y x+y x+y 个数优化成了暴力枚举前 x x x 个数,周期内剩余的数只需要前 x x x 个数便可以推出,怎么推呢,为了保证字典序最小,我使用了解法1。即从第 x + 1 x+1 x+1 个数开始,判断这个数是否能被取,也就是判断是否 a − x a-x ax a − y a-y ay 都没有取过,能取则取。由于这个思路本质上和解法2相同,可以保证其正确性。复杂度为 O ( ( x + y ) 2 m i n ( x , y ) ) O((x+y)2^{min(x,y)}) O((x+y)2min(x,y)),可以通过本题。

Code

#include<bits/stdc++.h>
using namespace std;

const int N=45+1;

int n,m,x,y,ans,a[N],b[N];

void dfs(int now){
	if(now==x+1){
		memcpy(b,a,sizeof(a));
		for(int i=x+1;i<=x+y;i++)
			if(!b[max(i-x,0)]&&!b[max(i-y,0)]) b[i]=1;
			for(int i=1;i<=x+y;i++)
				b[i]=b[i-1]+b[i];
			ans=max(ans,(n/(x+y))*b[x+y]+b[n%(x+y)]);
		return;
	}
	a[now]=true;
	dfs(now+1);
	a[now]=false;
	dfs(now+1);
}

int main(){
	scanf("%d%d%d",&n,&x,&y);
	if(x>y) swap(x,y);
	dfs(1);
	printf("%d",ans);
    return 0;
}

后记

表达能力不是太好加上这本来就是神秘的*3100思维题,可能有一些地方比较难以理解,建议自己拿着草稿纸手玩一下会比较好理解。
看了下网上题解复杂度好像大多是 O ( ( x + y ) 2 m a x ( x , y ) ) O((x+y)2^{max(x,y)}) O((x+y)2max(x,y)),虽然貌似也有大佬研究出来了 O ( x + y ) O(x+y) O(x+y) 的dp,但我这种菜鸟根本看不懂QAQ(其实因为不太擅长dp,甚至连标算的状压都没看懂……我太弱了QAQ)。所以这篇题解就给一些像我一样不擅长dp的菜逼食用。

  • 36
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值