好题收集

好题,比较有 trick/套路的题,印象深刻的题

P4240 毒瘤之神的考验

拆式子与根号分治思想的极致融合。

\(n,m\),求

\[\sum\limits_{i=1}^n\sum\limits_{j=1}^m \varphi(ij) \]

对 998244353 取模,多测。

\(n,m\le 10^5,T\le 10^4\)

\(n\le m\)

先要知道一个经典的式子:

\[\varphi(ij)=\frac{\varphi(i)\varphi(j)\gcd(i,j)}{\varphi(\gcd(i,j))} \]

然后拆式子

\[\begin{aligned} \sum\limits_{i=1}^n\sum\limits_{j=1}^m \varphi(ij)&=\sum\limits_{i=1}^n\sum\limits_{j=1}^m \frac{\varphi(i)\varphi(j)\gcd(i,j)}{\varphi(\gcd(i,j))} \end{aligned}\]

枚举 \(d=\gcd(i,j)\)

\[\begin{aligned} \sum\limits_{i=1}^n\sum\limits_{j=1}^m \frac{\varphi(i)\varphi(j)\gcd(i,j)}{\varphi(\gcd(i,j))}&=\sum\limits_{d=1}^n\sum\limits_{i=1}^n\sum\limits_{j=1}^m \frac{\varphi(i)\varphi(j)d[\gcd(i,j)=d]}{\varphi(d)}\\ &=\sum\limits_{d=1}^n\frac{d}{\varphi(d)}\sum\limits_{i=1}^n\sum\limits_{j=1}^m \varphi(i)\varphi(j)[\gcd(i,j)=d]\\ &=\sum\limits_{d=1}^{n}\frac{d}{\varphi(d)}\sum\limits_{i=1}^{n/d}\sum\limits_{j=1}^{m/d} \varphi(id)\varphi(jd)[\gcd(i,j)=1]\\ \end{aligned}\]

后面那个直接莫反拆掉,把枚举扔外面

\[\begin{aligned} \sum\limits_{d=1}^{n}\frac{d}{\varphi(d)}\sum\limits_{i=1}^{n/d}\sum\limits_{j=1}^{m/d} \varphi(id)\varphi(jd)[\gcd(i,j)=1]&=\sum\limits_{d=1}^{n}\frac{d}{\varphi(d)}\sum\limits_{i=1}^{n/d}\sum\limits_{j=1}^{m/d} \varphi(id)\varphi(jd)\sum\limits_{t\mid i,t\mid j}\mu(t)\\ &=\sum\limits_{d=1}^{n}\frac{d}{\varphi(d)}\sum\limits_{t=1}^{n/d}\mu(t)\sum\limits_{i=1}^{n/d}\sum\limits_{j=1}^{m/d} \varphi(idt)\varphi(jdt)\\ &=\sum\limits_{t=1}^{n}\sum\limits_{d\mid t}\frac{d\mu(\frac{t}{d})}{\varphi(d)}\sum\limits_{i=1}^{n/t}\varphi(it)\sum\limits_{j=1}^{m/t} \varphi(jt)\\ \end{aligned}\]

这个式子就很好看了,设

\[f(n)=\sum\limits_{d|n}\frac{d\mu(\frac{n}{d})}{\varphi(d)},g(n,k)=\sum\limits_{i=1}^{n}\varphi(ik) \]

则原式为

\[\sum\limits_{t=1}^n f(t)g(n/t,t)g(m/t,t) \]

\(f,g\) 直接预处理是可以的,因为 \(g\) 的数量是 \(n\log n\) 级别的,且 \(g\) 可以直接递推:

\[g(n,k)=g(n-1,k)+\varphi(nk) \]

所以预处理 \(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)\),这个三元函数就可以数论分块了,即

\[\sum\limits_{n/l=m/l,n/r=m/r}h(n/l,m/l,r)-h(n/l,m/l,l-1) \]

\(h\) 也可以递推

\[h(a,b,n)=h(a,b,n-1)+f(n)g(a,n)g(b,n) \]

否则可以得到 \(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\)保证有解

由于保证有解,考虑观察有解的情况所带来的性质:

  1. 区间 \([a_i,i]\) 要么把前面的若干区间完全包含,要么左端点与相邻区间端点相交;

由性质 1 与偏序关系可知,假如以偏序关系(大于号连接的两边)连边,\(p_i\) 为点权,以 \(p_0\) 为根,则形成一棵满足 \(u\) 子树内的点权大于 \(u\) 点权的树。(由不交想到转化为树上问题)

这棵树的性质很好啊,当你用拓扑序遍历这棵树他一定合法,即为充分必要条件了,虽然我没看出来

接下来就是一个裸的树上拓扑序计数了,也是个结论,即

\[\frac{n!}{\prod\limits_{i=1}^n siz(i)} \]

证明:
考虑树形 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\) 扩展是否合法,则有:

\[f(i,j)=[a_i<b_j]\And(f(i-1,j)\mid f(i,j-1)\mid f(i-1,j-1)) \]

很不能优化的样子。考虑上面在干啥。

相当于有一个矩阵 \(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;
}
posted @ 2024-11-04 21:26  view3937  阅读(8)  评论(0编辑  收藏  举报
Title