动态规划:洛谷 P2196 [NOIP1996 提高组] 挖地雷【三种题解 :DFS 动态规划 拓扑排序】

洛谷 P2196 [NOIP1996 提高组] 挖地雷
本文章包括本题的三种解法:DFS 动态规划 拓扑排序

 

 

一、拓扑排序

 这是一题普及/提高-的动态规划的题目,分析一下应该是图上动态规划。我思考了一下,因为还不是很会动态规划,图上动态规划,就想起我昨天做的那题P4017 最大食物链计数,这个图上动态规划,于是我就想到用那个拓扑排序的模板。但我画图发现,那题是有向图,这题是无向图,所以可能会出现环的情况,所以我们只能自己假设方向,从数字小的指向数字大的,自己做一个有向图,最后找最大的ans就行了。并且利用到技巧,pre数组存储前缀路径,用最后一次的地窖递归pre数组寻找路径,存入stack中,输出路径。

 

尝试①:就直接利用昨天 P4017 最大食物链计数 得出的拓扑排序进行求解。

 1 //洛谷 P2196 [NOIP1996 提高组] 挖地雷
 2 #include<iostream>
 3 #include<algorithm>
 4 #include<cmath>
 5 #include<vector>
 6 #include<queue>
 7 #include<stack>
 8 using namespace std;
 9 int mine[25];
10 bool vis[21][21];//二维 存放地雷的邻接关系
11 int in[25];//记录入度
12 queue<int>q;
13 stack<int>road;//记录路径
14 int pre[25];
15 int num[25];
16 void finds(int x)
17 {
18     if (pre[x])
19     {
20         road.push(pre[x]);
21         finds(pre[x]);
22     }
23     
24 }
25 int main()
26 {
27     int n;
28     cin >> n;
29     for (int i = 1; i <= n; ++i)
30         cin >> mine[i];
31     int linjie;
32     for (int i = 1; i <= n - 1; ++i)//输入邻接关系
33     {
34         for (int j = i + 1; j <= n; j++)
35         {
36             cin >> linjie;
37             if (linjie)
38             {
39                 vis[i][j] = 1;
40                 in[j]++;//入度加加
41             }
42         }
43     }
44 
45     for (int i = 1; i <= n; ++i)
46     {
47         if (in[i] == 0)
48             q.push(i);//寻找入度为0的点 入队,注意 并不是只会有一个点入度为0
49     }
50     int temp;
51     while (!q.empty())
52     {
53         temp = q.front();
54         q.pop();
55         for (int i = 1; i <= n; ++i)//遍历每一个邻接关系
56         {
57             if (vis[temp][i])
58             {
59                 pre[i] = temp;
60                 mine[i] += mine[temp];//相连接的都可以加上地雷数目
61                 for (int k = 1; k <= n; ++k)
62                 {
63                     if (vis[k][temp] && vis[k][i])
64                         mine[i] -= mine[k];
65                 }
66                 //判断一下会不会重复添加 比如1-3 1-4 3-4 会重复添加两次1
67                 in[i]--;//入度减1
68                 if (in[i] == 0)
69                     q.push(i);//入队
70             }
71 
72         }
73     }
74     int ans = -1;
75     int flag;
76     for (int i = 1; i <= n; ++i)
77     {
78         if (ans < mine[i])
79         {
80             ans = mine[i];
81             flag = i;
82         }
83     }
84     road.push(flag);
85     finds(flag);
86     while (road.size() != 1)
87     {
88         cout << road.top() << " ";
89         road.pop();
90     }
91     cout << road.top() << endl;
92     cout << ans;
93 }
拓扑排序Code one

 但是提交发现:

 

 

 

错了两个数据点,我们用免费下载机会下载一下测试数据。

标准输入为:

6
5 10 20 5 4 5
1 0 1 0 0
0 1 0 0
1 0 0
1 1
1

标准输出为:

3 4 5 6
34

在编译器中测试得

 

 

 

 

 

 我们一步步DEBUG可以发现,2的地雷数在我们队列计算的时候加上了1 而4也是加上了1的地雷数,所以会重复加上一块,需要加一个判断语句,判断一下比如2->4的时候4要加上2的雷数,此时看4和2有没有被同一个地雷所指向,有的话就删除一个重复的雷数,只要看vis[i][4]和vis[i][2]是不是都等1.

我们只要加上:

