好题收集
好题,比较有 trick/套路的题,印象深刻的题
P4240 毒瘤之神的考验
拆式子与根号分治思想的极致融合。
给 \(n,m\),求
对 998244353 取模,多测。
\(n,m\le 10^5,T\le 10^4\)
设 \(n\le m\)。
先要知道一个经典的式子:
然后拆式子
枚举 \(d=\gcd(i,j)\)
后面那个直接莫反拆掉,把枚举扔外面
这个式子就很好看了,设
则原式为
\(f,g\) 直接预处理是可以的,因为 \(g\) 的数量是 \(n\log n\) 级别的,且 \(g\) 可以直接递推:
所以预处理 \(f,g\) 时间复杂度 \(O(n\log n)\)。
由于原式并不好整除分块,也不能暴力预处理全部信息,所以我们只能考虑分治。
设阈值为 \(B\),记原式为三元函数 \(h(a,b,n)=\sum\limits_{t=1}^n f(t)g(a,t)g(b,t)\),考虑当 \(a,b\le B\) 时直接暴力预处理答案,时空复杂度 \(O(nB^2)\),这个三元函数就可以数论分块了,即
\(h\) 也可以递推
否则可以得到 \(n/a\ge B,a\le n/B\),暴力统计答案,时间复杂度 \(O(n/B)\)。
综上,时间复杂度 \(O(n\log n+nB^2+T(\sqrt n+\frac{n}{B}))\),取 \(B=\sqrt[3]{T}=22\),可以通过,500~700 ms,空间复杂度 \(O(n\log n+nB^2)\)。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
using ll=long long;
const int maxn=1e5+7;
const int B=24;
const int maxb=B+2;
const int N=1e5;
const int mod=998244353;
bool st;
int mu[maxn],phi[maxn],pr[maxn],pcnt;
ll f[maxn],inv[maxn];
bool isp[maxn];
vector<ll>g[maxn],h[maxb][maxb];
bool ed;
void solve(){
int n,m,l=1,r;
cin>>n>>m;
if(n>m) swap(n,m);
ll ans=0;
for(;l<=m/B;l++)
ans=(ans+f[l]*g[l][n/l]%mod*g[l][m/l]%mod)%mod;
for(;l<=n;l=r+1){
r=min(n/(n/l),m/(m/l));
ans=(ans+h[n/l][m/l][r]-h[n/l][m/l][l-1]+mod)%mod;
}
cout<<ans<<'\n';
}
signed main(){
cerr<<(&ed-&st)/1048576.0<<" MB\n";
inv[1]=mu[1]=phi[1]=1;
for(int i=2;i<=N;i++){
inv[i]=(mod-mod/i)*inv[mod%i]%mod;
if(!isp[i]) pr[++pcnt]=i,mu[i]=-1,phi[i]=i-1;
for(int j=1;j<=pcnt&&i*pr[j]<=N;j++){
isp[i*pr[j]]=1;
if(i%pr[j]==0){
mu[i*pr[j]]=0;
phi[i*pr[j]]=pr[j]*phi[i];
break;
}
mu[i*pr[j]]=-mu[i];
phi[i*pr[j]]=phi[i]*phi[pr[j]];
}
}
for(int i=1;i<=N;i++){
g[i].resize(N/i+2); g[i][0]=0;
for(int j=1;j<=N/i;j++)
g[i][j]=(g[i][j-1]+phi[i*j])%mod;
}
for(int i=1;i<=N;i++)
for(int j=1;j<=N/i;j++)
f[j*i]=(f[j*i]+i*mu[j]*inv[phi[i]]%mod+mod)%mod;
for(int j=1;j<=B;j++){
for(int k=1;k<=B;k++){
h[j][k].resize(N/k+7); h[j][k][0]=0;
for(int i=1;i<=N/k;i++)
h[j][k][i]=(h[j][k][i-1]+f[i]*g[i][j]%mod*g[i][k]%mod)%mod;
}
}
int TEST;
cin>>TEST;
while(TEST--){
solve();
}
return 0;
}
CF1491H Yuezheng Ling and Dynamic Tree
*3400
还是分块大佬
信友队还是放了一道可做题。
给你一棵树,\(i\) 与 \(fa(i)(<i)\) 连边,\(q\) 次操作:
- 将 \(\forall i\in[l,r](l>1)\),\(fa(i)\) 变为 \(\max(fa(i)-x,1)\);
- 求 \(u,v\) 的 LCA。
\(n,q\le 10^5,x\ge 1\)
考虑按下标分块,记 \(jmp(i)\) 表示点 \(i\) 第一个通过走父亲跳出块的下标,显然有 \(jmp(i)<i\)。
对于修改,散块暴力重构,整块也暴力重构——注意到 \(x\ge 1\),所以每个块至多进行 \(O(B)\) 次暴力修改后就满足对于所有块内的点一次就可以跳出去(即 \(block(fa(i))<block(i)\)),这有什么用呢?这说明,之后的更改对于块内的所有数都是同步的,与实际数无关,于是我们可以记个 tag 表示当前整块的懒标记,前 \(O(B)\) 次暴力下传,后面的就不用了。时间复杂度均摊 \(O(B)\)。
对于查询,类似树剖,交替跳 \(jmp\),每次跳下标大的。如果跳到一个块里了,且两个数的 \(jmp\) 相等,就再暴力跳 LCA 即可。时间复杂度 \(O(\frac{n}{B}+B)\)
综上时间复杂度 \(O(n+q(\frac{n}{B}+B))\),取 \(B=\sqrt n\)。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+7;
int B,n,q,bcnt;
int fa[maxn],st[maxn],ed[maxn],bl[maxn],jmp[maxn],ktag[maxn],siz[maxn],add[maxn];
void dfs(int u){
int now=bl[u],tmp=u;
for(;u&&bl[u]==now&&!jmp[u];u=fa[u]);
if(bl[u]!=now){
int to=u; u=tmp;
for(;u&&bl[u]==now;u=fa[u]) jmp[u]=to;
}else{
int to=u; u=tmp;
for(;u&&!jmp[u];u=fa[u]) jmp[u]=jmp[to];
}
}
void pushdown(int id){
for(int i=st[id];i<=ed[id];i++){
if(i==1) continue;
fa[i]=max(fa[i]-add[id],1);
}
add[id]=0;
for(int i=st[id];i<=ed[id];i++){
if(bl[fa[i]]!=bl[i]) jmp[i]=fa[i];
else jmp[i]=jmp[fa[i]];
}
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin>>n>>q;
B=sqrt(n)+1;
bcnt=(n-1)/B+1;
bl[1]=1;
for(int i=2;i<=n;i++){
cin>>fa[i];
bl[i]=(i-1)/B+1;
}
for(int i=1,j=1;i<=n;i+=B,j++){
st[j]=i;
ed[j-1]=i-1;
siz[j]=B;
}
ed[bcnt]=n;siz[bcnt]=ed[bcnt]-st[bcnt]+1;
for(int i=1;i<=bcnt;i++)
for(int j=st[i];j<=ed[i];j++)
if(!jmp[j]) dfs(j);
while(q--){
int op,x,y,z;
cin>>op>>x>>y;
if(op==1){
cin>>z;
if(bl[x]==bl[y]){
for(int i=x;i<=y;i++)
fa[i]=max(fa[i]-z,1);
pushdown(bl[x]);
}else{
for(int i=x;i<=ed[bl[x]];i++)
fa[i]=max(fa[i]-z,1);
pushdown(bl[x]);
add[bl[x]]=0;
for(int j=bl[x]+1;j<=bl[y]-1;j++){
if(add[j]<=n) add[j]+=z;
ktag[j]++;
if(ktag[j]<=siz[j]) pushdown(j);
}
for(int i=st[bl[y]];i<=y;i++)
fa[i]=max(fa[i]-z,1);
pushdown(bl[y]);
}
}else{
while(1){
if(x<y) swap(x,y);
if(bl[x]!=bl[y]) x=max(jmp[x]-add[bl[x]],1);
else{
if(max(jmp[x]-add[bl[x]],1)!=max(jmp[y]-add[bl[y]],1))
x=max(jmp[x]-add[bl[x]],1),y=max(jmp[y]-add[bl[y]],1);
else break;
}
}
while(x!=y){
if(x<y) swap(x,y);
x=max(fa[x]-add[bl[x]],1);
}
cout<<x<<'\n';
}
}
return 0;
}
ARC186B Typical Permutation Descriptor
这棵树这么好看感觉很典啊,wc 怎么是树上拓扑序计数板子
给你一个序列 \(a\) 满足 \(a_i<i\),求满足以下条件的排列 \(p\) 的数量:
- \(p_j>p_i>p_{a_i}(j\in(a_i,i))\)
\(n\le 3\times 10^5\),保证有解
由于保证有解,考虑观察有解的情况所带来的性质:
- 区间 \([a_i,i]\) 要么把前面的若干区间完全包含,要么左端点与相邻区间端点相交;
由性质 1 与偏序关系可知,假如以偏序关系(大于号连接的两边)连边,\(p_i\) 为点权,以 \(p_0\) 为根,则形成一棵满足 \(u\) 子树内的点权大于 \(u\) 点权的树。(由不交想到转化为树上问题)
这棵树的性质很好啊,当你用拓扑序遍历这棵树他一定合法,即为充分必要条件了,虽然我没看出来。
接下来就是一个裸的树上拓扑序计数了,也是个结论,即
证明:
考虑树形 DP。设 \(f(u)\) 为以 \(u\) 为根子树的拓扑序数量。
考虑合并两棵子树 \(v_1,v_2\),先把两棵子树的方案数乘起来然后考虑顺序,即在 \(siz(v_1)+siz(v_2)\) 个数里选掉 \(siz(v_1)\) 个数。更一般的,多个子树相当于叠加,而且组合约掉了,则有转移\[\begin{aligned} f(u)&=\binom{siz(v_1)+siz(v_2)}{siz(v_1)}\binom{siz(v_1)+siz(v_2)+siz(v_3)}{siz(v_1)+siz(v_2)}\cdots\binom{siz(u)-1}{siz(v_1)+siz(v_2)+\cdots+siz(v_{x-1})}\prod\limits_{v\in son(u)}f(v)\\ &=\frac{(siz(u)-1)!}{\prod\limits_{v\in son(u)}siz(v)!}\prod\limits_{v\in son(u)}f(v)\\ &=\frac{(siz(u)-1)!}{\prod\limits_{v\in son(u)}siz(v)!}\prod\limits_{v\in son(u)}f(v)\\ \end{aligned}\]考虑把每个 \((siz(u)-1)!\) 与 \(siz(v)!\) 相抵消,剩下 \(\prod\limits_{i=2}^n\frac{1}{siz(i)}\) 以及 \((siz(1)-1)!\),写得好看点,都乘个 \(siz(1)\),即得上式。
时间复杂度 \(O(n)\)。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int maxn=3e5+7;
const int mod=998244353;
int n,a[maxn],siz[maxn],inv[maxn];
vector<int>v[maxn],e[maxn];
int facn=1;
void dfs(int u,int fa){siz[u]=u>0;for(int v:e[u])if(v!=fa){dfs(v,u);siz[u]+=siz[v];}}
signed main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
v[a[i]].emplace_back(i);
facn=facn*i%mod;
}
queue<int>q;
q.push(0);
while(!q.empty()){
int now=q.front();
q.pop();
if(!v[now].empty()){
q.push(v[now].back());
e[now].emplace_back(v[now].back());
v[now].pop_back();
}
if(now&&!v[a[now]].empty()){
q.push(v[a[now]].back());
e[now].emplace_back(v[a[now]].back());
v[a[now]].pop_back();
}
}
dfs(0,0);
inv[1]=1;
for(int i=2;i<=n;i++)
inv[i]=(mod-mod/i)*inv[mod%i]%mod;
for(int i=1;i<=n;i++)
facn=facn*inv[siz[i]]%mod;
cout<<facn;
return 0;
}
P3332 [ZJOI2013] K大数查询
线段树套权值线段树怎么下传标记啊,怎么tj都没有这种写法啊,哦反着套就行了。笑点解析:权值线段树的作用 = 整体二分。
维护 \(n\) 个 multiset,支持以下操作:
- 在编号为 \([l,r]\) 的 multiset 中加入 \(c\)
- 查询 \(\bigcup\limits_{i=l}^r s_i\) 的第 \(k\) 大
\(|c|\le n\le 5\times 10^4,k< 2^{63}\)
非常显然的一个想法就是线段树套权值线段树:线段树每个节点开一棵权值线段树,每次在节点上打懒标记并单点修改。但是标记无法叠加。时空直接爆炸。
考虑权值线段树套线段树。每个节点维护值在 \([L,R]\) 范围内的全局线段树,每次修改相当于将包含 \([c,c]\) 的节点上的 \([l,r]\) 区间加 1.查询直接看当前节点在区间 \([l,r]\) 中的数的数量,跑 kth 即可。时空复杂度 \(O(n\log^2 n)\),空间至少开 512 倍,或者 vector。
注意 \(c\) 可能为负。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define lc tr[now].ls
#define rc tr[now].rs
#define mid ((l+r)>>1)
#define lson (now<<1)
#define rson (now<<1|1)
const int maxn=1e5+14;
const int N=5e4+3;
using ll=long long;
struct node{
int ls,rs;
ll val,tag;
}tr[maxn<<8];
int n,m,opt[maxn],L[maxn],R[maxn],root[maxn<<2];
ll q[maxn];
int tot;
int addnode(){
tr[++tot]={0,0,0,0};
return tot;
}
void pushup(int now){
tr[now].val=tr[lc].val+tr[rc].val;
}
void pushdown(int now,int l,int r){
if(!tr[now].tag) return;
if(!lc) lc=addnode();
if(!rc) rc=addnode();
tr[lc].tag+=tr[now].tag;
tr[rc].tag+=tr[now].tag;
tr[lc].val+=tr[now].tag*(mid-l+1);
tr[rc].val+=tr[now].tag*(r-mid);
tr[now].tag=0;
}
void modi(int &now,int l,int r,int L,int R,ll x){
if(!now) now=addnode();
if(L<=l&&r<=R){
tr[now].tag+=x;
tr[now].val+=x*(r-l+1);
return;
}
pushdown(now,l,r);
if(L<=mid) modi(lc,l,mid,L,R,x);
if(mid+1<=R) modi(rc,mid+1,r,L,R,x);
pushup(now);
}
ll qu(int &now,int l,int r,int L,int R){
if(!now) now=addnode();
if(L<=l&&r<=R) return tr[now].val;
pushdown(now,l,r);
ll res=0;
if(L<=mid) res+=qu(lc,l,mid,L,R);
if(mid+1<=R) res+=qu(rc,mid+1,r,L,R);
return res;
}
void modify(int now,int l,int r,int L,int R,ll x){
modi(root[now],1,n,L,R,1);
if(l==r) return;
if(x<=mid) modify(lson,l,mid,L,R,x);
else modify(rson,mid+1,r,L,R,x);
}
int query(int now,int l,int r,int L,int R,ll x){
if(l==r) return l;
ll res=qu(root[rson],1,n,L,R);
if(x<=res) return query(rson,mid+1,r,L,R,x);
else return query(lson,l,mid,L,R,x-res);
}
signed main(){
cin>>n>>m;
for(int i=1;i<=m;i++){
cin>>opt[i]>>L[i]>>R[i]>>q[i];
}
for(int i=1;i<=m;i++){
if(opt[i]==1){
modify(1,1,2*N,L[i],R[i],q[i]+N);
}else if(opt[i]==2){
cout<<query(1,1,2*N,L[i],R[i],q[i])-N<<'\n';
}
}
return 0;
}
P3527 [POI2011] MET-Meteors
整体二分的题都可以树套树做。——includer
唉我不会。
有一个长为 \(m\) 的环,每个位置都有一个颜色 \(c_i\),执行 \(q\) 次操作将区间 \([l_i,r_i]\) 的值 \(v_i\) 加上 \(a_i\),每个颜色有一个要求 \(h_i\)。对于每个颜色,求出其最早满足 \(v_i\ge h_i\) 的操作编号 \(p_i\)。
\(n,m,q\le 3\times 10^5\)
考虑我们暴力怎么做。对于每个颜色,二分 \(p_i\),然后做覆盖,时间复杂度 \(O(n^2)\)。
整体二分,相当于同时对所有询问二分。类似分治/归并思想。对于当前区间的一组询问 \(\{l,r,Q\}\),check mid 时 \(Q_i\) 是否合法,若合法则 \(Q_i\) 的答案一定 \(\le mid\),否则 \(>mid\),据此分成 \(\{l,mid,Q_l\},\{mid+1,r,Q_r\}\) 两部分继续递归。总共递归 \(O(\log n)\) 层,时间复杂度 \(O(n\log n F(n)+q\log nF(n))\),\(F(n)\) 指单次 check 的复杂度。
注意: 递归右区间时需要保留左边的影响。
void solve(int l,int r,vector<int>q){
// 用 vector 会慢一点,可以考虑开一个全局数组,并传当前的区间询问所在的下标区间 [L,R]
if(l==r){
for(int i:q) ans[i]=l;
return;
}
int mid=(l+r)>>1;
vector<int>v1,v2;
insert(l,mid);
for(int i:q){
if(check(i)) v1.push_back(i);
else v2.push_back(i);
}
solve(mid+1,r,v2); // 区间 [mid+1,r] 不需要撤销影响
erase(l,mid);
solve(l,mid,v1); // 区间 [l,mid] 需要撤销影响
}
整体二分都很板,只要写出 check 就差不多了。本题就相当于每次执行 \([l,mid]\) 的操作。判断所有位置颜色的和是否大于等于 \(h_i\),然后撤销即可。这个用一个差分树状数组维护即可。时间复杂度 \(O(n\log^2 n)\)。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=3e5+7;
const int N=3e5+3;
int tr[maxn];
int lowbit(int x){return x&-x;}
void add(int x,int c){for(int i=x;i<=N;i+=lowbit(i)) tr[i]+=c;}
int query(int x){
int res=0;
for(int i=x;i;i-=lowbit(i)) res+=tr[i];
return res;
}
int n,m,a[maxn],l[maxn],r[maxn],h[maxn],Q,ans[maxn];
vector<int>v[maxn];
void rain(int id){
if(l[id]<=r[id]) add(l[id],a[id]), add(r[id]+1,-a[id]);
else add(1,a[id]),add(r[id]+1,-a[id]),add(l[id],a[id]),add(m+1,-a[id]);
}
void quash(int id){
if(l[id]<=r[id]) add(l[id],-a[id]), add(r[id]+1,a[id]);
else add(1,-a[id]),add(r[id]+1,a[id]),add(l[id],-a[id]),add(m+1,a[id]);
}
void solve(int l,int r,vector<int>q){
if(l==r){
for(int i:q) ans[i]=l;
return;
}
int mid=(l+r)>>1;
vector<int>v1,v2;
for(int i=l;i<=mid;i++) rain(i);
for(int i:q){
int sum=0;
for(int j:v[i]) sum+=query(j);
if(sum>=h[i]) v1.push_back(i);
else v2.push_back(i);
}
solve(mid+1,r,v2);
for(int i=l;i<=mid;i++) quash(i);
solve(l,mid,v1);
}
signed main(){
cin>>n>>m;
for(int i=1;i<=m;i++){
cin>>a[i];
v[a[i]].push_back(i);
}
for(int i=1;i<=n;i++){
cin>>h[i];
}
cin>>Q;
for(int i=1;i<=Q;i++){
cin>>l[i]>>r[i]>>a[i];
}
vector<int>t;
for(int i=1;i<=n;i++) t.push_back(i);
solve(1,Q+1,t);
for(int i=1;i<=n;i++){
if(ans[i]!=Q+1)
cout<<ans[i]<<'\n';
else cout<<"NIE\n";
}
return 0;
}
P9870 [NOIP2023] 双序列拓展
NOIP 出这个我原地退役,这么喜欢 Ad-hoc
给你两个序列 \(a_n,b_m\),询问是否存在一组 \(a,b\) 的扩展 \(A_L,B_L(L=+\infty)\) 满足 \(\forall(A_i-B_i)(A_j-B_j)>0,i,j\in[1,+\infty)\)。
\(q\) 次修改 \(a,b\) 中的一些数。
\(a_n\) 的扩展 \(A_L\) 定义为存在一个序列 \(t=\{t_1,t_2,\cdots,t_n\}\) 满足 \(\sum t_i=L\) 且 \(A_L=\{a_1\times t_1+a_2\times t_2+\cdots+a_n\times t_n\}\),其中 \(+,\times\) 表示拼接与重复。
\(n,m\le 5\times 10^5,q\le 50\)
逆天 Ad-hoc。建议 FTR 11/AT 15.9。
与扩展长度相关的算法显然先要枪毙。然后就不会了。
不是哥们。
考虑序列中的数的种类至多 \(n+m\) 个,考虑设计相关复杂度的算法。
\(\forall(A_i-B_i)(A_j-B_j)>0\) 唉这个我认识。即要满足 \(\forall i,A_i<B_i\) 或 \(\forall i,A_i>B_i\),这两种本质一样。先考虑第一种。
考虑到我们只有边界条件要关心,其余的位置便自动合法。当扩展长度 \(L\to L+1\) 时,设当前 \(a\) 使用的是 \(a_i\) 扩展,\(b\) 使用的是 \(b_j\) 扩展,则只会出现四种情况:
- \((i,j)\to(i,j)\):还是使用原先的两个,所以不是边界,不用管;
- \((i,j)\to(i+1,j)\):\(a\) 变为使用 \(a_{i+1}\),这种情况需要满足 \(a_{i+1}<b_j\) 才能转移;
- \((i,j)\to(i,j+1)\):\(b\) 变为使用 \(b_{j+1}\),这种情况需要满足 \(a_{i}<b_{j+1}\) 才能转移;
- \((i,j)\to(i+1,j+1)\):\(a\) 变为使用 \(a_{i+1}\),\(b\) 变为使用 \(b_{j+1}\),这种情况需要满足 \(a_{i+1}<b_{j+1}\) 才能转移;
转移都出来了,直接 DP 是 \(O(qnm)\) 的,可以得 35 分。设 \(f(i,j)\) 表示当前 \(a\) 使用的是 \(a_i\) 扩展,\(b\) 使用的是 \(b_j\) 扩展是否合法,则有:
很不能优化的样子。考虑上面在干啥。
相当于有一个矩阵 \(c_{i,j}=[a_i<b_j]\) 要从 \((1,1)\) 走到 \((n,m)\) 可以向右、下、右下走,且要满足 \(c_{i,j}=1\)。
考虑什么时候无解。当 \(a_{\min}\ge b_{\min}/b_{\max}\le a_{\max}\) 时 \(b_{\min}/a_{\max}\) 那一列/排的 \(c\) 都为 0,所以无解。
特殊性质:\(a_n \ll a_1<b_1\ll b_m\),所以有 \(a_n< \forall b_j,b_m>\forall a_i\)。即最后一行/列都是 1。所以我们只要到达最后一行/列即可。
我们只要走到 \(n-1\) 行或 \(m-1\) 列就到了。递归地看,问题变小。然后其实变成了上面的子问题。直接递归求解即可(合法情况下,即 \(\exists a_i=a_{\min}<\forall b_j=b_{\min}/\exists b_j=b_{\max}>\forall a_i=a_{\max}\),说明我们只要走到 \(i\) 行/\(j\) 列即可,再次缩小范围求解)。
没有特殊性质也一样。仅第一步不同而已。预处理前/后缀 \(\min/\max\),时间复杂度 \(O(q(n+m))\)。
code
#include<bits/stdc++.h>
using namespace std;
const int maxn=5e5+7;
int a[maxn],b[maxn],f[maxn],g[maxn],ta[maxn],tb[maxn];
#define get(A,p) {A[i]>A[p.mx] ? i : p.mx, A[i]<A[p.mi] ? i : p.mi}
struct node{
int mx,mi;
node(int x=0,int y=0): mx(x),mi(y){}
}pren[maxn],prem[maxn],sufn[maxn],sufm[maxn];
void update(int n,int m){
pren[1]={1,1}; sufn[n]={n,n};
prem[1]={1,1}; sufm[m]={m,m};
for(int i=2;i<=n;i++) pren[i]=get(f,pren[i-1]);
for(int i=2;i<=m;i++) prem[i]=get(g,prem[i-1]);
for(int i=n-1;i ;i--) sufn[i]=get(f,sufn[i+1]);
for(int i=m-1;i ;i--) sufm[i]=get(g,sufm[i+1]);
}
bool check1(int x,int y,int n,int m){
if(x==1||y==1) return 1;
node X=pren[x-1],Y=prem[y-1];
if(f[X.mi]<g[Y.mi]) return check1(X.mi,y,n,m);
if(f[X.mx]<g[Y.mx]) return check1(x,Y.mx,n,m);
return 0;
}
bool check2(int x,int y,int n,int m){
if(x==n||y==m) return 1;
node X=sufn[x+1],Y=sufm[y+1];
if(f[X.mi]<g[Y.mi]) return check2(X.mi,y,n,m);
if(f[X.mx]<g[Y.mx]) return check2(x,Y.mx,n,m);
return 0;
}
bool solve(int tta[],int ttb[],int n,int m){
if(tta[1]>=ttb[1]) return 0;
for(int i=1;i<=n;i++) f[i]=tta[i];
for(int i=1;i<=m;i++) g[i]=ttb[i];
update(n,m);
node X=pren[n],Y=prem[m];
if(f[X.mi]>=g[Y.mi] || f[X.mx]>=g[Y.mx]) return 0;
return check1(X.mi,Y.mx,n,m) && check2(X.mi,Y.mx,n,m);
}
signed main(){
int c,n,m;
cin>>c>>n>>m;
int T;
cin>>T;
for(int i=1;i<=n;i++){
cin>>a[i];
}
for(int i=1;i<=m;i++){
cin>>b[i];
}
cout<<"01"[solve(a,b,n,m)||solve(b,a,m,n)];
while(T--){
for(int i=1;i<=n;i++) ta[i]=a[i];
for(int i=1;i<=m;i++) tb[i]=b[i];
int k1,k2;
cin>>k1>>k2;
for(int i=1,x,y;i<=k1;i++){
cin>>x>>y;
ta[x]=y;
}
for(int i=1,x,y;i<=k2;i++){
cin>>x>>y;
tb[x]=y;
}
cout<<"01"[solve(ta,tb,n,m)||solve(tb,ta,m,n)];
}
return 0;
}