暑假时校考的题,某种意义上算是我第一次爆标。
挺水的一道*3100。
题目大意
规定一个正整数集合 S S S。一个集合是合法的当且仅当:
-
S ⊆ { 1 , 2 , . . . , n } S \subseteq \{1,2,...,n\} S⊆{1,2,...,n}
-
如果 a ∈ s a \in s a∈s 并且 b ∈ s b \in s b∈s,那么 ∣ a − b ∣ ≠ x |a - b| \not ={x} ∣a−b∣=x 并且 ∣ a − b ∣ ≠ y |a - b| \not ={y} ∣a−b∣=y
输出最大合法集合的大小。
1
≤
n
≤
1
0
9
,
1
≤
x
,
y
≤
22
1 \leq n \leq 10 ^ 9,1 \leq x, y \leq 22
1≤n≤109,1≤x,y≤22。
题解
这题我的做法有点诡异,比较意识流,可能较为难懂,本来不是很想写这个题的,但是转了一圈发现貌似还没见到这个做法的题解,且这应该是目前我找到的做法中代码难度最小的做法,于是就写了。看不懂的话建议跟着题解手玩一下。
解法1
大家拿到题的第一个想法应该和我一样,暴力贪心。遍历整个数组,对于一个位置 a a a,如果 a − x a-x a−x 和 a − y a-y a−y 都没有取过,就取它。复杂度为 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
a−x,a−y,a+x,a+y;若是不选,那么尽量选
a
−
x
,
a
−
y
,
a
+
x
,
a
+
y
a-x,a-y,a+x,a+y
a−x,a−y,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
a−x 和
a
−
y
a-y
a−y 都没有取过,能取则取。由于这个思路本质上和解法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的菜逼食用。