[NOI Online 2021 提高组] 愤怒的小N

题目

点这里看题目。

分析

很有意思的题目!!!

简单分析可以发现 a 等价于二进制中有偶数个 1 , b 等价于二进制中有奇数个 1 。

这个部分可以直接考虑自顶向下的确定某一位为 ab 的过程。

\(n\) 为给定的 " 字符串 " 的长度,而 \(N\) 为输入的数,下面可以考虑 60 pts 怎么做:

当然可以直接数位 DP ,状态设计为 \(f_{i,j,0/1,0/1}\) 表示高 \(i\) 位,在 \(x^j\) 上面的贡献;其中第一组 0/1 表示是否有前缀相等,第二组 0/1 表示 1 的奇偶性。

转移可以考虑下一位放什么,具体细节就略过了,因为写题解的人自己都不会这个方法


众所周知数位 DP 还有另外一种不用 DFS 的处理方法,就是枚举一段相等的前缀 + 钦定下一位更小 + 无限制贡献

那么我们只需要考虑怎么处理无限制,也就是 \(0\sim 2^k-1\) 中任意选的贡献即可。由于一段前缀已知,需要加上,这里就叫它为 \(p\)

不妨将 \(0\sim 2^k-1\) 的贡献表示为与 \(p\) 相关的多项式。那么可以设 \(s_i\) 为游戏局面的字符串,之后就可以定义:

\[S_k(p)=\sum_{j=0}^{2^{k}-1}[s_j='a']f(p+j)\\ T_k(p)=\sum_{j=0}^{2^{k}-1}[s_j='b']f(p+j) \]

并且还不难得到如下递推式:

\[S_k(p)=S_{k-1}(p)+T_{k-1}(p+2^{k-1})\\ T_k(p)=T_{k-1}(p)+S_{k-1}(p+2^{k-1}) \]

注意到后面其实是多项式位移,那么可以暴力卷积做到 \(O(k^2)\) 地转移。

对于每一个前缀,再用 \(O(k)\) 的时间暴力计算,总时间就是 \(O(nk^2)\) 的。

常数很大,建议补题也不要轻易尝试。


接着考虑 80 pts 的做法。

这里会用到一个很奇妙的性质,你可以通过打表得到它:

对于 \([0,2^k-1]\) 的数,设\(S_e\) 为二进制表示中有偶数个 1 的数的 \(x^{k-1}\)之和, \(S_o\) 为二进制表示中有奇数个 1 的数的 \(x^{k-1}\) 之和,\(S_e=S_o\)

譬如,当 \(k=3\) 的时候,有 \(0^2+3^2+5^2+6^2=1^2+2^2+4^2+7^2\)

评论:太美妙了!


证明:

被降智了,直接证明不就完了。

对于 \(n\) ,可以将 \(n\) 拆成若干个 \(2^{e_1}+2^{e_2}+\ldots+2^{e_p}\) 的形式。为了区别,不妨设 \(t_e=2^e\)

根据多项式定理, \(n^{k-1}\) 必然为多个 " 齐次项 " 之和。考察其中的任意一项 \(\prod_{j=0}^{k-1} t_j^{e_j}\) ,其中满足 \(\sum e_j=k-1\) ,那么这一项的多项式系数是确定的,我们只需要考虑它的出现次数。显然只要某一个数包含了所有的非 0 次项,它就会被计算一次。因而可以设 \(q=\sum_j[e_j>0]\) ,那么只需要在剩余的 \(k-q\) 位里面随便选都能算到这一项。我们只需要说明在剩余的 \(k-q\) 位里面,有奇数个 1 和有偶数个 1 的数个数相等即可。

新的命题可以直接对位数使用归纳法证明,这里不再赘述。因而这个性质得到了证明。


不难得到一个推广:

\(C_{k,0}\)\([0,2^k-1]\) 中二进制下有偶数个 1 的数的集合, \(C_{k,1}\)\([0,2^k-1]\) 中二进制下有奇数个 1 的数的集合;则下述命题成立:

