1 条题解

  • 0
    @ 2025-10-30 10:29:20

    野猪路径规划问题(Wild Boar)题解

    这道题的核心是在动态修改的节点序列中,寻找满足“不沿原路返回前一个到达节点”规则的最短路径总和。解题关键在于预处理图中特殊路径的最短距离,再用线段树维护动态序列的路径总和,平衡预处理复杂度与动态查询效率。

    一、题目分析与核心规则

    1. 问题背景

    • 森林是含 (N) 个节点、(M) 条带权无向边的连通图,边无重复。
    • 初始有长度为 (L) 的节点序列 (X_1,X_2,\dots,X_L),JOI 需从 (X_1) 依次访问到 (X_L)。
    • 每次修改将 (X_{P_k}) 改为 (Q_k),需输出修改后“合法路径”的最短总长度(无法则输出 -1)。

    2. 核心规则:合法路径的判定

    关键约束是“不能沿原路返回前一个到达的节点”,即:

    • 若从节点 (u) 经边 (e) 到达节点 (v),则从 (v) 出发的下一条边不能是 (e)(原路返回)。
    • 例如:从 (1) 经边 (e1) 到 (2),则从 (2) 出发不能直接经 (e1) 回 (1),但可经其他边(如 (e2) 到 (3))后,再经 (e1) 回 (1)。

    二、核心思路:预处理 + 动态维护

    1. 问题拆解

    整个路径可拆分为 (L-1) 段:(X_1 \to X_2)、(X_2 \to X_3)、…、(X_{L-1} \to X_L)。总长度是各段最短合法路径长度之和,因此:

    • 预处理:提前计算任意两个节点 (u \to v) 的所有合法路径的最短距离(考虑路径首尾边的约束)。
    • 动态维护:用线段树维护序列中相邻节点对的路径长度,支持单点修改(修改 (X_P) 会影响 (X_{P-1} \to X_P) 和 (X_P \to X_{P+1}) 两段),快速查询总和。

    2. 预处理关键:路径的“状态”定义

    由于合法路径受“前一条边”约束,需用“边”来定义路径状态:

    • 定义 (L[e1][e2]):从边 (e1) 的终点出发,经合法路径到达边 (e2) 的起点,且路径不包含 (e1) 的反向边的最短长度。
      • 边 (e1) 是“进入当前节点的边”,边 (e2) 是“离开当前节点的边”,确保不原路返回。
    • 进一步将边映射到节点对:对任意节点对 (u \to v),预处理 4 种状态的最短距离(对应路径首尾边的不同约束),存储在 dis00dis01dis10dis11 中,覆盖所有合法路径场景。

    3. 动态维护关键:线段树

    • 线段树每个叶子节点对应序列中相邻节点对 (X_i \to X_{i+1}),存储该节点对 4 种状态的最短距离。
    • 线段树内部节点通过“合并”操作,计算两段路径拼接后的最短距离(确保前一段的尾边与后一段的头边满足合法约束)。
    • 修改 (X_P) 时,只需更新叶子节点 (P-1) 和 (P),再向上合并,即可快速得到新的总长度。

    三、完整代码实现

    #include <bits/stdc++.h>
    using namespace std;
    #define ll long long
    const ll N = 4005, M = 1e5 + 5, inf = 1e18;
    int n, m, q, len;
    int p1[N], p2[N], val[N], tot1 = 1;  // p1[e]:边e的起点,p2[e]:边e的终点,val[e]:边e的权值,tot1:边的总数(从2开始,1为占位)
    ll dis00[2005][2005];  // 节点u→v的状态00最短距离:s00[u][v]是进入u的边,t00[u][v]是离开v的边
    int s00[2005][2005], t00[2005][2005];  // s00[u][v]:u→v路径的起始边,t00[u][v]:u→v路径的结束边
    ll dis01[2005][2005];  // 节点u→v的状态01最短距离
    int s01[2005][2005], t01[2005][2005];
    ll dis10[2005][2005];  // 节点u→v的状态10最短距离
    int s10[2005][2005], t10[2005][2005];
    ll dis11[2005][2005];  // 节点u→v的状态11最短距离
    int s11[2005][2005], t11[2005][2005];
    ll L[N][N];  // L[e1][e2]:从边e1的终点到边e2的起点的最短合法路径长度
    ll dis[N][2];  // 迪杰斯特拉中节点u的两种最短距离:dis[u][0]是最优,dis[u][1]是次优
    int las[N][2], vis[N][2];  // las[u][c]:到达u且状态为c的最后一条边;vis[u][c]:是否访问过
    struct edge {
        int to, id, w;  // to:邻接节点,id:边的编号,w:边的权值
    };
    vector<edge> v[N];  // 图的邻接表:v[u]存储u的所有邻接边
    struct node {
        ll dis;
        int pos, k1;  // pos:节点编号,k1:状态(0=最优,1=次优)
    };
    inline bool operator < (node x, node y) {  // 优先队列按距离从小到大排序(大根堆转小根堆)
        return x.dis > y.dis;
    }
    priority_queue<node> que;
    inline void chx(ll &x, ll y) {  // 更新x为x和y的最小值
        x = min(x, y);
    }
    int pos[M];  // 节点序列X:pos[i] = X_i
    struct qwq {  // 线段树节点存储的信息:4种状态的距离、起始边、结束边
        ll dis[2][2];
        int s[2][2], t[2][2];
    } tree[M * 4];  // 线段树,大小为4*M(M是序列长度上限1e5)
    
    // 检查两段路径的拼接是否合法:前一段的结束边t与后一段的起始边s是否满足“不原路返回”
    bool check(int x, int y) {
        return ((x ^ y) >= 2);  // 边x和y不是反向边(反向边编号相差1,如e和e^1)
    }
    
    // 合并两个线段树节点x(左段)和y(右段),得到拼接后的节点z
    qwq merge(qwq x, qwq y) {
        qwq z;
        // 初始化所有状态的距离为无穷大
        for (int i = 0; i < 2; i++)
            for (int j = 0; j < 2; j++)
                z.dis[i][j] = inf;
    
        // 合并状态00:左段任意状态 → 右段任意状态,取最短距离
        for (int p1 = 0; p1 < 2; p1++)
            for (int p2 = 0; p2 < 2; p2++)
                for (int p3 = 0; p3 < 2; p3++)
                    for (int p4 = 0; p4 < 2; p4++)
                        if (check(x.t[p1][p2], y.s[p3][p4]))  // 左段尾边与右段头边合法
                            if (z.dis[0][0] > x.dis[p1][p2] + y.dis[p3][p4]) {
                                z.dis[0][0] = x.dis[p1][p2] + y.dis[p3][p4];
                                z.s[0][0] = x.s[p1][p2];  // 拼接后路径的起始边=左段起始边
                                z.t[0][0] = y.t[p3][p4];  // 拼接后路径的结束边=右段结束边
                            }
    
        // 合并状态11:在00基础上,额外约束起始边和结束边的唯一性(确保路径不重复)
        for (int p1 = 0; p1 < 2; p1++)
            for (int p2 = 0; p2 < 2; p2++)
                for (int p3 = 0; p3 < 2; p3++)
                    for (int p4 = 0; p4 < 2; p4++)
                        if (check(x.t[p1][p2], y.s[p3][p4]))
                            if (check(z.s[0][0], x.s[p1][p2]))
                                if (check(z.t[0][0], y.t[p3][p4]))
                                    if (z.dis[1][1] > x.dis[p1][p2] + y.dis[p3][p4]) {
                                        z.dis[1][1] = x.dis[p1][p2] + y.dis[p3][p4];
                                        z.s[1][1] = x.s[p1][p2];
                                        z.t[1][1] = y.t[p3][p4];
                                    }
    
        // 合并状态10和01:混合约束起始边和结束边
        for (int p1 = 0; p1 < 2; p1++)
            for (int p2 = 0; p2 < 2; p2++)
                for (int p3 = 0; p3 < 2; p3++)
                    for (int p4 = 0; p4 < 2; p4++)
                        if (check(x.t[p1][p2], y.s[p3][p4])) {
                            // 状态10:起始边满足11约束,结束边满足00约束
                            if (check(z.s[0][0], x.s[p1][p2]))
                                if (check(z.t[1][1], y.t[p3][p4]))
                                    if (z.dis[1][0] > x.dis[p1][p2] + y.dis[p3][p4]) {
                                        z.dis[1][0] = x.dis[p1][p2] + y.dis[p3][p4];
                                        z.s[1][0] = x.s[p1][p2];
                                        z.t[1][0] = y.t[p3][p4];
                                    }
                            // 状态01:起始边满足00约束,结束边满足11约束
                            if (check(z.s[1][1], x.s[p1][p2]))
                                if (check(z.t[0][0], y.t[p3][p4]))
                                    if (z.dis[0][1] > x.dis[p1][p2] + y.dis[p3][p4]) {
                                        z.dis[0][1] = x.dis[p1][p2] + y.dis[p3][p4];
                                        z.s[0][1] = x.s[p1][p2];
                                        z.t[0][1] = y.t[p3][p4];
                                    }
                        }
    
        return z;
    }
    
    #define lc k<<1        // 左孩子节点编号
    #define rc k<<1|1      // 右孩子节点编号
    #define ls lc,l,mid    // 左子树:节点k,区间[l,mid]
    #define rs rc,mid+1,r  // 右子树:节点k,区间[mid+1,r]
    
    // 线段树pushup:合并左右孩子信息到父节点
    void pushup(int k) {
        tree[k] = merge(tree[lc], tree[rc]);
    }
    
    // 线段树单点更新:将第x个叶子节点(对应序列X_x→X_{x+1})更新为最新的4种状态
    void change(int k, int l, int r, int x) {
        if (l == r) {  // 叶子节点:对应X_l→X_{l+1}(即pos[l]→pos[l+1])
            tree[k].dis[0][0] = dis00[pos[l]][pos[l+1]];
            tree[k].s[0][0] = s00[pos[l]][pos[l+1]];
            tree[k].t[0][0] = t00[pos[l]][pos[l+1]];
            tree[k].dis[0][1] = dis01[pos[l]][pos[l+1]];
            tree[k].s[0][1] = s01[pos[l]][pos[l+1]];
            tree[k].t[0][1] = t01[pos[l]][pos[l+1]];
            tree[k].dis[1][0] = dis10[pos[l]][pos[l+1]];
            tree[k].s[1][0] = s10[pos[l]][pos[l+1]];
            tree[k].t[1][0] = t10[pos[l]][pos[l+1]];
            tree[k].dis[1][1] = dis11[pos[l]][pos[l+1]];
            tree[k].s[1][1] = s11[pos[l]][pos[l+1]];
            tree[k].t[1][1] = t11[pos[l]][pos[l+1]];
            return ;
        }
        int mid = l + r >> 1;
        if (x <= mid)
            change(ls, x);
        else
            change(rs, x);
        pushup(k);  // 更新父节点
    }
    
    // 处理动态查询:初始化序列并响应T次修改
    void work() {
        // 读入初始节点序列X
        for (ll i = 1; i <= len; i++)
            scanf("%d", &pos[i]);
        // 初始化线段树:每个叶子节点对应X_i→X_{i+1}(共len-1个)
        for (ll i = 1; i < len; i++)
            change(1, 1, len - 1, i);
        
        ll ans = inf;
        while (q--) {  // 处理T次修改查询
            ll x, y;
            scanf("%lld%lld", &x, &y);
            pos[x] = y;  // 修改X_x为y
            ans = inf;
            // 修改会影响X_{x-1}→X_x和X_x→X_{x+1}两段,需更新对应叶子节点
            if (x >= 2)
                change(1, 1, len - 1, x - 1);
            if (x < len)
                change(1, 1, len - 1, x);
            // 取4种状态中的最短距离作为答案
            for (int i = 0; i < 2; i++)
                for (int j = 0; j < 2; j++)
                    chx(ans, tree[1].dis[i][j]);
            // 输出结果:若距离为无穷大则输出-1,否则输出距离
            if (ans >= inf)
                puts("-1");
            else
                printf("%lld\n", ans);
        }
    }
    
    int main() {
        // 读入N(节点数)、M(边数)、T(修改次数)、len(序列长度L)
        scanf("%d%d%d%d", &n, &m, &q, &len);
        // 读入M条边,构建邻接表(无向边拆分为两条有向边,编号连续)
        for (int i = 1, x, y, z; i <= m; i++) {
            scanf("%d%d%d", &x, &y, &z);
            tot1++;  // 边编号从2开始(1为占位)
            p1[tot1] = x, p2[tot1] = y, val[tot1] = z;
            v[x].push_back(edge{y, tot1, z});  // x→y的边,编号tot1
            tot1++;
            p1[tot1] = y, p2[tot1] = x, val[tot1] = z;
            v[y].push_back(edge{x, tot1, z});  // y→x的边,编号tot1(与上一条反向)
        }
    
        //
    • 1

    信息

    ID
    3614
    时间
    2000ms
    内存
    512MiB
    难度
    10
    标签
    递交数
    10
    已通过
    1
    上传者