2024杭电ACM-个人PK赛(1) 1005异或 - HDU7416

HDU7416


题意

给定大小为 n n n 的数组 { a n } \{a_n\} {an}

构造 b b b 数组, b x , y = { a y , x = 0 b x − 1 , y ⊕ b x − 1 , y + 1 , x > 0   b_{x, y} = \begin{cases} a_y, & x=0 \\ b_{x-1,y} \oplus b_{x-1,y+1} , & x>0 \end{cases}~ bx,y={ay,bx1,ybx1,y+1,x=0x>0 (其中, x + y ⩽ n − 1 x+y \leqslant n-1 x+yn1 ⊕ \oplus 表示异或操作)

q q q 次询问,给定 ( x , y ) (x, y) (x,y),求 b x , y b_{x, y} bx,y


思路

(此方法几乎不需要任何知识储备,但思维难度较高)

Step0

首先应考虑如何减小计算次数。

依异或运算的性质 x ⊕ x = 0 x \oplus x=0 xx=0,每一个数至多计算一次。(显然的)

我们首先想到将 b x , y b_{x, y} bx,y 化到第一行计算,容易知道,计算 b x , y b_{x, y} bx,y 只需要用到第一行的 b 0 , y ,   b 0 , y + 1 ,   … ,   b 0 , y + x b_{0, y},~b_{0, y+1},~\dots,~b_{0, y+x} b0,y, b0,y+1, , b0,y+x 这些数。哪些数需要计算呢?我们可以通过 在异或表达式中出现的次数 进行判断,如出现奇数次,就需要计算,如果是偶数次,那么就消掉了,不需要计算。

举个栗子,计算 b 2 , 2 = b 1 , 2 ⊕ b 1 , 3 = b 0 , 2 ⊕ b 0 , 3 ⊕ b 0 , 3 ⊕ b 0 , 4 b_{2,2}=b_{1,2} \oplus b_{1,3}=b_{0,2} \oplus b_{0,3} \oplus b_{0,3} \oplus b_{0,4} b2,2=b1,2b1,3=b0,2b0,3b0,3b0,4,它实际上等于 b 0 , 2 ⊕ b 0 , 4 b_{0,2} \oplus b_{0,4} b0,2b0,4。我们称 b 0 , 3 b_{0,3} b0,3 出现偶数次, b 0 , 2 b_{0,2} b0,2 b 0 , 4 b_{0,4} b0,4 出现奇数次。

如何判断出现的次数呢?


Step1 模二杨辉三角

刚才的栗子中, b 0 , 2 , b 0 , 3 , b 0 , 4 b_{0,2},b_{0,3},b_{0,4} b0,2,b0,3,b0,4 出现的次数分别是 1 , 2 , 1 1,2,1 1,2,1,它是杨辉三角的第 2 2 2 行。

能够证明,在 b x , y b_{x, y} bx,y 的异或表达式中, b 0 , y ,   b 0 , y + 1 ,   … ,   b 0 , y + x b_{0, y},~b_{0, y+1},~\dots,~b_{0, y+x} b0,y, b0,y+1, , b0,y+x 出现的次数恰好是杨辉三角的第 x x x 行。因此需要知道,杨辉三角中,哪些项是奇数,哪些是偶数

先打个表:

打表程序(最朴素的杨辉三角):

int a[88][88] = { 0 };
for (int i = 0; i < n; ++i) {
	a[i][0] = a[i][i] = 1;
	for (int j = 1; j < i; ++j) a[i][j] = a[i-1][j] ^ a[i-1][j-1];
}
for (int i = 0; i < n; ++i) {
	std::cout << '\n' << i << ":\t";
	for (int j = 0; j < 88 - i; ++j) std::cout << " ";
	for (int j = 0; j < i + 1; ++j) std::cout << a[i][j] << " ";
}

我发现它的第 2 i 2^i 2i 行只有首末项是奇数,其它是偶数,而第 2 i − 1 2^i-1 2i1 行全为奇数。有一个想法是,将欲求的项化到第 2 i 2^i 2i 行之前的两项,即 b x , y = b x − 2 i , y ⊕ b x − 2 i , y + 2 i b_{x,y}=b_{x-2^i,y} \oplus b_{x-2^i,y+2^i} bx,y=bx2i,ybx2i,y+2i,再(依二进制位)递归计算。

x = 19 → 19 − 16 = 3 x = 3 → 3 − 2 = 1 x = 1 → 1 − 1 = 0 x = 0 x=19 \xrightarrow{19-16=3} x=3 \xrightarrow{3-2=1} x=1 \xrightarrow{1-1=0} x=0 x=191916=3 x=332=1 x=111=0 x=0

这样计算的项数会比较少……吗?每有一个二进制位为 1 1 1,计算次数就要翻倍,最坏情况需要计算 2 15 2^{15} 215 次, TLE \text{TLE} TLE

一个自然的想法是不要全部化为 x = 0 x=0 x=0,可以先预处理前 N N N 行,化到 x < N x<N x<N 时直接出结果(有点 根号分治 的想法),但依旧 TLE \text{TLE} TLE。我们必须解决「计算次数就要翻倍」这个问题。

再次问这个问题:如何减小计算次数?

既然我们决定化到 x < N x<N x<N 了,那到底哪一行合适?


Step2 分形

借助你强大的观察力,发现,模二杨辉三角第 x \pmb x x 行的数是由 x N \pmb{\frac xN} Nx 行的每两个数之间插入 N − 1 \pmb{N-1} N1 0 \pmb0 0 得到的(其中 N = 2 i \pmb{N=2^i} N=2i)。

没注意到也没关系,如果你的注意力足够……近视的话,能发现上面那个图形是个 分形

分形是一种可以分成数个部分,且每一部分都是整体缩小后的形状的形态特征。

有什么用?我们可以从图中「抽出」一个图形,比如,每隔 4 4 4 4 4 4 列的数全部提取出来,依旧是一个模二杨辉三角。同样可得,第 x x x 行的数是由 x 4 \frac x4 4x 行的每两个数之间插入 3 3 3 0 0 0 得到的。
1 1   1 1   0   1 1   1   1   1 1   0   0   0   1 1   1   0   0   1   1 1   0   1   0   1   0   1 1   1   1   1   1   1   1   1 1   0   0   0   0   0   0   0   1 {\color{green}1} \\ {\color{blue}1~1} \\ {\color{blue}1~0~1} \\ 1~1~1~1 \\ {\color{red}1}~0~0~0~{\color{red}1} \\ 1~1~0~0~1~1 \\ 1~0~1~0~1~0~1 \\ 1~1~1~1~1~1~1~1 \\ {\color{red}1}~0~0~0~{\color{red}0}~0~0~0~{\color{red}1} \\ 11 11 0 11 1 1 11 0 0 0 11 1 0 0 1 11 0 1 0 1 0 11 1 1 1 1 1 1 11 0 0 0 0 0 0 0 1
这样的递归就快很多了:如果 x = N q x=Nq x=Nq(绿色的尖尖),直接看第 0 0 0 行(最下面一行红色)中哪些数需要计算,一步结束。而第 0 0 0 行又和蓝色的杨辉三角的最下面一行(实际上是第 q q q 行的杨辉三角)相同,依据杨辉三角第 q q q 行的 01 01 01 值就能判断第 0 0 0 行哪些数需要计算了。

如果带有余数 x = N q + r x=Nq+r x=Nq+r,考虑子三角,化到第 r r r 行,因为已经预处理前 N N N 行了,第 r r r 行的所有数已知,需要计算的数同理依据蓝色的杨辉三角判断。


实现

N N N 行直接算:

const int maxN = 3e5 + 7, N = 512;
int a[N][maxN];
for (int j = 1; j < N; ++j) {
	for (int i = 0; i < n - j; ++i) {
		a[j][i] = a[j - 1][i] ^ a[j - 1][i + 1];
	}
}

按上面的分析, N N N 只能取 2 2 2 的幂,且应该在 maxN \sqrt{\text{maxN}} maxN 级别最优,因此我们取 512 512 512

预处理模二杨辉三角:

const int M = 587;
int b[M][M];

b[0][0] = 1;
for (int j = 1; j < M; ++j) {
	b[j][0] = b[j][j] = 1;
	for (int i = 1; i < j; ++i)
		b[j][i] = b[j - 1][i] ^ b[j - 1][i - 1];
}

因为计算 x = N q + r x=Nq+r x=Nq+r 需要用到杨辉三角的第 q q q 行, q q q 的最大取值是 ⌈ x N ⌉ \lceil\frac{x}{N}\rceil Nx,因此至少要预处理这么多行,即 M = ⌈ x N ⌉ = 587 M=\lceil\frac{x}{N}\rceil=587 M=Nx=587

核心部分:

inline int solve(int x, int y) {
	int q = x / N, r = x % N;
	int ans = 0;
	for (int i = 0; i <= q; ++i) { // 第q行的杨辉三角
		if (b[q][i] == 1) // 是1 需要计算
			ans ^= a[r][i * N + y]; // 依次计算 y和r是余数或者说偏差 *N是因为每两个数之间插入N-1个0
	}
	return ans;
}

时间复杂度 Θ ( N n + M 2 + q M ) ≈ Θ ( n + q ) \Theta(Nn+M^2+qM)\approx\Theta(n+q) Θ(Nn+M2+qM)Θ(n+q),实测 1778ms \text{1778ms} 1778ms


AC code

#include <cstdio>

#define Code int main() { int T=1;
#define Codes int main() { int T=read();
#define by while (T--) eachT();
#define HTLDL return 0;
#define HappyHDUPKSAI }
inline long long read() { char st; long long x = 0; int fu = 1; st = getchar(); while (st > 57 || st < 48) { if (st == 45) fu = -1; st = getchar(); }while (st <= 57 && st >= 48) { x = (x << 3) + (x << 1) + st - 48; st = getchar(); }return x * fu; }

typedef long long ll;
const int maxN = 3e5 + 7, N = 512, M = 587;
int a[N][maxN], b[M][M];

inline int solve(int y, int x) {
	int q = x / N, r = x % N;
	int ans = 0;
	for (int i = 0; i <= q; ++i) {
		if (b[q][i] == 1)
			ans ^= a[r][i * N + y];
	}
	return ans;
}

void eachT() {
	int n = read();

	for (int i = 0; i < n; ++i) a[0][i] = read();
	for (int j = 1; j < N; ++j) {
		for (int i = 0; i < n - j; ++i)
			a[j][i] = a[j - 1][i] ^ a[j - 1][i + 1];
	} // 前N行直接算

	b[0][0] = 1;
	for (int j = 1; j < M; ++j) {
		b[j][0] = b[j][j] = 1;
		for (int i = 1; i < j; ++i)
			b[j][i] = b[j - 1][i] ^ b[j - 1][i - 1];
	} // 楊輝三角

	int q = read();
	while (q--) printf("%d\n", solve(read(), read()));
}

Code by HTLDL
HappyHDUPKSAI
  • 39
    点赞
  • 48
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值