P2196 [NOIP 1996 提高组] 挖地雷

P2196 [NOIP 1996 提高组] 挖地雷

题目描述

在一个地图上有 $N\ (N \le 20)$ 个地窖,每个地窖中埋有一定数量的地雷。同时,给出地窖之间的连接路径。当地窖及其连接的数据给出之后,某人可以从任一处开始挖地雷,然后每次可以移动到一个编号比当前节点大且联通的节点去挖地雷,当无满足条件的节点时挖地雷工作结束。设计一个挖地雷的方案,使某人能挖到最多的地雷。

输入格式

有若干行。

第 $1$ 行只有一个数字,表示地窖的个数 $N$。

第 $2$ 行有 $N$ 个数,分别表示每个地窖中的地雷个数。

第 $3$ 行至第 $N+1$ 行表示地窖之间的连接情况:

第 $3$ 行有 $n-1$ 个数($0$ 或 $1$),表示第一个地窖至第 $2$ 个、第 $3$ 个 $\dots$ 第 $n$ 个地窖有否路径连接。如第 $3$ 行为 $1\space 1\space 0\space 0\space 0\cdots 0$,则表示第 $1$ 个地窖至第 $2$ 个地窖有路径,至第 $3$ 个地窖有路径,至第 $4$ 个地窖、第 $5$ 个 $\dots$ 第 $n$ 个地窖没有路径。

第 $4$ 行有 $n-2$ 个数,表示第二个地窖至第 $3$ 个、第 $4$ 个 $\dots$ 第 $n$ 个地窖有否路径连接。

……

第 $n+1$ 行有 $1$ 个数,表示第 $n-1$ 个地窖至第 $n$ 个地窖有否路径连接。(为 $0$ 表示没有路径,为 $1$ 表示有路径)。

输出格式

第一行表示挖得最多地雷时的挖地雷的顺序,各地窖序号间以一个空格分隔,不得有多余的空格。

第二行只有一个数,表示能挖到的最多地雷数。

输入输出样例 #1

输入 #1

5
10 8 4 7 6
1 1 1 0
0 0 0
1 1
1

输出 #1

1 3 4 5
27

说明/提示

【样例解释】 最优路径为 $1 \to 3 \to 4 \to 5$,结果为 $27$。

【题目来源】 NOIP 1996 提高组第三题。

思路分析 + 代码实现

地窖之间依据编号(从小到大)构成一个 DAG ,相当于一个在有向无环图(DAG)上求最长路径的问题,每个节点有正权值(地雷数)。所以一开始想到的是拓扑排序,用 BFS 解决,代码如下:

#include <bits/stdc++.h>
using namespace std;
constexpr int MAXN = 25;
vector<int> G[MAXN];
int a[MAXN];
struct State{
    int u, cnt;
    vector<int> rec;
};
int main(){
    int N, ans = 0;
    cin >> N;
    for(int i = 1; i <= N; i++)
        cin >> a[i];
    for(int t, i = 1; i < N; i++)
        for(int j = i + 1; j <= N; j++) {
            cin >> t;
            if (t) G[i].push_back(j);
        }
    vector<int> ans_rec;
    queue<State> Q;
    for(int i = 1; i <= N; i++) {
        State t = {i, a[i]};
        t.rec.push_back(i);
        Q.push(t);
    }
    while (!Q.empty()) {
        State now = Q.front();
        Q.pop();
        if (now.cnt > ans) {
            ans = now.cnt;
            ans_rec.clear();
            ans_rec = now.rec;
        }
        for(int v : G[now.u]) {
            State t = now;
            t.u = v;
            t.rec.push_back(v);
            t.cnt += a[v];
            Q.push(t);
        }
    }
    for(int i : ans_rec)
        cout << i << " ";
    cout << "\n" << ans;
    return 0;
}

当然也可以用记忆化搜索,时间复杂度 $O(n^2)$ ,代码如下:

#include <bits/stdc++.h>
using namespace std;
constexpr int MAXN = 25;
bool connected[MAXN][MAXN];
int a[MAXN], dp[MAXN], N;
int dfs(int x) {
    if (dp[x]) return dp[x];
    int t = 0;
    for(int i = x + 1; i <= N; i++)
        if (connected[x][i])
            t = max(t, dfs(i));
    return dp[x] = a[x] + t;
}
int main() {
    cin >> N;
    for(int i = 1; i <= N; i++)
        cin >> a[i];
    for(int i = 1; i < N; i++)
        for(int j = i + 1; j <= N; j++)
            cin >> connected[i][j];
    int ans = 0;
    for(int i = 1; i <= N; i++)
        ans = max(ans, dfs(i));
    for(int i = 1; i <= N; i++)
        if (dp[i] == ans) {
            cout << i << " ";
            for(int j = i + 1; j <= N; j++)
                if (connected[i][j] && dp[i] == a[i] + dp[j])
                    cout << (i = j) << " "; // 输出与赋值合并
        }
    cout << "\n" << ans;
    return 0;
}

注意根据答案推路径的细节。

当然, DP 更加简洁,设 $dp[i]$ 表示以地窖 $i$ 为起点能挖到的最大地雷数。

对于每个地窖 $i$,考虑它所有能到达的后续节点 $j$(满足 $j > i$ 且 $connected[i][j] = 1$)则状态转移方程为:

$$ dp[i] = a[i] + \max_{j>i && connected[i][j]} dp[j] $$

DP 部分代码如下(其他与 DFS 做法相同):

for(int i = N; i >= 1; i--) {
    dp[i] = a[i];
    for(int j = N; j > i; j--)
        if (connected[i][j])
            dp[i] = max(dp[i], a[i] + dp[j]);
    ans = max(ans, dp[i]);
}
Licensed under CC BY-NC-SA 4.0
使用 Hugo 构建
主题 StackJimmy 设计