题目
思路
无论怎么想,这个题都挺离谱的……官方题解离谱,我的做法更离谱……
官方题解
先来看看这个至少是 l o g i c a l \rm logical logical 的做法。
由于每层都是求差分,可以认为 a i ∈ [ 0 , 2 ] a_i\in[0,2] ai∈[0,2] 。此时我们发现: 1 1 1 非常特殊,它与 0 , 2 0,2 0,2 相邻时,总是仍然产生 1 1 1 。
这就是说,只要序列中存在 1 1 1,并且序列不全是 1 1 1,那下一层中也有至少一个 1 1 1 。一直到最后,答案就会是 1 1 1 了。
是否有可能打破这个魔咒?那就是需要全是 1 1 1,那么答案必然是 0 0 0 。知道这个好像还是需要模拟?
还会发现: 0 , 2 0,2 0,2 没有区别。即,统称 0 , 2 0,2 0,2 为 e e e,那么 1 1 1 和 e e e 产生 1 1 1、 e e e 和 e e e 产生 e e e、 1 1 1 和 1 1 1 产生 e e e 。目的只是检测,是否有全 1 1 1 的时刻存在,或者说最后剩下的是 e e e 还是 1 1 1 。
这就很简单了,因为明显发现这就是异或运算, e e e 映射到 0 0 0, 1 1 1 映射到 1 1 1 。异或运算就是不进位加法,或者说 m o d \bmod mod 2 2 2 意义下的普通加法,系数就是杨辉三角了。
那么不存在 1 1 1 的时候呢?其实还是异或运算, 2 2 2 映射到 1 1 1 罢了……
众所周知,检查组合数是否为 2 2 2 的倍数,这是 O ( 1 ) \mathcal O(1) O(1) 的位运算,所以总复杂度 O ( n ) \mathcal O(n) O(n) 。
我的做法
我首先把 n = 4 n=4 n=4 的所有情况打了个表。然后发现了一个规律。——话说为什么我要打 n = 4 n=4 n=4 的表呢?我也不知道。我只是觉得 n = 4 n=4 n=4 包含了 n ⩽ 4 n\leqslant 4 n⩽4 的情况。那么为何不选择 n = 5 n=5 n=5 呢?说不清。——虽说 n = 5 n=5 n=5 的规律也是有的,但是没那么明显。而且就很难往 2 k 2^k 2k 联想了……
- 当 1 1 1 存在时:奇数个 1 1 1 则答案为 1 1 1,否则为 0 0 0 。
- 当 1 1 1 不存在时:奇数个 2 2 2 则答案为 2 2 2,否则为 0 0 0 。
显然我没有给出任何证明,因为这真的是打表发现……只对 n = 4 n=4 n=4 有效。
我觉得这是一个不错的规律。我试着把它再套一层,发现 n = 8 n=8 n=8 也满足该性质!
兴致勃勃的我验证了 n = 12 n=12 n=12 的情况,希望 4 ∣ n 4|n 4∣n 就有这个规律。结果大失所望。 n = 12 n=12 n=12 就嗝屁了。
可是我没有绝望,接着试下一个, n = 16 n=16 n=16 又行了!敏锐的你一定猜到了: n = 2 k n=2^k n=2k 则满足该条件。
只需要这样就够了。如果 n ≠ 2 k n\ne 2^k n=2k 呢?作二进制分解呗。时间复杂度 O ( n log n ) \mathcal O(n\log n) O(nlogn) 。
现在回头来看,这玩意儿的正确性是啥?是 n = 2 k n=2^k n=2k 时 ( n − 1 i ) m o d 2 {n-1\choose i}\bmod 2 (in−1)mod2 恒为 1 1 1,即每一位的贡献都是 1 1 1,所以 1 1 1 存在时(它映射到异或运算中的 1 1 1 )看它个数的奇偶性,否则看 2 2 2 个数的奇偶性……
没错,这么一道困难的题目,竟然被愚蠢的暴力打表给破解了!虽然复杂度不是最优。
代码
这里就贴上我的做法的代码吧。官方正解可见于 P P L ′ s b l o g \rm PPL's\;blog PPL′sblog 。
#include <cstdio>
#include <iostream>
#include <cstring>
using namespace std;
typedef long long int_;
# define rep(i,a,b) for(int i=(a); i<=(b); ++i)
# define drep(i,a,b) for(int i=(a); i>=(b); --i)
inline int readint(){
int a = 0; char c = getchar(), f = 1;
for(; c<'0'||c>'9'; c=getchar())
if(c == '-') f = -f;
for(; '0'<=c&&c<='9'; c=getchar())
a = (a<<3)+(a<<1)+(c^48);
return a*f;
}
const int MaxN = 1000005;
int cnt[MaxN][3], a[MaxN];
void solve(int n){
if(n == 1) return ;
int len = 1; // how to compress
for(; (len<<1)<=n; len<<=1);
rep(i,1,n){
memcpy(cnt[i],cnt[i-1],3<<2);
++ cnt[i][a[i]];
}
rep(i,1,n-len+1){
int one = cnt[i+len-1][1]-cnt[i-1][1];
int two = cnt[i+len-1][2]-cnt[i-1][2];
if(one) a[i] = (one&1);
else a[i] = (two&1)<<1;
}
return solve(n-len+1);
}
char str[MaxN];
int main(){
int n = readint();
scanf("%s",str+1);
rep(i,1,n) a[i] = str[i]-'1';
solve(n); printf("%d\n",a[1]);
return 0;
}
最后 别有用心地 贴上暴力打表的核心代码。
const int N = 4;
for(a[1][1]=0; a[1][1]<3; ++a[1][1])
for(a[1][2]=0; a[1][2]<3; ++a[1][2])
for(a[1][3]=0; a[1][3]<3; ++a[1][3])
for(a[1][4]=0; a[1][4]<3; ++a[1][4]){
rep(j,2,N) rep(i,1,N-j+1)
a[j][i] = ABS(a[j-1][i]-a[j-1][i+1]);
if(true){ // print result to console
rep(j,1,N){
rep(i,2,j) putchar(' ');
rep(i,1,N-j+1){
writeint(a[j][i]);
putchar(' ');
}
putchar('\n');
}
}
int cnt[3] = {0,0,0};
rep(i,1,N) ++ cnt[a[1][i]];
if(cnt[1]) assert((cnt[1]&1) == a[N][1]);
else assert((cnt[2]&1)*2 == a[N][1]);
}