P9358题解

简化题意

给定一个 \(n \times (m+1)\) 的矩阵,开始时只有同一行有边,有 \(q\) 次操作,会在相邻的两行建一条边,或者是询问你从 \(x\) 行的第一列开始走,会走到哪里(有边必须走,不能走重复的边,保证路径唯一)。

做法

在模拟赛的时候想到了一个关于线段树的做法,就是对每一行开一棵线段树,然后对于一次建桥的操作,将 \(x\) 前面的全部修改为 \(y\) 的答案,对于 \(y\) 前面的全部修改为 \(x\) 的答案,但是复杂度有点爆炸,于是就去打暴力了。

但是赛后一想,发现我们其实只需要维护每一行开头的答案就可以了,即维护一个数组 \(to_i\) 表示从第 \(i\) 行开始可以到达的行。虽然查询是 \(O(1)\) 的,但是修改是 \(O(n)\) 的。我们需要想办法优化,想到了很久之前做过的一道分块题 P3203 [HNOI2010] 弹飞绵羊。所以我们可以对于 \(m\) 分块,则 \(to_{i,j}\) 表示从第 \(j\) 行的第 \(i\) 个块的左端点出发可以达到的下一个块的左端点的行,也就是从 \(j\) 行的 \(L_i\) 列出发,前往 \(R_i+1\) 列到达的行(\(L\)\(R\) 为块的左右端点)。

那么查询直接从 \(x,1\) 往前走就行了。至于修改,我们维护两个节点,\(s,t\) 分别从 \(x,x+1\) 行的第 \(y\) 列开始,往这个块的左端点走,如果有桥,就过桥,就可以找到从哪个节点出发可以到达 \(s,t\),将其交换即可。

查询经过了 \(\sqrt{m}\) 个块,修改经过了一个大小为 \(\sqrt{m}\) 的块,所以时间复杂度也就是 \(O(q\sqrt{m})\)

代码
#include<bits/stdc++.h>
using namespace std;
const int N=100005,M=505;
int n,m,q;
int t;
int id[N],L[M],R[M];
int to[M][N];
map<int,int>g[N];
void update(int x,int y){
    int yid=id[y];
    g[x][y]=x+1;
    g[x+1][y]=x;
    int u=x,v=x+1;
    for(int i=y;i>=L[yid];i--){
        if(g[u].count(i))u=g[u][i];
        if(g[v].count(i))v=g[v][i];
    }
    swap(to[yid][u],to[yid][v]);
    return;
}
int query(int x){
    for(int i=1;i<=t;i++)x=to[i][x];
    return x;
}
signed main(){
    ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    cin>>n>>m>>q;
    t=sqrt(m);t+=(t*t!=m);
    for(int i=1;i<=t;i++){
        L[i]=R[i-1]+1;
        R[i]=min(m,i*t);
        for(int j=L[i];j<=R[i];j++)id[j]=i;
        for(int j=1;j<=n;j++)to[i][j]=j;
    }
    while(q--){
        int op,x,y;cin>>op>>x;
        if(op==1){
            cin>>y;
            update(x,y);
        }else cout<<query(x)<<"\n";
    }
    return 0;
}
posted @ 2026-08-17 12:02  tangkaiming  阅读(2)  评论(0)    收藏  举报