1 for (int k = 1; k <= n; ++k)
2  {
3      if (vis[k][temp] && vis[k][i])
4           tt -= mine[k];
5  //判断一下会不会重复添加 比如1-3 1-4 3-4 会重复添加两次1
6  }
拓扑排序Code two

 

 但我们发现还是错的,所以我们修改一下,发现不能无脑的一直把前面的雷数加在下一个连接的地窖,会多加,必须每次比较一下是不是最优解,选择性的加,就有点像DP里面的循环中的Max一样,每一次选择一个最优的,所以我就修改为

 1  for (int i = 1; i <= n; ++i)//遍历每一个邻接关系
 2         {
 3             if (vis[temp][i])
 4             {
 5                 int tt=mine[i];
 6                 tt+= mine[temp];
 7                 for (int k = 1; k <= n; ++k)
 8                 {
 9                     if (vis[k][temp] && vis[k][i])
10                         tt -= mine[k];
11                     //判断一下会不会重复添加 比如1-3 1-4 3-4 会重复添加两次1
12                 }
13                 if (tt > best[i])//如果这个解更优 更新一下
14                 {
15                     pre[i] = temp;
16                     best[i] = tt;
17                 }

加一个best数组,也就是DP数组,并且每次遍历a连接的所有地窖的时候,要compare一下是不是更好地,如果更好就更新一下,但测试后发现,还有问题。

 

 还是不对,5去哪里了?

我继续DEBUG,发现原来我没有写动态规划的重点,

 

 这边tt加的应该是最优解best数组,而不应该是mine雷数数组,因为best数组也是dp数组,dp要递推出最优解,所以求的这个最优dp【i】一定是连接的地窖的最优dp【temp】加上本身的mine【i】;

于是修改后的代码:

  1 //洛谷 P2196 [NOIP1996 提高组] 挖地雷
  2 #include<iostream>
  3 #include<algorithm>
  4 #include<cmath>
  5 #include<vector>
  6 #include<queue>
  7 #include<stack>
  8 using namespace std;
  9 int mine[25];
 10 int best[25];
 11 bool vis[21][21];//二维 存放地雷的邻接关系
 12 int in[25];//记录入度
 13 queue<int>q;
 14 stack<int>road;//记录路径
 15 int pre[25];
 16 int num[25];
 17 void finds(int x)
 18 {
 19     if (pre[x])
 20     {
 21         road.push(pre[x]);
 22         finds(pre[x]);
 23     }
 24 
 25 }
 26 int main()
 27 {
 28     int n;
 29     cin >> n;
 30     for (int i = 1; i <= n; ++i)
 31     {
 32         cin >> mine[i];
 33         best[i] = mine[i];
 34     }
 35 
 36     int linjie;
 37     for (int i = 1; i <= n - 1; ++i)//输入邻接关系
 38     {
 39         for (int j = i + 1; j <= n; j++)
 40         {
 41             cin >> linjie;
 42             if (linjie)
 43             {
 44                 vis[i][j] = 1;
 45                 in[j]++;//入度加加
 46             }
 47         }
 48     }
 49 
 50     for (int i = 1; i <= n; ++i)
 51     {
 52         if (in[i] == 0)
 53             q.push(i);//寻找入度为0的点 入队,注意 并不是只会有一个点入度为0
 54     }
 55     int temp;
 56     while (!q.empty())
 57     {
 58         temp = q.front();
 59         q.pop();
 60         for (int i = temp + 1; i <= n; ++i)//遍历每一个邻接关系 i从temp+1开始 节约时间
 61         {
 62             if (vis[temp][i])
 63             {
 64                 int tt = mine[i];
 65                 tt += best[temp];//相连接的都可以加上最佳地雷数目 我在这犯过一次错 加上的是地雷数!
 66                 for (int k = 1; k <= n; ++k)
 67                 {
 68                     if (vis[k][temp] && vis[k][i])
 69                         tt -= mine[k];
 70                     //判断一下会不会重复添加 比如1-3 1-4 3-4 会重复添加两次1
 71                 }
 72                 if (tt > best[i])//如果这个解更优 更新一下
 73                 {
 74                     pre[i] = temp;
 75                     best[i] = tt;
 76                 }
 77 
 78                 in[i]--;//入度减1
 79                 if (in[i] == 0)
 80                     q.push(i);//入队
 81             }
 82 
 83         }
 84     }
 85     int ans = -1;
 86     int flag;
 87     for (int i = 1; i <= n; ++i)
 88     {
 89         if (ans < best[i])
 90         {
 91             ans = best[i];
 92             flag = i;
 93         }
 94     }
 95     road.push(flag);
 96     finds(flag);
 97     while (road.size() != 1)
 98     {
 99         cout << road.top() << " ";
100         road.pop();
101     }
102     cout << road.top() << endl;
103     cout << ans;
104 }
拓扑排序Code three

 

 十分正确!提交答案!

 但是到这里我发现,根本不需要去重了,我们有比较最优的解的话,就不需要,这个不是像食物链一样把所以连接的都加在被连接的结点里,选择最优的就行,有比较best就不需要去重了。这题和食物链最本质的区别,有compare。

 二、DFS

    深度优先搜索解法,因为这题数据量并不大,最多才20个地窖,完全可以暴搜,也不需要剪枝。并且利用到技巧,pre数组存储前缀路径,用最后一次的地窖递归pre数组寻找路径,存入stack中,输出路径。

 1 //洛谷 P2196 [NOIP1996 提高组] 挖地雷
 2 #include<iostream>
 3 #include<algorithm>
 4 #include<cmath>
 5 #include<vector>
 6 using namespace std;
 7 int mine[25];
 8 int vis[22][22];
 9 int path[22], ans[22];//path记录路径 ans记录答案路径
10 int Max;
11 int n;
12 int flag;//记录答案挖了几个地窖 方便遍历输出
13 bool v[22];//记录点是否被访问过
14 bool check(int x)
15 {
16     for (int i = 1; i <= n; ++i)
17     {
18         if (vis[x][i] && !v[i])
19             return true;
20     }
21     return false;
22 }
23 void dfs(int num, int floor, int sum)
24 {
25     //边界条件 检查一下是否还能挖 不行就比较一下sum和Max 
26     //到最后一层再复制ans数组 可以节约时间复杂度,每一层都比ans大 
27     //复制一下时间耗费过多
28     if (!check(num))
29     {
30         if (sum > Max)
31         {
32             for (int i = 0; i <= floor; ++i)
33                 ans[i] = path[i];
34             Max = sum;
35             flag = floor;
36         }
37         return;
38     }
39     for (int i = 1; i <= n; ++i)
40     {
41         if (vis[num][i] && !v[i])
42         {
43             path[floor + 1] = i;
44             v[i] = 1;
45             dfs(i, floor + 1, sum + mine[i]);//递归
46             v[i] = 0;//回溯
47 
48         }
49     }
50     return;
51 
52 }
53 int main()
54 {
55     cin >> n;
56     for (int i = 1; i <= n; ++i)
57         cin >> mine[i];
58     for (int i = 1; i <= n - 1; ++i)
59         for (int j = i + 1; j <= n; ++j)
60         {
61             cin >> vis[i][j];
62         }
63     for (int i = 1; i <= n; ++i)
64     {
65         v[i] = 1;
66         path[0] = i;
67         dfs(i, 0, mine[i]);
68         v[i] = 0;//这边也要回溯!
69     }
70     for (int i = 0; i < flag; ++i)
71     {
72         cout << ans[i] << " ";
73     }
74     cout << ans[flag] << endl;
75     cout << Max;
76     return 0;
77 
78 }
DFS CODE

 

 

 完美通过!

三、动态规划

一个小技巧,这边利用递归就是堆栈的本质,不用stack,递归直接输出路径。

 1 //洛谷 P2196 [NOIP1996 提高组] 挖地雷
 2 #include <iostream>
 3 #include <cstdio>
 4 #include <cstring>
 5 using namespace std;
 6 int dp[25];//dp数组
 7 int mine[25];//存储地雷数
 8 int vis[25][25];//存储邻接关系
 9 int pre[25];//存储答案的前驱元素 以便递归输出ans
10 int Max;
11 int cnt;//cnt记录最佳答案的最后一个路径
12 int n;
13 void finds(int x)//因为递归本身的性质就是堆栈 所以不需要用stack 递归即可
14 {
15     if (!pre[x])
16     {
17         //说明是起点
18         cout << x;
19         return;
20     }
21     finds(pre[x]);
22     cout << " " << x;
23 }
24 int main()
25 {
26 
27     cin >> n;
28     for (int i = 1; i <= n; ++i)
29         cin >> mine[i];
30     for (int i = 1; i <= n; ++i)
31         for (int j = i + 1; j <= n; ++j)
32             cin >> vis[i][j];
33     for (int i = 1; i <= n; ++i)//外层循环遍历每一个地窖
34     {//就是循环遍历以i为终点的每一种可能
35         for (int j = 1; j <= n; ++j)//内层循环遍历 看是否有地窖联通外层循环的地窖
36         {
37             if (vis[j][i] && dp[j] > dp[i])
38             {
39 
40                 dp[i] = dp[j];
41                 pre[i] = j;//更新前驱数组
42 
43             }
44 
45         }
46         //一定要在后面加上自己的雷数!不会影响上一步的比较
47         dp[i] += mine[i];//先动态规划,最后再加上自身的地雷数
48         if (dp[i] > Max)
49         {
50             Max = dp[i];
51             cnt = i;
52         }
53     }
54     finds(cnt);
55     cout << endl;
56     cout << Max;
57     return 0;
58 }
DP Code

 

 我们可以由这题,得出这类无向图的一个做题小模板:

 1     for (int i = 1; i <= n; ++i)//外层循环遍历每一个地窖
 2     {//就是循环遍历以i为终点的每一种可能
 3         for (int j = 1; j <= n; ++j)//内层循环遍历 看是否有地窖联通外层循环的地窖
 4         {
 5             if (vis[j][i] && dp[j] > dp[i])
 6             {
 7 
 8                 dp[i] = dp[j];
 9                 pre[i] = j;//更新前驱数组
10 
11             }
12 
13         }
14         //一定要在后面加上自己的雷数!不会影响上一步的比较
15         dp[i] += mine[i];//先动态规划,最后再加上自身的地雷数
16     }

 

四、分析三者的时间复杂度与空间复杂度

上图顺序分别是拓扑,DFS,动规

 

 

 

 

 

 可以发现时间复杂度和空间复杂度都相差无几,所以数据小哪种方法都可以,但是我还是觉得DP写得代码量最小,只是这题我没用DP想出来。。还是不擅长DP,需要继续练。还有一个类比就是,与食物链那题类比,食物链是可以只要指向,被指的就得加上,但是这题是要选一个最优的连接,有所区别。

posted @ 2022-04-03 11:04  朱朱成  阅读(311)  评论(1编辑  收藏  举报