\[\forall k\in \mathbb N_+,\forall m\in \mathbb N,m<k,\sum_{p\in C_{k,0}}p^m=\sum_{q\in C_{k,1}}q^m \]

使用同样的方法即可证明。

接着知道了这个性质之后我们该怎么做?

之前我们在枚举一段前缀,枚举完之后如果有 \(c\) 个低位未定,我们就让低位在 \([0,2^c-1]\) 里面随便乱选。不难发现上面的结论也可以推广到 \([p,p+2^c-1]\) 的情况之下(二项式定理展开即证),因此我们只需要计算:

\[\sum_{j=0}^{2^c-1}f(p+j)=\sum_{j=p}^{p+2^c-1}f(j) \]

再除以 2 就好了鸭。

由于 \(f\)\(x\)\(k-1\) 次多项式,因此 \(f\) 的前缀和是 \(x\)\(k\) 次多项式,那么就可以直接 Lagrange 插值得到我们要求的东西。

因此在 \(c<k\) 的时候需要暴力, \(c\ge k\) 的时候直接计算。

现在就得到了 \(O(nk+k^3)\) 的做法( x 值 " 连续 " 的 Lagrange 插值可优化)。


最后考虑 100pts 怎么做,这里的操作更加巧妙。

分析一下我们枚举前缀的过程,以 k=3,'n'= 100110101 为例:

c=4: 10010(....) <=> 10010(0000-1111)
c=5: 1000(.....) <=> 1000(00000-11111)
c=9: 0(........) <=> 0(00000000-11111111)

括号内就表示低位的范围。我们发现这个过程其实等价于是,保证前 \(n-k\) 位小于 \(N\) 的前 \(n-k\) 位的情况下,后面剩余的位随便乱选。

那么整体来看,怎么保证前 \(n-k\) 位小于 \(N\) 的前 \(n-k\) 位呢?直接提取出这 \(n-k\) 位的前缀、减去一再接上一个 11...1 的后缀就好了。比如这里的 100110101 就应该操作成 100101111

因此我们只需要做一次 Lagrange 插值,时间是 \(O(n+k^3)\)

亲测如果用多项式的方法需要开 -O2 。

小结:

  1. 巧妙之处一:发现和相等的性质这个完全只能靠运气和打表好嘛,然后将问题倒向了较简单的函数前缀和部分,直接用 Lagrange 插值。
  2. 巧妙之处二:对于多次 Lagrange 插值的优化,这样的构造思想可以借鉴。

代码

#pragma GCC opitmize( 2 )
#include <cstdio>
#include <cstring>
#include <algorithm>

#define rep( i, a, b ) for( int i = (a) ; i <= (b) ; i ++ )
#define per( i, a, b ) for( int i = (a) ; i >= (b) ; i -- )

const int mod = 1e9 + 7, inv2 = 5e8 + 4;
const int MAXN = 5e5 + 5, MAXK = 605;

template<typename _T>
void read( _T &x )
{
	x = 0; char s = getchar(); int f = 1;
	while( s < '0' || '9' < s ) { f = 1; if( s == '-' ) f = -1; s = getchar(); }
	while( '0' <= s && s <= '9' ) { x = ( x << 3 ) + ( x << 1 ) + ( s - '0' ), s = getchar(); }
	x *= f;
}

template<typename _T>
void write( _T x )
{
	if( x < 0 ) putchar( '-' ), x = - x;
	if( 9 < x ) write( x / 10 );
	putchar( x % 10 + '0' );
}

template<typename _T>
_T MAX( const _T a, const _T b )
{
	return a > b ? a : b;
}

int y[MAXK];

int S[MAXK], T[MAXK], tmp[MAXK];

int fac[MAXN], ifac[MAXN], pw[MAXN];
char str[MAXN];
int num[MAXN];
int N, K;

