1 条题解

  • 0
    @ 2025-11-27 15:48:41

    🧩 问题核心与约束分析

    题目要求我们删除序列中的若干项,使得最长上升子序列(LIS)长度减少至少1,且满足:

    1. 代价最小:删除项的代价之和 BiB_i 最小。
    2. 字典序最小:当存在多种方案时,选择删去项的附加属性 CiC_i 排序后字典序最小的方案。

    关键点

    • 需要求出原序列的LIS长度。
    • 删除操作需要破坏所有的LIS
    • 需要在所有最小代价方案中,找出字典序最小的割边集。

    💡 核心思路:网络流最小割建模

    此问题的标准解法是使用网络流最小割模型

    🔹 第一步:计算LIS相关数组

    首先,我们需要计算以每个位置 ii 结尾的最长上升子序列长度 dp[i]dp[i],并记整个序列的LIS长度为 lenlen

    • dp[i]dp[i]:以第 ii 个元素结尾的LIS长度。
    • 找出所有满足 dp[i]=lendp[i] = len 的位置,它们是LIS的终点。

    🔹 第二步:构建网络流图

    我们将每个点 ii 拆成两个点:入点 ii 和出点 ii',并在它们之间连一条边,容量为删除该点的代价 BiB_i。这条边被割掉就代表删除了这个点。

    1. 点拆分与内部边

      • 对于每个位置 ii,建立边 (i,i,Bi)(i, i', B_i)。这是可能被割的边。
    2. 连接源点和汇点

      • 对于所有满足 dp[i]=1dp[i] = 1 的点 ii,连接源点 SSii,容量为无穷大 (inf)。
      • 对于所有满足 dp[i]=lendp[i] = len 的点 ii,连接 ii' 到汇点 TT,容量为无穷大。
    3. 连接LIS相邻点

      • 对于满足 i<ji < j, Ai<AjA_i < A_jdp[j]=dp[i]+1dp[j] = dp[i] + 1 的点对,从 ii'jj 连一条容量为无穷大的边。这保证了只有破坏了点内部边,才能破坏LIS路径。

    这样,图中的每一条从 SSTT 的路径都对应原序列中的一个LIS。要破坏所有LIS,就需要找到一组边,使得删除后 SSTT 不连通,且这些边的容量和最小——这就是最小割问题。

    🔹 第三步:求最小割并输出方案

    根据最大流最小割定理,我们可以通过求 SSTT最大流来得到最小割的容量值。

    难点在于,如何从所有可能的最小割中,找到删去项的 CiC_i 排序后字典序最小的方案。

    1. CiC_i 排序:将所有的点按照 CiC_i 的值从小到大排序。
    2. 贪心判断:按 CiC_i 从小到大的顺序,依次判断每个点对应的边 (i,i)(i, i') 是否可以在最小割中。
      • 判断一条边 (i,i)(i, i') 是否可以在最小割中,需要满足:
        • 该边满流(即在最大流算法中流量已用尽)。
        • 残量网络中,不存在从 iiii' 的路径(即该边是唯一连接两个强连通分量的边)。
    3. 强制选择与退流:一旦确定边 (i,i)(i, i') 可以在最小割中,我们就强制选择它,然后需要消除这条边对后续计算的影响,即"退流":
      • 将这条边及其反向边的容量设为0。
      • 重新计算从 SSii 以及从 ii'TT 的流量,调整残量网络。
    4. 记录方案:将所有选择的点记录下来,最后按升序输出。

    🧮 算法实现与细节

    代码框架(仅供参考)

    以下是基于和整理的主要逻辑框架:

    #include <bits/stdc++.h>
    using namespace std;
    typedef long long LL;
    const LL INF = 1e18;
    const int MAXN = 2005; // 注意点数,因为拆点,实际点数为 2*n+2
    
    struct Edge {
        int to;
        LL cap;
        int rev;
    };
    
    vector<Edge> graph[MAXN];
    int level[MAXN], iter[MAXN];
    int n, S, T;
    int A[MAXN], B[MAXN], C[MAXN], dp[MAXN];
    vector<int> minCutSet;
    
    // 添加边
    void addEdge(int from, int to, LL cap) {
        graph[from].push_back((Edge){to, cap, (int)graph[to].size()});
        graph[to].push_back((Edge){from, 0, (int)graph[from].size()-1});
    }
    
    // BFS计算层次图
    bool bfs() {
        memset(level, -1, sizeof(level));
        queue<int> q;
        level[S] = 0;
        q.push(S);
        while (!q.empty()) {
            int u = q.front(); q.pop();
            for (auto &e : graph[u]) {
                if (e.cap > 0 && level[e.to] < 0) {
                    level[e.to] = level[u] + 1;
                    q.push(e.to);
                }
            }
        }
        return level[T] >= 0;
    }
    
    // DFS寻找增广路
    LL dfs(int u, int t, LL f) {
        if (u == t) return f;
        for (int &i = iter[u]; i < graph[u].size(); i++) {
            Edge &e = graph[u][i];
            if (e.cap > 0 && level[u] < level[e.to]) {
                LL d = dfs(e.to, t, min(f, e.cap));
                if (d > 0) {
                    e.cap -= d;
                    graph[e.to][e.rev].cap += d;
                    return d;
                }
            }
        }
        return 0;
    }
    
    // Dinic算法求最大流
    LL maxFlow() {
        LL flow = 0;
        while (bfs()) {
            memset(iter, 0, sizeof(iter));
            LL f;
            while ((f = dfs(S, T, INF)) > 0) {
                flow += f;
            }
        }
        return flow;
    }
    
    // 判断边(u, u')是否可以在最小割中
    bool canBeInMinCut(int u) {
        // 在残量网络中,如果从u不能到达u',则这条边可以在最小割中
        // 可以通过BFS判断连通性
        vector<bool> visited(2*n+3, false);
        queue<int> q;
        q.push(u);
        visited[u] = true;
        while (!q.empty()) {
            int cur = q.front(); q.pop();
            for (auto &e : graph[cur]) {
                if (e.cap > 0 && !visited[e.to]) {
                    if (e.to == u + n) return false; // 存在路径,不能成为割边
                    visited[e.to] = true;
                    q.push(e.to);
                }
            }
        }
        return true;
    }
    
    int main() {
        int testCase;
        cin >> testCase;
        while (testCase--) {
            cin >> n;
            for (int i = 1; i <= n; i++) cin >> A[i];
            for (int i = 1; i <= n; i++) cin >> B[i];
            for (int i = 1; i <= n; i++) cin >> C[i];
            
            // 计算dp数组
            int len = 0;
            for (int i = 1; i <= n; i++) {
                dp[i] = 1;
                for (int j = 1; j < i; j++) {
                    if (A[j] < A[i]) {
                        dp[i] = max(dp[i], dp[j] + 1);
                    }
                }
                len = max(len, dp[i]);
            }
            
            // 建图
            S = 0, T = 2*n + 1;
            for (int i = 0; i <= T; i++) graph[i].clear();
            
            // 拆点连边
            for (int i = 1; i <= n; i++) {
                addEdge(i, i+n, B[i]);
            }
            
            // 连接源点和汇点
            for (int i = 1; i <= n; i++) {
                if (dp[i] == 1) addEdge(S, i, INF);
                if (dp[i] == len) addEdge(i+n, T, INF);
            }
            
            // 连接LIS相邻点
            for (int i = 1; i <= n; i++) {
                for (int j = i+1; j <= n; j++) {
                    if (A[i] < A[j] && dp[j] == dp[i] + 1) {
                        addEdge(i+n, j, INF);
                    }
                }
            }
            
            // 求最大流(最小割容量)
            LL minCost = maxFlow();
            
            // 按C_i排序,求字典序最小的方案
            vector<pair<int, int>> nodes; // (C_i, i)
            for (int i = 1; i <= n; i++) {
                nodes.push_back({C[i], i});
            }
            sort(nodes.begin(), nodes.end());
            
            minCutSet.clear();
            for (auto &node : nodes) {
                int u = node.second;
                if (canBeInMinCut(u)) {
                    minCutSet.push_back(u);
                    // 强制选择这条边:将其容量设为0
                    for (auto &e : graph[u]) {
                        if (e.to == u+n) {
                            e.cap = 0;
                            graph[e.to][e.rev].cap = 0;
                            break;
                        }
                    }
                    // 退流操作:需要重新计算最大流
                    maxFlow(); // 这里可能需要更精细的退流操作
                }
            }
            
            // 输出结果
            sort(minCutSet.begin(), minCutSet.end());
            cout << minCost << " " << minCutSet.size() << endl;
            for (int i = 0; i < minCutSet.size(); i++) {
                if (i > 0) cout << " ";
                cout << minCutSet[i];
            }
            cout << endl;
        }
        return 0;
    }
    

    ⚠️ 注意事项

    1. 无穷大设置:根据 BiB_i 的范围(最大 10910^9)和点的数量(n700n \leq 700),INF 需要设置得足够大,例如 101810^{18}
    2. 退流操作:上述代码中的退流操作是简化的,实际实现可能需要更精细的处理,例如分别从 uuSS 和从 TTuu' 进行退流。
    3. 复杂度分析:由于需要多次求最大流和判断割边,整体复杂度较高,但考虑到 n700n \leq 700,且数据组数 T5T \leq 5,通常可以接受。
    4. 字典序保证:通过按 CiC_i 从小到大排序并贪心选择,可以保证最终方案字典序最小。

    💎 总结

    这道题的核心在于:

    1. 网络流建模:通过拆点将点权转化为边权,利用最小割破坏所有LIS路径。
    2. 最大流算法:使用Dinic等高效算法求解最小割容量。
    3. 贪心策略:按 CiC_i 排序后贪心选择割边,并通过退流操作维护网络,得到字典序最小的方案。
    • 1

    信息

    ID
    5666
    时间
    1000ms
    内存
    256MiB
    难度
    10
    标签
    递交数
    1
    已通过
    1
    上传者