1 条题解
-
0
🧩 问题核心与约束分析
题目要求我们删除序列中的若干项,使得最长上升子序列(LIS)长度减少至少1,且满足:
- 代价最小:删除项的代价之和 最小。
- 字典序最小:当存在多种方案时,选择删去项的附加属性 排序后字典序最小的方案。
关键点:
- 需要求出原序列的LIS长度。
- 删除操作需要破坏所有的LIS。
- 需要在所有最小代价方案中,找出字典序最小的割边集。
💡 核心思路:网络流最小割建模
此问题的标准解法是使用网络流最小割模型。
🔹 第一步:计算LIS相关数组
首先,我们需要计算以每个位置 结尾的最长上升子序列长度 ,并记整个序列的LIS长度为 。
- :以第 个元素结尾的LIS长度。
- 找出所有满足 的位置,它们是LIS的终点。
🔹 第二步:构建网络流图
我们将每个点 拆成两个点:入点 和出点 ,并在它们之间连一条边,容量为删除该点的代价 。这条边被割掉就代表删除了这个点。
-
点拆分与内部边:
- 对于每个位置 ,建立边 。这是可能被割的边。
-
连接源点和汇点:
- 对于所有满足 的点 ,连接源点 到 ,容量为无穷大 (
inf)。 - 对于所有满足 的点 ,连接 到汇点 ,容量为无穷大。
- 对于所有满足 的点 ,连接源点 到 ,容量为无穷大 (
-
连接LIS相邻点:
- 对于满足 , 且 的点对,从 向 连一条容量为无穷大的边。这保证了只有破坏了点内部边,才能破坏LIS路径。
这样,图中的每一条从 到 的路径都对应原序列中的一个LIS。要破坏所有LIS,就需要找到一组边,使得删除后 和 不连通,且这些边的容量和最小——这就是最小割问题。
🔹 第三步:求最小割并输出方案
根据最大流最小割定理,我们可以通过求 到 的最大流来得到最小割的容量值。
难点在于,如何从所有可能的最小割中,找到删去项的 排序后字典序最小的方案。
- 按 排序:将所有的点按照 的值从小到大排序。
- 贪心判断:按 从小到大的顺序,依次判断每个点对应的边 是否可以在最小割中。
- 判断一条边 是否可以在最小割中,需要满足:
- 该边满流(即在最大流算法中流量已用尽)。
- 在残量网络中,不存在从 到 的路径(即该边是唯一连接两个强连通分量的边)。
- 判断一条边 是否可以在最小割中,需要满足:
- 强制选择与退流:一旦确定边 可以在最小割中,我们就强制选择它,然后需要消除这条边对后续计算的影响,即"退流":
- 将这条边及其反向边的容量设为0。
- 重新计算从 到 以及从 到 的流量,调整残量网络。
- 记录方案:将所有选择的点记录下来,最后按升序输出。
🧮 算法实现与细节
代码框架(仅供参考)
以下是基于和整理的主要逻辑框架:
#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; }⚠️ 注意事项
- 无穷大设置:根据 的范围(最大 )和点的数量(),
INF需要设置得足够大,例如 。 - 退流操作:上述代码中的退流操作是简化的,实际实现可能需要更精细的处理,例如分别从 到 和从 到 进行退流。
- 复杂度分析:由于需要多次求最大流和判断割边,整体复杂度较高,但考虑到 ,且数据组数 ,通常可以接受。
- 字典序保证:通过按 从小到大排序并贪心选择,可以保证最终方案字典序最小。
💎 总结
这道题的核心在于:
- 网络流建模:通过拆点将点权转化为边权,利用最小割破坏所有LIS路径。
- 最大流算法:使用Dinic等高效算法求解最小割容量。
- 贪心策略:按 排序后贪心选择割边,并通过退流操作维护网络,得到字典序最小的方案。
- 1
信息
- ID
- 5666
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 10
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者