inline int Qkpow( int, int );
inline int Mul( int x, int v ) { return 1ll * x * v % mod; }
inline int Inv( const int a ) { return Qkpow( a, mod - 2 ); }
inline int Sub( int x, int v ) { return ( x -= v ) < 0 ? x + mod : x; }
inline int Add( int x, int v ) { return ( x += v ) >= mod ? x - mod : x; }

inline int Qkpow( int base, int indx )
{
	int ret = 1;
	while( indx )
	{
		if( indx & 1 ) ret = Mul( ret, base );
		base = Mul( base, base ), indx >>= 1;
	}
	return ret;
}

void Init( const int n )
{
	fac[0] = 1; rep( i, 1, n ) fac[i] = Mul( fac[i - 1], i );
	ifac[n] = Inv( fac[n] ); per( i, n - 1, 0 ) ifac[i] = Mul( ifac[i + 1], i + 1 );
	pw[0] = 1;
	for( int i = 1 ; i <= n ; i ++ )
		pw[i] = Mul( pw[i - 1], 2 );
}

void Push( const int k )
{
	int coe = 0, base = pw[k], now;
	for( int i = 0 ; i < K ; i ++ ) tmp[i] = S[i];
	for( int j = 0 ; j < K ; j ++ )
	{
		coe = 0, now = 1;
		for( int i = j ; i < K ; i ++, now = Mul( now, base ) )
			coe = Add( coe, Mul( Mul( T[i], fac[i] ), Mul( now, ifac[i - j] ) ) );
		S[j] = Add( S[j], Mul( ifac[j], coe ) );
	}
	for( int j = 0 ; j < K ; j ++ )
	{
		coe = 0, now = 1;
		for( int i = j ; i < K ; i ++, now = Mul( now, base ) )
			coe = Add( coe, Mul( Mul( tmp[i], fac[i] ), Mul( now, ifac[i - j] ) ) );
		T[j] = Add( T[j], Mul( ifac[j], coe ) );
	}
}

int Ask( const int p, const int t )
{
	int ret = 0, now = 1;
	for( int i = 0 ; i < K ; i ++, now = Mul( now, p ) )
		ret = Add( ret, Mul( now, t ? S[i] : T[i] ) );
	return ret;
}

int Lagrange( const int x )
{
	int ret = 0, tmp = 0;
	rep( i, 0, K )
	{
		tmp = 1;
		rep( j, 0, K )
			if( i ^ j )
				tmp = Mul( tmp, Mul( Sub( x, j ), Inv( Sub( i, j ) ) ) );
		ret = Add( ret, Mul( tmp, y[i] ) );
	}
	return ret;
}

int main()
{
//	freopen( "angry.in", "r", stdin );
//	freopen( "angry.out", "w", stdout );
	scanf( "%s", str );
	N = strlen( str ), read( K );
	std :: reverse( str, str + N );
	for( int i = 0 ; i < K ; i ++ ) read( S[i] );
	rep( i, 0, K )
	{
		y[i] = 0;
		for( int j = 0, pw = 1 ; j < K ; j ++, pw = Mul( pw, i ) )
			y[i] = Add( y[i], Mul( S[j], pw ) );
		if( i ) y[i] = Add( y[i], y[i - 1] );
	}
	Init( MAX( N, K ) );
	int above = 0, ans = 0, typ = 0;
	for( int i = 0 ; i < N ; i ++ ) 
		above = Add( above, Mul( pw[i], str[i] == '1' ) ), typ ^= str[i] == '1';
	for( int i = 0 ; i < K ; i ++ )
	{
		typ ^= str[i] == '1';
		above = Sub( above, Mul( pw[i], str[i] == '1' ) );
		if( str[i] == '1' ) ans = Add( ans, Ask( above, typ ) );
		if( i < N - 1 ) Push( i );
	}
	write( Add( ans, Mul( Lagrange( Sub( above, 1 ) ), inv2 ) ) ), putchar( '\n' );
	return 0;
}
posted @ 2021-03-27 16:51  crashed  阅读(97)  评论(0编辑  收藏  举报