动态规划:洛谷 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 }
但是提交发现:
错了两个数据点,我们用免费下载机会下载一下测试数据。
标准输入为:
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 }
但我们发现还是错的,所以我们修改一下,发现不能无脑的一直把前面的雷数加在下一个连接的地窖,会多加,必须每次比较一下是不是最优解,选择性的加,就有点像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 }
十分正确!提交答案!
但是到这里我发现,根本不需要去重了,我们有比较最优的解的话,就不需要,这个不是像食物链一样把所以连接的都加在被连接的结点里,选择最优的就行,有比较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 }
完美通过!
三、动态规划
一个小技巧,这边利用递归就是堆栈的本质,不用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 }
我们可以由这题,得出这类无向图的一个做题小模板:
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,需要继续练。还有一个类比就是,与食物链那题类比,食物链是可以只要指向,被指的就得加上,但是这题是要选一个最优的连接,有所区别。