题目大意
给出一个长度为
2
n
2^n
2n 的序列
a
a
a,并进行
n
n
n 次更新操作
在第奇数次操作时,将每两个相邻的数 按位或,并将这些得到的值组成的序列代替序列
a
a
a
在第偶数数次操作时,将每两个相邻的数 按位异或,并将这些得到的值组成的序列代替序列
a
a
a
在
n
n
n 次操作后,
a
a
a 序列中只剩下了一个数,我们称这个数为序列
a
a
a 的键值
给出
m
m
m 个询问,每次询问包含两个数
p
,
b
p,b
p,b,表示求当
a
p
=
b
a_p=b
ap=b (其他数不变)时序列
a
a
a 的键值
解题思路
首先,我们要思考如何求出序列 a a a 的键值(先不考虑多组询问)
方法1:直接模拟
这个就没什么好说的了,按照题意 异或/按位或 相邻元素就好了。时间复杂度
O
(
2
n
+
1
−
1
)
\operatorname{O}(2^{n+1}-1)
O(2n+1−1)
方法2:树形DP/dfs
让
n
n
n 次更新操作中出现的所有数组成一个节点数为
2
n
+
1
−
1
2^{n+1}-1
2n+1−1 的满二叉树。其中,从下往上数第
i
i
i 层的节点代表第
i
i
i 次操作前序列
a
a
a 的状态,最后的答案就是根节点的值。时间复杂度
O
(
2
n
+
1
−
1
)
\operatorname{O}(2^{n+1}-1)
O(2n+1−1)
虽然两个方法时间复杂度相同,但方法2在多组询问中更胜一筹
最开始,我们把初始序列
a
a
a 对应的满二叉树预处理出来
在每次询问前修改元素后(假如修改的元素位置为
x
x
x),我们发现有变化的节点都在
x
x
x 到根节点的路径上,并且这些节点的总数不超过
n
n
n
这样,每次询问时只需
O
(
n
)
\operatorname{O}(n)
O(n) 修改二叉树,
O
(
1
)
\operatorname{O}(1)
O(1) 查询答案。总时间复杂度
O
(
(
2
n
+
1
−
1
)
+
(
n
m
)
)
\operatorname{O}((2^{n+1}-1)+(nm))
O((2n+1−1)+(nm))
#include<cstdio>
#include<iostream>
using namespace std;
const long long Maxn=3000000+10;
long long a[Maxn],d[Maxn];
long long n,m;
void dfs(long long x) // dfs 预处理
{
if(x>=(1<<(n+1)))return; // 注意:这种写法一定要判断边界,不然就无限递归了
d[x]=d[x>>1]+1; // 计算深度,根节点的深度为 1
dfs(x<<1);
dfs(x<<1|1);
if(a[x]!=-1)return; // 如果是叶子节点,就不需要合并子树信息
if((n-d[x]) & 1)a[x]=(a[x<<1]^a[x<<1|1]); // 判断是异或还是按位或
else a[x]=(a[x<<1]|a[x<<1|1]);
}
int main()
{
// freopen("in.txt","r",stdin);
scanf("%lld%lld",&n,&m);
for(long long i=1;i<(1<<(n+1));++i)
a[i]=-1;
for(long long i=(1<<(n+1))-(1<<n);i<(1<<(n+1));++i)
scanf("%lld",a+i); // 给叶子节点赋值
dfs(1);
while(m--)
{
long long x,val;
scanf("%lld%lld",&x,&val);
x=(1<<(n+1))-(1<<n)+x-1; // 找到在树中对应的位置
a[x]=val;
while(x>1) // 更新祖先
{
x>>=1;
if((n-d[x]) & 1)a[x]=(a[x<<1]^a[x<<1|1]);
else a[x]=(a[x<<1]|a[x<<1|1]);
}
printf("%lld\n",a[1]);
}
return 0;
}