莫队学习笔记
普通莫队
初遇——从暴力谈起
我们通过一道例题来讨论普通莫队。
题目链接。
观察数据范围,一个很直接的想法是:开一个数组 \(cnt\),\(cnt_i\) 表示在询问的区间内数字 \(i\) 出现的次数。对于每一个询问,记询问区间左端点为 \(lt\),询问区间右端点为 \(rt\),对于 \(i\in [lt,rt],cnt_{a_{i}}\leftarrow cnt_{a_{i}}+1\),最后扫描一遍 \(cnt\) 数组统计 \(cnt\) 数组中有几个位置不为 \(0\),统计的结果即为这次询问的答案。记给定的 \(a\) 数组中最大的数为 \(K\),这个算法的时间复杂度在最坏的情况(即每个询问的左端点都是 \(1\),右端点都是 \(N\))下为 \(O(NQK)\),无法通过本题。
接下来我们考虑如何优化这个算法。
首先,对于 \(i\in [lt,rt]\),如果 \(cnt_{a_{i}}=0\),则说明 \(a_{i}\) 在询问的区间中第一次出现,答案加一,这样省去了扫描 \(cnt\) 数组的过程,在最坏的情况下时间复杂度降为 \(O(NQ)\),但是效率还是不够高。
接下来我们看几个询问:
1 5
2 6
5 7
...
注意到,这几个询问的区间有重复部分,解决每个询问时都会重复扫描重复部分,很浪费时间。于是,我们可以使用两个指针 \(l\) 和 \(r\),对于一个询问,我们不再直接扫描对应的区间,而是将 \(l\) 指针移动到对应的左端点,将 \(r\) 指针移动到对应的右端点,并且仅在 \(l\) 指针或 \(r\) 指针移动时对 \(cnt\) 数组和答案进行修改。
于是可以写出如下代码:
#include<bits/stdc++.h>
#define endl '\n'
#define int long long
using namespace std;
int n,a[30005],m,cnt[1000005],now,l=1,r=0,lt,rt;
void add(int x){
if(!cnt[a[x]]) now++;
cnt[a[x]]++;
}
void del(int x){
cnt[a[x]]--;
if(!cnt[a[x]]) now--;
}
signed main(){
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
cin>>m;
for(int i=1;i<=m;i++){
cin>>lt>>rt;
while(l>lt) add(--l);
while(r<rt) add(++r);
while(l<lt) del(l++);
while(r>rt) del(r--);
cout<<now<<endl;
}
return 0;
}
困境——乱跑的指针
当一个询问的左端点和右端点与上一个询问的左端点与右端点差距过大时,就会出现 \(l\) 指针和 \(r\) 指针在数列上乱跑的情况,无法保证效率。
例如当 \(N=30000\) 时的这几个询问:
1 2
29999 30000
3 4
29997 29998
...
这时, \(l\) 指针和 \(r\) 指针先从序列开头跑到了序列末尾,又从序列末尾跑到了接近序列开头的位置,然后又跑到了接近序列末尾的位置。
那么如何提高指针移动的效率呢?
观察到,指针的低效移动是由询问左右端点的无序性引起的,所以我们可以把询问离线,并将数列分块。按照左端点所处的块的位置升序排序,如果左端的所处的块位置相同,则按照右端点升序排序。这样,就在保证了左端点比较有序也保证了右端点比较有序,从而提高了指针的移动效率。
代码如下:
#include<bits/stdc++.h>
#define endl '\n'
#define int long long
using namespace std;
int n,a[30005],m,cnt[1000005],now,l=1,r=0,L[30005],R[30005],pos[30005],t,ans[200005];
struct node{
int l,r,id;
}q[200005];
inline bool cmp(node x,node y){
if(pos[x.l]==pos[y.l]) return x.r<y.r;
return pos[x.l]<pos[y.l];
}
inline void add(int x){
if(!cnt[a[x]]) now++;
cnt[a[x]]++;
}
inline void del(int x){
cnt[a[x]]--;
if(!cnt[a[x]]) now--;
}
signed main(){
std::ios::sync_with_stdio(false);
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
t=sqrt(n);
for(int i=1;i<=t;i++){
L[i]=(i-1)*t+1;
R[i]=i*t;
}
if(R[t]<n){
t++;
L[t]=R[t-1]+1;
R[t]=n;
}
for(int i=1;i<=t;i++){
for(int j=L[i];j<=R[i];j++){
pos[j]=i;
}
}
cin>>m;
for(int i=1;i<=m;i++){
cin>>q[i].l>>q[i].r;
q[i].id=i;
}
sort(q+1,q+1+m,cmp);
for(int i=1;i<=m;i++){
while(l>q[i].l) add(--l);
while(r<q[i].r) add(++r);
while(l<q[i].l) del(l++);
while(r>q[i].r) del(r--);
ans[q[i].id]=now;//注意,排序只有询问的顺序可能会改变,所以应该将答案存在ans[q[i].id]而不是ans[i]
//4个while循环的位置有一定的要求,详见OI-Wiki
}
for(int i=1;i<=m;i++) cout<<ans[i]<<endl;
return 0;
}
优化——顺路而为之
考虑一种排序方式:
对于左端点所处的块不同的询问,按左端点升序排序。对于左端点所处的块相同的询问,若这个块是的编号是奇数,则按右端点升序排序,否则按右端点降序排序。
按照这样的方式排序后,\(r\) 指针能在处理完奇数块的询问后,可以在返回的途中顺路处理偶数块的询问,减少 \(r\) 指针的移动次数,从而提高效率。
代码如下:
bool cmp(node x,node y){
if(x.l/t!=y.l/t) return x.l<y.l;
if((x.l/t)&1) return x.r<y.r;
return x.r>y.r;
}
一些习题
题意简述
求区间众数的出现次数。
解法分析
本题可以离线,于是可以考虑使用莫队解决。
add
操作比较好写,下面我们主要考虑 del
操作。
记删掉的数为 \(x\),当前答案为 \(y\),分 \(3\) 种情况讨论:
-
\(x\) 的出现次数小于 \(y\),对 \(y\) 没有影响。
-
\(x\) 的出现次数等于 \(y\),但不止 \(x\) 一个数出现了 \(y\) 次,对 \(y\) 没有影响。
-
\(x\) 的出现次数等于 \(y\),并且只有 \(x\) 一个数出现了 \(y\) 次,\(y\leftarrow y-1\)。
综上,我们需要开两个数组 \(cnt1,cnt2\),\(cnt1_i\) 记录 \(i\) 出现的次数, \(cnt2_i\) 记录出现次数为 \(i\) 的数的个数。
代码
#include<bits/stdc++.h>
using namespace std;
inline int read(){
...
}
inline void write(int x){
...
}
// read函数与write函数为快读与快写
int n,m,a[100005],b[100005],t,l=1,r,cnt1[200005],cnt2[200005],now,ans[200005];
struct node{
int l,r,id;
}q[200005];
bool cmp(node x,node y){
if(x.l/t!=y.l/t) return x.l<y.l;
if((x.l/t)&1) return x.r<y.r;
return x.r>y.r;
}//对排序策略的优化
void add(int x){
cnt2[cnt1[x]]--;
cnt1[x]++;
cnt2[cnt1[x]]++;
now=max(now,cnt1[x]);
}
void del(int x){
cnt2[cnt1[x]]--;
if(!cnt2[cnt1[x]] && now==cnt1[x]) now--;
cnt1[x]--;
cnt2[cnt1[x]]++;
}
int main(){
n=read();
m=read();
for(int i=1;i<=n;i++){
a[i]=read();
b[i]=a[i];
}
sort(b+1,b+1+n);
t=unique(b+1,b+1+n)-b-1;
for(int i=1;i<=n;i++) a[i]=lower_bound(b+1,b+1+t,a[i]-b;//离散化
t=sqrt(n);
for(int i=1;i<=m;i++){
q[i].l=read();
q[i].r=read();
q[i].id=i;
}
sort(q+1,q+1+m,cmp);
for(int i=1;i<=m;i++){
while(l>q[i].l) add(a[--l]);
while(r<q[i].r) add(a[++r]);
while(l<q[i].l) del(a[l++]);
while(r>q[i].r) del(a[r--]);
ans[q[i].id]=now;
}
for(int i=1;i<=m;i++){
write(ans[i]);
putchar('\n');
}
return 0;
}
带修莫队
显然,想小标题是困难的QAQ
修改——与时间旅行
我们同样通过一道例题来探究带修莫队。
题目链接。
本题要求支持修改操作,我们可以在修改操作上加上一维时间维度,它的取值是最近一次修改操作的时间,即最近一次修改操作是第几次修改操作。同样,在主函数记录一个当前时间,如果当前时间早于查询操作的时间,就说明在回答这个询问之前需要按照输入数据进行修改操作;如果当前时间晚于查询操作的时间,就说明在回答这个查询之前需要按照输入数据对修改操作进行撤销,也就是尝试向以下6个方向扩展或缩小。
-
\([l-1,r,time]\)
-
\([l+1,r,time]\)
-
\([l,r-1,time]\)
-
\([l,r+1,time]\)
-
\([l,r,time-1]\)
-
\([l,r,time+1]\)
另外,由于加上了时间这一维,所以我们需要改变排序策略,具体的代码如下:
#include<bits/stdc++.h>
#define endl '\n'
#define int long long
using namespace std;
int n,m,a[200005],t,cnt_c,cnt_q,cnt[1000005],now,l=1,r,last,ans[200005];
struct que{
int id,ti,l,r;
}q[200005];
struct cha{
int x,dat;
}c[200005];
bool cmp(que x,que y){
if(x.l/t!=y.l/t) return x.l/t<y.l/t;
else if(x.r/t!=y.r/t) return x.r/t<y.r/t;
else return x.ti<y.ti;//在排序时需要考虑时间
}
void add(int x){
if(!cnt[x]) now++;
cnt[x]++;
}
void del(int x){
cnt[x]--;
if(!cnt[x]) now--;
}
signed main(){
std::ios::sync_with_stdio(false);
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i];
}
for(int i=1;i<=m;i++){
char t1;
int t2,t3;
cin>>t1>>t2>>t3;
if(t1=='Q') q[++cnt_q]=(que){cnt_q,cnt_c,t2,t3};
else if(t1=='R') c[++cnt_c]=(cha){t2,t3};
}
t=pow(n,2.0/3.0);
sort(q+1,q+1+cnt_q,cmp);
for(int i=1;i<=cnt_q;i++){
while(l>q[i].l) add(a[--l]);
while(r<q[i].r) add(a[++r]);
while(l<q[i].l) del(a[l++]);
while(r>q[i].r) del(a[r--]);
while(last<q[i].ti){
last++;
if(c[last].x<=r && c[last].x>=l){
add(c[last].dat);
del(a[c[last].x]);
}
swap(a[c[last].x],c[last].dat);//在下面的撤销修改中再次swap即可撤销修改,减少码量
}
while(last>q[i].ti){
if(c[last].x<=r && c[last].x>=l){
add(c[last].dat);
del(a[c[last].x]);
}
swap(a[c[last].x],c[last].dat);
last--;
}
ans[q[i].id]=now;
}
for(int i=1;i<=cnt_q;i++) cout<<ans[i]<<endl;
return 0;
}
一些习题
【模板】树套树 。
题意简述
实现树套树。
解法分析
众所周知,实现树套树的代码一般又臭又长,但是如果题目没有强制在线,我们可以考虑使用莫队代替树套树。
单点修改可以交给带修莫队完成,其他操作则需要用到值域分块的技巧。
记离散化后的最大值为 \(X\),我们将值域 \([1,X]\) 分为若干个长度为 \(\sqrt{X}\) 的块,记第 \(i\) 块的右端点为 \(R_i\),左端点为 \(L_i\),块内各数字的出现次数之和为 \(sum_i\)。在移动莫队的左右端点时,除了维护每个数字在莫队中出现的次数外,还要维护该数字所处块内各数字的出现次数之和。
当查询 \(k\) 的排名时,记其排名为 \(A\),\(A\) 的初值为 \(1\),我们依次考虑每个块 \(i\),若 \(k>R_i\),则第 \(i\) 块内的每个数字都比 \(k\) 更小,\(k\) 的排名必定大于第 \(i\) 块内的所有数字,\(A \leftarrow A+sum_i\)。否则,说明 \(k\) 就在第 \(i\) 块内,扫描第 \(i\) 块内每个数字 \(j\) 在莫队中的出现次数,\(A \leftarrow A+cnt_{j}\),直到 \(k=j\) 时,\(A\) 的值就是 \(k\) 的排名。
当查询排名为 \(k\) 的值时,我们依次考虑每个块 \(i\),若 \(sum_i<k\),则第 \(i\) 块中的所有数的排名均小于 \(k\),\(k \leftarrow k-sum_i\)。否则,说明排名为 \(k\) 的值就在第 \(i\) 块内,扫描第 \(i\) 块内的每个数字 \(j\),\(k \leftarrow k-cnt_j\),若 \(k \leq 0\),则说明 \(j\) 就是排名为 \(k\) 的值。
当查询 \(k\) 的前驱时,我们先查询 \(k\) 的排名,记为 \(p\),然后查询排名为 \(p-1\) 的值,即为答案。
当查询 \(k\) 的后继时,我们先查询 \(k\) 的排名,记为 \(p\),然后查询排名为 \(p+1\) 的值,即为答案。
代码
#include<bits/stdc++.h>
using namespace std;
inline int read(){register int t1=0,t2=0;register char x=getchar();while(x<'0' ||x>'9'){if(x=='-') t2|=1;x=getchar();}while(x>='0' && x<='9'){t1=(t1<<1)+(t1<<3)+(x^48),x=getchar();}return t2?-t1:t1;}
inline void write(int x){register int sta[100],top=0;if(x<0) putchar('-'),x=-x;do{sta[top++]=x%10,x/=10;}while(x);while(top) putchar(sta[--top]+48);}
int n,m,cnt_q,cnt_c,temp,t,l=1,r,last;
int a[500005],b[500005],L[500005],R[500005],pos[500005],sum[500005],cnt[500005],ans[500005];
struct que{
int l,r,ti,id,k,opt;
}q[500005];
struct cha{
int x,d;
}c[500005];
bool cmp(que x,que y){
if(x.l/t!=y.l/t) return x.l/t<y.l/t;
else if(x.r/t!=y.r/t) return x.r/t<y.r/t;
return x.ti<y.ti;
}
inline void add(int x){
cnt[x]++;
sum[pos[x]]++;
}
inline void del(int x){
cnt[x]--;
sum[pos[x]]--;
}
inline int get_rank(int x){
int an=1;
for(int i=1;i<=t;i++){
if(x<=R[i]) for(int j=L[i];j<x;j++) an+=cnt[j];
else an+=sum[i];
}
return an;
}
inline int get_kth(int x){
for(int i=1;i<=t;i++){
if(x<=sum[i]){
for(int j=L[i];j<=R[i];j++){
x-=cnt[j];
if(x<=0) return b[j];
}
}
x-=sum[i];
}
return -114514;
}
inline int get_pre(int x){
int r=get_rank(x),an;
if(r==1) return -2147483647;
an=get_kth(r-1);
return an;
}
inline int get_aft(int x){
int r=get_rank(x),an;
if(!cnt[x]) an=get_kth(r);//对于 x 不存在的情况需要特殊考虑
else an=get_kth(r+1);
if(an==-114514) return 2147483647;
else return an;
}
int main(){
n=read();
m=read();
for(int i=1;i<=n;i++){
a[i]=read();
b[++temp]=a[i];
}
for(int i=1;i<=m;i++){
int t1=read(),t2=read(),t3=read();
if(t1==3){
c[++cnt_c]={t2,t3};
b[++temp]=t3;
}
else{
int t4=read();
q[++cnt_q].opt=t1;
q[cnt_q].l=t2;
q[cnt_q].r=t3;
q[cnt_q].k=t4;
b[++temp]=t4;
q[cnt_q].id=cnt_q;
q[cnt_q].ti=cnt_c;
}
}
sort(b+1,b+1+temp);
temp=unique(b+1,b+1+temp)-b-1;
for(int i=1;i<=n;i++) a[i]=lower_bound(b+1,b+1+temp,a[i])-b;
for(int i=1;i<=cnt_c;i++) c[i].d=lower_bound(b+1,b+1+temp,c[i].d)-b;
for(int i=1;i<=cnt_q;i++) if(q[i].opt!=2) q[i].k=lower_bound(b+1,b+1+temp,q[i].k)-b;
t=pow(n,2.0/3.0);
sort(q+1,q+1+cnt_q,cmp);
t=sqrt(temp);
for(int i=1;i<=t;i++){
L[i]=(i-1)*t+1;
R[i]=i*t;
}
if(R[t]<temp){
t++;
L[t]=R[t-1]+1;
R[t]=temp;
}
for(int i=1;i<=t;i++){
for(int j=L[i];j<=R[i];j++){
pos[j]=i;
}
}
for(int i=1;i<=cnt_q;i++){
while(l>q[i].l) add(a[--l]);
while(r<q[i].r) add(a[++r]);
while(l<q[i].l) del(a[l++]);
while(r>q[i].r) del(a[r--]);
while(last<q[i].ti){
last++;
if(c[last].x<=r && c[last].x>=l){
add(c[last].d);
del(a[c[last].x]);
}
swap(a[c[last].x],c[last].d);
}
while(last>q[i].ti){
if(c[last].x<=r && c[last].x>=l){
add(c[last].d);
del(a[c[last].x]);
}
swap(a[c[last].x],c[last].d);
last--;
}
if(q[i].opt==1) ans[q[i].id]=get_rank(q[i].k);
else if(q[i].opt==2) ans[q[i].id]=get_kth(q[i].k);
else if(q[i].opt==4) ans[q[i].id]=get_pre(q[i].k);
else ans[q[i].id]=get_aft(q[i].k);
}
for(int i=1;i<=cnt_q;i++){
write(ans[i]);
putchar('\n');
}
return 0;
}
优缺点评价
优点:
- 代码相比于传统树套树较为简短,细节较少。
缺点:
- 无法在线回答询问。
回滚莫队
但是我还是决定想小标题。
回滚——我是小标题
我们依然从一道例题开始讲起。
题目链接。
在这道题目中,在莫队中插入元素比较简单,但是从莫队中删除元素较为困难,于是,我们可以只使用插入这一个操作,剩下的交给回滚解决,这就是回滚莫队的核心思想。
假设块长为 \(t\)。
首先,我们将询问离线下来,并且将询问按照左端点所处的块为第一关键字,右端点为第二关键字升序排序,这样左端点在同一块的询问就会变得连续,而它们的右端点将递增。按排序后的顺序处理询问,若该询问的左右端点在同一个块内,可以在 \(O(t)\) 的时间复杂度内暴力的解决这个询问。若它们不在同一个块内,则考虑该询问的左端点是否与上一个询问的左端点在同一块内,若否,则消去之前询问对答案的影响,将莫队的左端点设置为当前询问所处块的右端点 \(+1\),将莫队的右端点设置为当前询问所处块的右端点,接着,不断扩展莫队的右端点直到它与询问的右端点重合,再不断扩展莫队的左端点直到它与莫队的左端点重合,然后回答该询问,最后撤销莫队左端点的改动。
具体代码如下:
#include<bits/stdc++.h>
#define int long long
using namespace std;
inline int read(){register int t1=0,t2=0;register char x=getchar();while(x<'0' ||x>'9'){if(x=='-') t2|=1;x=getchar();}while(x>='0' && x<='9'){t1=(t1<<1)+(t1<<3)+(x^48),x=getchar();}return t2?-t1:t1;}
inline void write(int x){register int sta[105],top=0;if(x<0) putchar('-'),x=-x;do{sta[top++]=x%10,x/=10;}while(x);while(top) putchar(sta[--top]+48);}
int n,m,a[100005],b[100005],pos[100005],R[100005],L[100005],t,temp,l=1,r=0,now,cnt[100005],temp_cnt[100005],ans[100005],last,temp_l;
struct que{
int l,r,id;
}q[100005];
void build(){
t=sqrt(n);
for(int i=1;i<=t;i++){
L[i]=(i-1)*t+1;
R[i]=i*t;
}
if(R[t]<n){
t++;
R[t]=n;
L[t]=R[t-1]+1;
}
for(int i=1;i<=t;i++) for(int j=L[i];j<=R[i];j++) pos[j]=i;
}
bool cmp(que x,que y){
if(pos[x.l]!=pos[y.l]) return pos[x.l]<pos[y.l];
return x.r<y.r;
}
inline void add(int x,int& y){
cnt[a[x]]++;
y=max(y,cnt[a[x]]*b[a[x]]);
}
inline void del(int x){cnt[a[x]]--;}
signed main(){
n=read();
m=read();
build();
for(int i=1;i<=n;i++){
b[i]=read();
a[i]=b[i];
}
for(int i=1;i<=m;i++){
q[i].l=read();
q[i].r=read();
q[i].id=i;
}
sort(q+1,q+1+m,cmp);
sort(b+1,b+1+n);
temp=unique(b+1,b+1+n)-b-1;
for(int i=1;i<=n;i++) a[i]=lower_bound(b+1,b+1+temp,a[i])-b;
for(int i=1;i<=m;i++){
if(pos[q[i].l]==pos[q[i].r]){
for(int j=q[i].l;j<=q[i].r;j++){
temp_cnt[a[j]]++;
ans[q[i].id]=max(ans[q[i].id],temp_cnt[a[j]]*b[a[j]]);
}
for(int j=q[i].l;j<=q[i].r;j++) temp_cnt[a[j]]--;
}
else{
if(pos[q[i].l]!=last){
while(l<R[pos[q[i].l]]+1) del(l++);
while(r>R[pos[q[i].l]]) del(r--);
now=0;
last=pos[q[i].l];
}
while(r<q[i].r) add(++r,now);
temp_l=l;
temp=now;
while(temp_l>q[i].l) add(--temp_l,temp);
ans[q[i].id]=temp;
while(temp_l<l) del(temp_l++);
}
}
for(int i=1;i<=m;i++){
write(ans[i]);
putchar('\n');
}
return 0;
}
树上莫队
前置知识:最近公共祖先的求法。
我们还是从一道例题开始讲起。
题目链接。
我们先考虑给出的树满足链的性质的情况,在这种情况下,使用带修莫队就可以轻松地解决这个问题。
但是本题给出的是一棵树,这时就需要用到树上莫队的技术。
我们先使用 dfs 将给定的树拍成相应的括号序,记第 \(i\) 个点在括号序中第 \(1\) 次出现的位置为 \(st_i\),最后一次(即第 \(2\) 次)出现的位置为 \(ed_i\),第 \(i\) 个点和第 \(j\) 个点的最近公共祖先为 \(LCA(i,j)\)。对于询问中的一条路径 \(x \to y\) 满足 \(st_x \le st_y\)(不满足交换一下 \(x\) 和 \(y\) 就行),若 \(LCA(x,y) = x\),则使用 \(st_x\) 作为该次询问的左端点,使用 \(st_y\) 作为该次询问的右端点。否则,使用 \(ed_x\) 作为该次询问的左端点,\(st_y\) 作为该次询问的右端点,因为 \((st_x,ed_x)\) 这段路径并没有出现在询问的路径上。需要注意的是,在第 \(2\) 种情况中,询问区间没有包含 \(LCA(x,y)\),在计算答案时需要加上它的贡献。
若使用树链剖分求最近公共祖先,则代码如下:
#include<bits/stdc++.h>
#define int long long
using namespace std;
inline int read(){register int t1=0,t2=0;register char x=getchar();while(x<'0' ||x>'9'){if(x=='-') t2|=1;x=getchar();}while(x>='0' && x<='9'){t1=(t1<<1)+(t1<<3)+(x^48),x=getchar();}return t2?-t1:t1;}
inline void write(int x){register int sta[105],top=0;if(x<0) putchar('-'),x=-x;do{sta[top++]=x%10,x/=10;}while(x);while(top) putchar(sta[--top]+48);}
const int MAXN=1e6+5;
int n,m,Q,tot,cnt_q,cnt_c,now,l=1,r,last;
int v[MAXN],w[MAXN],a[MAXN],fa[MAXN],son[MAXN],dep[MAXN],siz[MAXN],top[MAXN],st[MAXN],ed[MAXN],rnk[MAXN],ans[MAXN],cnt[MAXN];
bool vis[MAXN];
vector<int> e[MAXN];
struct que{
int l,r,lca,id,ti;
}q[MAXN];
bool cmp(que x,que y){
if(x.l/Q!=y.l/Q) return x.l/Q<y.l/Q;
else if(x.r/Q!=y.r/Q) return x.r/Q<y.r/Q;
return x.ti<y.ti;
}
struct cha{
int x,y;
}c[MAXN];
inline void dfs1(int x){ //树剖顺便求括号序
son[x]=-1;
siz[x]=1;
st[x]=++tot;
rnk[tot]=x;
for(int i:e[x]){
if(!dep[i]){
dep[i]=dep[x]+1;
fa[i]=x;
dfs1(i);
siz[x]+=siz[i];
if(son[x]==-1 || siz[i]>siz[son[x]]) son[x]=i;
}
}
ed[x]=++tot;
rnk[tot]=x;
}
inline void dfs2(int x,int y){ //树剖
top[x]=y;
if(son[x]==-1) return;
dfs2(son[x],y);
for(int i:e[x]) if(i!=son[x] && i!=fa[x]) dfs2(i,i);
}
inline int lca(int x,int y){ //树剖求LCA
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]]) swap(x,y);
x=fa[top[x]];
}
if(dep[x]<dep[y]) return x;
else return y;
}
inline void add(int x){
cnt[x]++;
now+=v[x]*w[cnt[x]];
}
inline void del(int x){
now-=v[x]*w[cnt[x]];
cnt[x]--;
}
inline void change(int x){
if(!vis[x]) add(a[x]);
else del(a[x]);
vis[x]^=1;
}
inline void change_time(int x){ //进行修改操作
if(vis[c[x].x]){
add(c[x].y);
del(a[c[x].x]);
}
swap(c[x].y,a[c[x].x]);
}
signed main(){
n=read();
m=read();
Q=read();
for(int i=1;i<=m;i++) v[i]=read();
for(int i=1;i<=n;i++) w[i]=read();
for(int i=1;i<n;i++){
int t1=read(),t2=read();
e[t1].push_back(t2);
e[t2].push_back(t1);
}
for(int i=1;i<=n;i++) a[i]=read();
dep[1]=1;
dfs1(1);
dfs2(1,1);
while(Q--){
int t1=read(),t2=read(),t3=read();
if(t1==0){
cnt_c++;
c[cnt_c]={t2,t3};
}
else{
cnt_q++;
if(st[t2]>st[t3]) swap(t2,t3);
q[cnt_q].lca=lca(t2,t3);
q[cnt_q].id=cnt_q;
q[cnt_q].ti=cnt_c;
if(q[cnt_q].lca==t2){
q[cnt_q].l=st[t2];
q[cnt_q].r=st[t3];
q[cnt_q].lca=0;
}
else{
q[cnt_q].l=ed[t2];
q[cnt_q].r=st[t3];
}
}
}
Q=sqrt(tot);
sort(q+1,q+1+cnt_q,cmp);
for(int i=1;i<=cnt_q;i++){ //先计算再考虑修改也是可以的
while(last<q[i].ti) change_time(++last);
while(last>q[i].ti) change_time(last--);
while(l>q[i].l) change(rnk[--l]);
while(r<q[i].r) change(rnk[++r]);
while(l<q[i].l) change(rnk[l++]);
while(r>q[i].r) change(rnk[r--]);
if(q[i].lca) change(q[i].lca);
ans[q[i].id]=now;
if(q[i].lca) change(q[i].lca);
}
for(int i=1;i<=cnt_q;i++){
write(ans[i]);
putchar('\n');
}
return 0;
}