差分 异或
专题技巧:差分数组、异或操作、证明的技巧、树上异或、边权转点权。
目录
- T1 偷闲
- T2 数列问题
- T3 变色小球
- T4 灯与开关(ATabc155f)
- T5 树异或(ATapc001f)
- T6 星空(洛谷P3943)
- 总结
T1 偷闲
题意
有一个长度为
n
n
n 的序列,每次操作可以选择以下三种的任意一种操作一次:
- 选择任意 i i i ( 1 ≤ i ≤ n 1\le i \le n 1≤i≤n),将第 1 1 1 个数到第 i i i 个数分别减 1 1 1 ;
- 选择任意 i i i ( 1 ≤ i ≤ n 1\le i \le n 1≤i≤n),将第 i i i 个数到第 n n n 个数分别减 1 1 1 ;
- 将全部数分别加
1
1
1 ;
求最少操作数使全部数变为 0 0 0 。(保证答案存在)
1 ≤ t ≤ 100 , − 1 0 9 ≤ a i ≤ 1 0 9 , ∑ n ≤ 2 × 1 0 5 1\le t \le 100,-10^9\le a_i \le 10^9,\sum_{}n\le 2\times 10^5 1≤t≤100,−109≤ai≤109,∑n≤2×105
思路
首先看到对某个区间进行加一减一操作可以想到差分。建立一个差分数组
b
b
b,用差分数组维护操作,最后只要保证差分数组都为
0
0
0 即可。对于操作一,相当于对
b
[
1
]
−
1
,
b
[
i
]
+
1
b[1]-1,b[i]+1
b[1]−1,b[i]+1 ;操作二相当于对
b
[
i
]
−
1
,
b
[
n
+
1
]
+
1
b[i]-1,b[n+1]+1
b[i]−1,b[n+1]+1 ;操作三相当于
b
[
1
]
+
1
,
b
[
n
+
1
]
−
1
b[1]+1,b[n+1]-1
b[1]+1,b[n+1]−1 。现在要将
b
i
b_i
bi 变为
0
0
0 ,如果
b
i
≥
0
b_i \ge 0
bi≥0 ,就做
b
i
b_i
bi 次操作二,否则就做
b
i
b_i
bi 次操作一。发现
b
1
b_1
b1 如果先变为
0
0
0 的话会浪费一些操作数,那么就先将
b
2
…
b
n
b_2 \dots b_n
b2…bn 变为
0
0
0,最后
b
1
b_1
b1 和
b
n
+
1
b_{n+1}
bn+1 一定是一对相反数,再操作
∣
b
1
∣
|b_1|
∣b1∣ 次就可以将序列全部变为
0
0
0 。
代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=2e5+5;
ll a[maxn],b[maxn];
int main(){
int t; cin>>t;
while(t--){
int n; cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
b[i]=a[i]-a[i-1];
}
b[n+1]=-a[n];
ll ans=0;
for(int i=2;i<=n;i++){
if(b[i]>0) ans+=b[i],b[n+1]+=b[i],b[i]=0;
else ans+=(-b[i]),b[1]+=b[i],b[i]=0;
}
if(b[1]>0) ans+=b[1];
else ans-=b[1];
cout<<ans<<"\n";
}
return 0;
}
/* -1 0 0 0 1
1:-1 i:+1
i:-1 n+1:+1
1:+1 n+1:-1
*/
T2 数列问题
题意
一个长为
n
n
n 的整数数列
a
a
a ,每次可以进行操作:选择任意一段连续的数,全都
+
1
+1
+1 或
−
1
-1
−1。求最少的操作次数使所有数变成
0
0
0 。
n
≤
1
0
5
,
−
1
0
4
≤
a
i
≤
1
0
4
n\le 10^5,-10^4\le a_i \le 10^4
n≤105,−104≤ai≤104
思路
对数列
a
a
a 进行差分得到数组
b
b
b 。题目转化为在
b
b
b 数组上每次任选两个数进行一个
+
1
+1
+1 ,一个
−
1
-1
−1 的操作,最后将
b
b
b 数组全部变为
0
0
0 。那当然是正数
−
1
-1
−1 ,负数
+
1
+1
+1 ,最后如果到剩下一些正数(或负数)就单独操作即可。相当于取
b
b
b 中正数之和,负数之和的最大值。
代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+5;
int a[maxn],b[maxn];
int main(){
int n; cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
b[i]=a[i]-a[i-1];
}
b[n+1]=-a[n];
int z=0,f=0;
for(int i=1;i<=n+1;i++){
if(b[i]>0) z+=b[i];
else f-=b[i];
}
cout<<max(z,f);
return 0;
}
/*
[i,j]
+1: i:+1 j+1:-1
-1: i:-1 j+1:+1
*/
T3 变色小球
题意
有
n
n
n 个白色小球,可以选择
m
m
m 次操作,每次都会使相连的两个小球同时变色,问最多可以有多少个黑色小球?
1
≤
n
,
m
≤
1
0
6
1\le n,m \le 10^6
1≤n,m≤106
思路
结论一:一开始黑点数量是偶数,每次操作有
3
3
3 种可能:选择两白、两黑、一黑一白;可以发现操作之后,黑点数量一定还是偶数。
考虑将可以操作的两个小球之间连一条边,会得到一个图(图中可能会有很多个连通块)。每个连通块可以单独考虑。
由结论一可以猜到结论二:每个连通块可以得到的最多黑球个数
=
=
= 联通块中的点数(若点数为奇数则另外
−
1
-1
−1 )。
给出证明:(如何构造出一种方案)
对一个连通块进行深搜,得到一棵树。现在希望得到最多黑色小球。从叶子节点往上推,发现到某个子树的根节点时,他的子树内除自己外的结点都能变成黑色。所以说整棵树除根结点外其他点都能被染成黑色,根节点可能可以也可能不可以。
代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e6+5;
int fa[maxn];
int Find_fa(int x){ return (fa[x]==x?x:fa[x]=Find_fa(fa[x])); }
int siz[maxn];
int main(){
int n,m; cin>>n>>m;
for(int i=1;i<=n;i++)
fa[i]=i,siz[i]=1;
for(int i=1;i<=m;i++){
int x,y; cin>>x>>y;
int fx=Find_fa(x),fy=Find_fa(y);
if(fx!=fy){
fa[fy]=fx;
siz[fx]+=siz[fy];
}
}
int ans=0;
for(int i=1;i<=n;i++)
if(Find_fa(i)==i) ans+=siz[i]-(siz[i]&1);
cout<<ans;
return 0;
}
T4 灯与开关(ATabc155f)
link
题意
在一条数轴上有
n
n
n 盏灯,第
i
i
i 盏灯在整点
a
i
a_i
ai处,它的初始状态是
b
i
b_i
bi。有
m
m
m 个开关,若操作第
i
i
i 个开关,则数轴上
i
∈
[
x
i
,
y
i
]
i \in [x_i,y_i]
i∈[xi,yi] 的灯的状态都会改变。如果能通过操作一些开关使得最后所有灯的状态都是
0
0
0 ,从小到大输出操作的开关的编号;否则,输出
−
1
-1
−1。
1
≤
n
≤
1
0
5
,
1
≤
m
≤
2
×
1
0
5
,
1
≤
x
i
≤
y
i
≤
1
0
9
1\le n \le 10^5 , 1 \le m \le 2\times 10^5 , 1\le x_i \le y_i \le 10^9
1≤n≤105,1≤m≤2×105,1≤xi≤yi≤109
思路
看到变换一个区间的
01
01
01序列,可以想到先对序列进行异或差分,这样就可以
O
(
1
)
O(1)
O(1) 完成操作。
题意变成:考虑要选择哪些操作,每次操作将将两个
1
1
1 变为
0
0
0 ,最后序列要全
0
0
0 。
同
T
3
T3
T3 ,将操作区间的两个端点连一条边,就有一个图。考虑将图深搜简化成树,这是可行的。现在有了一个森林,森林中的每棵树可以单独考虑。然后对每棵树进行 树形
D
P
DP
DP。从叶子节点往上推,假设现在考虑到某个结点,如果他的儿子结点得到的
D
P
DP
DP 值为
1
1
1 ,说明要选择这个结点到儿子这条边的操作,那么儿子的
D
P
DP
DP 值变为
0
0
0 ,这个结点的值要反转。最后根节点的
D
P
DP
DP 值若为
1
1
1 则说明没有合法的解决方案。(当然若根节点为
n
+
1
n+1
n+1 则最后
D
P
DP
DP 值是
1
1
1 或
0
0
0 都是可行的)
代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+5,maxm=2e5+5;
int idx,head[maxn];
struct EDGE{ int v,next,k; }e[maxm*2];
void Insert(int u,int v,int k){
e[++idx]={v,head[u],k};
head[u]=idx;
}
int ans,ansl[maxm],f[maxn],d[maxn];
bool vis[maxn];
void Dfs(int x,int fat){
vis[x]=1;
for(int i=head[x];i;i=e[i].next){
int v=e[i].v;
if(v!=fat){
Dfs(v,x);
if(f[v]) ansl[++ans]=e[i].k,f[x]^=f[v];
}
}
}
int fa[maxn];
int Find_fa(int x){ return (fa[x]==x?x:fa[x]=Find_fa(fa[x])); }
int n;
struct NODE{ int id,x; }a[maxn];
int Get_l(int x){
int l=0,r=n+1,mid;
while(l+1<r){
mid=(l+r)>>1;
if(a[mid].id<x) l=mid;
else r=mid;
}
return r;
}
int Get_r(int x){
int l=0,r=n+1,mid;
while(l+1<r){
mid=(l+r)>>1;
if(a[mid].id<=x) l=mid;
else r=mid;
}
return l;
}
int main(){
int m; cin>>n>>m;
for(int i=1;i<=n;i++)
cin>>a[i].id>>a[i].x;
sort(a+1,a+n+1,[](NODE a,NODE b){ return a.id<b.id; });
for(int i=1;i<=n+1;i++)
fa[i]=i;
for(int i=1;i<=m;i++){
int x,y; cin>>x>>y;
x=Get_l(x);
y=Get_r(y);
if(x>y) continue;
int fx=Find_fa(x),fy=Find_fa(y+1);
if(fx!=fy) fa[fy]=fx,Insert(x,y+1,i),Insert(y+1,x,i);
}
for(int i=1;i<=n;i++)
f[i]=a[i].x^a[i-1].x;
Dfs(n+1,0);
for(int i=1;i<=n;i++)
if(!vis[i]){
Dfs(i,0);
if(f[i]){
cout<<-1;
return 0;
}
}
sort(ansl+1,ansl+ans+1);
cout<<ans<<"\n";
for(int i=1;i<=ans;i++)
cout<<ansl[i]<<" ";
return 0;
}
T5 树异或(ATapc001f)
link
题意
给一棵有
n
n
n 个节点的树,节点编号从
0
0
0 到
n
−
1
n-1
n−1,树边编号从
1
1
1 到
n
−
1
n-1
n−1。第
i
i
i 条边连接节点
x
i
x_i
xi 和
y
i
y_i
yi,其权值为
a
i
a_i
ai。你可以对树执行任意次操作,每次操作选取一条链和一个非负整数
x
x
x,将链上的边的权值与
x
x
x 异或成为该边的新权值。问最少需要多少次操作,使得所有边的权值都为
0
0
0。
2
≤
N
≤
1
0
5
,
0
≤
x
i
,
y
i
≤
N
−
1
,
0
≤
a
i
≤
15
2\leq N \leq 10^5 , 0\leq x_i,y_i \leq N-1 , 0\leq a_i \leq 15
2≤N≤105,0≤xi,yi≤N−1,0≤ai≤15
思路
看到链操作可以想到树上差分。对于链
u
−
>
v
u->v
u−>v 操作就相当于在差分数组上
d
u
=
d
u
⊕
x
,
d
y
=
d
y
⊕
x
d_u = d_u \oplus x , d_y = d_y \oplus x
du=du⊕x,dy=dy⊕x ,就可以
O
(
1
)
O(1)
O(1) 完成操作(也可以归为“边权化点权”)。现在的问题就是:每次在差分数组中选
2
2
2 个数异或上同一个数,最后要全部数变为
0
0
0 。由于所有点权
d
i
d_i
di 的值域为
[
0
,
15
]
[0,15]
[0,15] ,点的个数很多,所以会有很多相同的值。容易想到先选择两个相同的数操作是最优的,因为一次就能将两个非
0
0
0 数清零。这样之后,每个值只会剩下至多一个。
所以这样对多只有
16
16
16 个数,想到状压
D
P
DP
DP 。
又可以想到,对于一个集合
S
S
S 中所有数全部异或起来为
0
0
0 时,只需
∣
S
∣
−
1
|S|-1
∣S∣−1 次操作就能将
S
S
S 清零。
所以设
f
s
f_s
fs 表示将
s
s
s 这个集合中的所有数清零所需的最小操作数,有转移:
f
s
=
m
i
n
{
∣
s
∣
⊕
x
∈
s
x
≠
0
∣
s
∣
−
1
⊕
x
∈
s
x
=
0
min
t
∈
s
f
t
+
f
s
−
t
f_s = min \begin{cases} |s| & \oplus_{x \in s} x \not= 0 \\ |s|-1 & \oplus_{x \in s} x = 0 \\ \min\limits_{t \in s} f_t + f_{s-t} \end{cases}
fs=min⎩
⎨
⎧∣s∣∣s∣−1t∈sminft+fs−t⊕x∈sx=0⊕x∈sx=0
代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+5,maxa=20,maxs=(1<<16)+5;
int d[maxn],gs[maxa],f[maxs];
int main(){
int n; cin>>n;
for(int i=1;i<n;i++){
int x,y,z; cin>>x>>y>>z;
d[x]^=z,d[y]^=z;
}
for(int i=0;i<n;i++)
gs[d[i]]++;
int ans=0,S=0,k=0;
for(int i=1;i<=15;i++){
ans+=gs[i]/2;
if(gs[i]&1) S+=(1<<i),k++;
}
//一个数字的集合有解(也就是说可以用取两个数字异或上同一个数字将这个集合里的所有数字变成0)当且当这个数字集合的异或和为0
for(int i=1;i<=S;i++)
f[i]=k-1;
for(int i=1;i<=S;i++){
int tmp=0,cnt=0;;
for(int j=0;j<=15;j++)
if(i&(1<<j)) tmp^=j,cnt++;
if(!tmp) f[i]=min(f[i],cnt-1);
for(int j=i;j;j=(j-1)&i)
f[i]=min(f[i],f[j]+f[i^j]);
}
cout<<f[S]+ans;
return 0;
}
T6 星空(洛谷P3943)
link
题意
n
n
n 个灯泡中有
k
k
k 个被熄灭。有
m
m
m 种长度的灯泡段可以被选择翻转,每种可以被选择多次,求将所有灯泡点亮的最少操作数。
n
,
b
i
≤
4
×
1
0
4
,
m
≤
64
,
k
≤
8
n,b_i \le 4 \times 10^4 , m \le 64 , k \le 8
n,bi≤4×104,m≤64,k≤8
思路
翻转一段灯泡,可以想到异或差分。每次可以消去两个
1
1
1 ,求消去所有
1
1
1 的最小方案数。发现差分后的序列最多只有
2
k
2k
2k 个
1
1
1 ,考虑状压。可以先
b
f
s
bfs
bfs 求出任意两个
1
1
1 同时消去所需的操作数,不能用(或不方便)递推(
D
P
DP
DP)是因为这个翻转操作会有后效性,其实只是本人这样弄不出来。复杂度能过,好像会有一点细节。感觉和前几题差不多。
代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+5,maxk=20;
int n,m,hv,b[maxn],lgt[maxk*2],dis[maxk*2][maxk*2],cot[maxn];
queue<int>q;
void Bfs(int st){
while(!q.empty()) q.pop();
for(int i=1;i<=n+1;i++)
cot[i]=1e9;
q.push(lgt[st]);
cot[lgt[st]]=0;
while(!q.empty()){
int x=q.front();
q.pop();
for(int i=1;i<=m;i++){
if(x+b[i]<=n+1&&cot[x+b[i]]==1e9){
cot[x+b[i]]=cot[x]+1;
q.push(x+b[i]);
}
if(x-b[i]>=1&&cot[x-b[i]]==1e9){
cot[x-b[i]]=cot[x]+1;
q.push(x-b[i]);
}
}
}
for(int i=1;i<=hv;i++)
dis[st][i]=dis[i][st]=min(dis[st][i],cot[lgt[i]]);
}
int d[maxn],f[(1<<maxk)];
int main(){
int k; cin>>n>>k>>m;
for(int i=1,x;i<=k;i++){
cin>>x;
d[x]^=1,d[x+1]^=1;
}
for(int i=1;i<=m;i++)
cin>>b[i];
for(int i=1;i<=n+1;i++)
if(d[i]==1) lgt[++hv]=i;
for(int i=1;i<=hv;i++)
for(int j=1;j<=hv;j++)
dis[i][j]=1e9;
for(int i=1;i<=hv;i++)
dis[i][i]=0;
for(int i=1;i<=hv;i++)
Bfs(i);
for(int i=0;i<(1<<hv);i++)
f[i]=1e18;
f[(1<<hv)-1]=0;
for(int s=(1<<hv)-1;s>=0;s--)
for(int i=1;i<=hv;i++)
if((1<<(i-1))&s){
for(int j=i+1;j<=hv;j++)
if((1<<(j-1))&s){
int s2=(s^(1<<(i-1))^(1<<(j-1)));
f[s2]=min(f[s2],f[s]+dis[i][j]);
}
}
cout<<f[0];
return 0;
}
总结
感觉主要是要看到对一段区间进行加减异或等要想到差分。