diff --git a/TangDou/AcWing/DisjointSet/1250.cpp b/TangDou/AcWing/DisjointSet/1250.cpp deleted file mode 100644 index 3f0bb7f..0000000 --- a/TangDou/AcWing/DisjointSet/1250.cpp +++ /dev/null @@ -1,50 +0,0 @@ -#include -using namespace std; - -const int N = 200 * 200 + 10; - -int n, m; -int p[N]; - -//二维转一维的办法,坐标从(1,1)开始 -inline int get(int x, int y) { - return (x - 1) * n + y; -} - -//最简并查集 -int find(int x) { - if (p[x] != x) p[x] = find(p[x]); //路径压缩 - return p[x]; -} -int main() { - cin >> n >> m; - for (int i = 1; i <= n * n; i++) p[i] = i; - - int res = 0; - for (int i = 1; i <= m; i++) { - int x, y; - char d; - cin >> x >> y >> d; - int a = get(x, y); //计算a点点号 - int b; - if (d == 'D') //向下走 - b = get(x + 1, y); - else //向右走 - b = get(x, y + 1); - - // a,b需要两次相遇,才是出现了环~ - int pa = find(a), pb = find(b); - if (pa == pb) { - res = i; //记录操作步数 - break; - } - //合并并查集 - p[pa] = pb; - } - - if (!res) //没有修改过这个值 - puts("draw"); //平局 - else //输出操作步数 - printf("%d\n", res); - return 0; -} \ No newline at end of file diff --git a/TangDou/AcWing/Floyd/343.md b/TangDou/AcWing/Floyd/343.md deleted file mode 100644 index 7305f97..0000000 --- a/TangDou/AcWing/Floyd/343.md +++ /dev/null @@ -1,370 +0,0 @@ -## [$AcWing$ $343$. 排序](https://www.acwing.com/problem/content/345/) - -### 一、题目描述 -给定 $n$ 个变量和 $m$ 个不等式。其中 $n$ 小于等于 $26$,变量分别用前 $n$ 的大写英文字母表示。 - -不等式之间具有传递性,即若 $A>B$ 且 $B>C$,则 $A>C$。 - -请从前往后遍历每对关系,每次遍历时判断: - -* 如果能够确定全部关系且无矛盾,则结束循环,输出确定的次序; -* 如果发生矛盾,则结束循环,输出有矛盾; -* 如果循环结束时没有发生上述两种情况,则输出无定解。 - -**输入格式** -输入包含多组测试数据。 - -每组测试数据,第一行包含两个整数 $n$ 和 $m$。 - -接下来 $m$ 行,每行包含一个不等式,不等式全部为 **小于** 关系。 - -当输入一行 `0 0` 时,表示输入终止。 - -**输出格式** -每组数据输出一个占一行的结果。 - -结果可能为下列三种之一: - -* 如果可以确定两两之间的关系,则输出 `Sorted sequence determined after t relations: yyy...y.`,其中`t`指 **迭代次数**,`yyy...y`是指 **升序排列** 的所有变量。 - -* 如果有矛盾,则输出: `Inconsistency found after t relations.`,其中`t`指迭代次数。 - -* 如果没有矛盾,且不能确定两两之间的关系,则输出 `Sorted sequence cannot be determined.`。 - -**数据范围** -$2≤n≤26$,变量只可能为大写字母 $A$∼$Z$。 - -**输入样例$1$**: -```cpp {.line-numbers} -4 6 -A**解释**:比如$a < b,b < c$,就可以推导出$a < c$,如果用图形表示出这种大小关系,就是$a$到$b$有一条有向边,$b$到$c$有一条有向边,可以推出$a$可以到达$c$,找出图中各点能够到达点的集合,就 **类似** 于$floyd$算法求图中任意两点间的最短距离。 - -**模板** -```cpp {.line-numbers} -//传递闭包 -void floyd(){ - for(int k = 0;k < n;k++) - for(int i = 0;i < n;i++) - for(int j = 0;j < n;j++) - f[i][j] |= f[i][k] & f[k][j]; -} -// 原始版本 -/* -for (int k = 0; k < n; k++) - for (int i = 0; i < n; i++) - for (int j = 0; j < n; j++) - d[i][j] = min(d[i][j], d[i][k] + d[k][j]); -*/ -``` - -**回到本题** - -- 题目描述要求按顺序遍历二元关系,一旦前$i$个二元关系可以确定次序了就不再遍历了,即使第$i + 1$对二元关系就会出现矛盾也不去管它了。 -- 题目字母只会在$A$到$Z$间,因此可以映射为$0$到$25$这$26$个元素 -- $A < B$,则$f[0][1]=1$。如果$f[0][1] = f[1][0] = 1$,推出$f[0][0] = 1$,此时$A < B$并且$B < A$发生矛盾,即$f[i][i]= 1$时表示发生矛盾。 - -**算法步骤** -每读取一对二元关系,就执行一遍$floyd$算法求 **传递闭包**,然后执行$check$函数判断: -* ① 如果发生矛盾终止遍历 -* ② 如果次序全部被确定终止遍历 -* ③ 两者都没有,继续遍历 - -在确定所有的次序后,需要 **输出大小关系**,需要一个$getorder$函数。 - ->**注意**: -终止遍历仅仅是不再针对新增的二元关系去求传递闭包,循环还是要继续的,需要读完数据才能继续读下一组数据。 - -下面设计$check$函数和$getorder$函数。 -```cpp {.line-numbers} -// 1:可以确定两两之间的关系,2:矛盾,3:不能确定两两之间的关系 -int check() { - // 如果i **解释**:确定所有元素次序后如何判断元素`i`在第几个位置呢?`f[i][j] = 1`表示`i < j`,因此计算下`i`小于元素的个数`cnt`,就可以判定`i`是第`cnt + 1`大的元素了 - - -#### $Code$ -```cpp {.line-numbers} -#include -// Floyd解决传送闭包问题 -using namespace std; -const int N = 27; -int n; // n个变量 -int m; // m个不等式 -int f[N][N]; // 传递闭包结果 - -void floyd() { - for (int k = 0; k < n; k++) - for (int i = 0; i < n; i++) - for (int j = 0; j < n; j++) - f[i][j] |= f[i][k] & f[k][j]; // i可以到达k,k可以到达j,那么i可以到达j -} -// 1:可以确定两两之间的关系,2:矛盾,3:不能确定两两之间的关系 -int check() { - // 如果i> S; - // 已确定或者出现了矛盾,就没有必要再处理了,但是,还需要耐心的读取完毕,因为可能还有下一轮,不读入完耽误下一轮 - if (k < 3) continue; - // 变量只可能为大写字母A~Z,映射到0~25 - int a = S[0] - 'A', b = S[2] - 'A'; - f[a][b] = 1; // 记录a -using namespace std; - -const int N = 30, M = N * N; - -int n, m; -int a[1050], b[1050]; // a[i] q; - bool flag = 0; - - for (int i = 0; i < n; i++) // 枚举每个节点,入度为零的入队列 - if (in[i] == 0) q.push(i); - - while (q.size()) { - /* - 注意:此处需要优先检查是不是有环,即使检查到某个点有多个前序节点,也并不表示它应该返回2,因为此时也可能是一个环! - 因为一旦检查是环,就不必再录入新的大小关系的,是一个截止的标识! - 总结:判断是不是拓扑序不唯一的标准是: - ① 队列节点数量等于n - ② 在过程中,有2个或以上的点在队列中 - - 如果只发现了②就着急返回拓扑序不唯一,就可能会掉入到是环的坑中! - */ - if (q.size() > 1) flag = 1; - - int u = q.front(); - q.pop(); - d[++dl] = u; // 按出队列的顺序来记录由小到大的关系 - for (int i = h[u]; ~i; i = ne[i]) { - int j = e[i]; - if (--in[j] == 0) q.push(j); - } - } - // 有环 - if (dl < n) return 1; - // 不确定 - if (dl == n && flag) return 2; - // 已确定 - return 3; -} - -int main() { - // n个变量,m个不等式,也就是n 个节点,m条边 - while (~scanf("%d%d", &n, &m) && n | m) { - // 多组测试数据,需要初始化 - - // 链式前向星 - memset(h, -1, sizeof h); - idx = 0; - // 入度数组初始化 - memset(ind, 0, sizeof ind); - - // 输入大小关系,'A'->0,...,'Z'->25 - for (int i = 1; i <= m; i++) { - scanf("%s", s); // 通用格式 类似于: B **注**:此题没有给出$M$的数据范围,导致我调试了半个多小时,差评$yxc$ \ No newline at end of file diff --git a/TangDou/AcWing/Floyd/343_1.cpp b/TangDou/AcWing/Floyd/343_1.cpp deleted file mode 100644 index 2f30f0c..0000000 --- a/TangDou/AcWing/Floyd/343_1.cpp +++ /dev/null @@ -1,74 +0,0 @@ -#include -// Floyd解决传送闭包问题 -using namespace std; -const int N = 27; -int n; // n个变量 -int m; // m个不等式 -int f[N][N]; // 传递闭包结果 - -void floyd() { - for (int k = 0; k < n; k++) - for (int i = 0; i < n; i++) - for (int j = 0; j < n; j++) - f[i][j] |= f[i][k] & f[k][j]; // i可以到达k,k可以到达j,那么i可以到达j -} -// 1:可以确定两两之间的关系,2:矛盾,3:不能确定两两之间的关系 -int check() { - // 如果i> S; - // 已确定或者出现了矛盾,就没有必要再处理了,但是,还需要耐心的读取完毕,因为可能还有下一轮,不读入完耽误下一轮 - if (k < 3) continue; - // 变量只可能为大写字母A~Z,映射到0~25 - int a = S[0] - 'A', b = S[2] - 'A'; - f[a][b] = 1; // 记录a -using namespace std; - -const int N = 30, M = N * N; - -int n, m; -int a[1050], b[1050]; // a[i] q; - bool flag = 0; - - for (int i = 0; i < n; i++) // 枚举每个节点,入度为零的入队列 - if (in[i] == 0) q.push(i); - - while (q.size()) { - /* - 注意:此处需要优先检查是不是有环,即使检查到某个点有多个前序节点,也并不表示它应该返回2,因为此时也可能是一个环! - 因为一旦检查是环,就不必再录入新的大小关系的,是一个截止的标识! - 总结:判断是不是拓扑序不唯一的标准是: - ① 队列节点数量等于n - ② 在过程中,有2个或以上的点在队列中 - - 如果只发现了②就着急返回拓扑序不唯一,就可能会掉入到是环的坑中! - */ - if (q.size() > 1) flag = 1; - - int u = q.front(); - q.pop(); - d[++dl] = u; // 按出队列的顺序来记录由小到大的关系 - for (int i = h[u]; ~i; i = ne[i]) { - int j = e[i]; - if (--in[j] == 0) q.push(j); - } - } - // 有环 - if (dl < n) return 1; - // 不确定 - if (dl == n && flag) return 2; - // 已确定 - return 3; -} - -int main() { - // n个变量,m个不等式,也就是n 个节点,m条边 - while (~scanf("%d%d", &n, &m) && n | m) { - // 多组测试数据,需要初始化 - - // 链式前向星 - memset(h, -1, sizeof h); - idx = 0; - // 入度数组初始化 - memset(ind, 0, sizeof ind); - - // 输入大小关系,'A'->0,...,'Z'->25 - for (int i = 1; i <= m; i++) { - scanf("%s", s); // 通用格式 类似于: B - -using namespace std; -const int N = 110, INF = 0x3f3f3f3f; -int n, m; -int g[N][N], dist[N][N]; -int path[N], idx; -int mid[N][N]; -int ans = INF; - -// i->j之间的最短路径中途经点有哪些 -void get_path(int i, int j) { - int k = mid[i][j]; // 获取中间转移点 - if (!k) return; // 如果i,j之间没有中间点,停止 - get_path(i, k); // 递归前半段 - path[idx++] = k; // 记录k节点 - get_path(k, j); // 递归后半段 -} - -int main() { - // n个顶点,m条边 - scanf("%d %d", &n, &m); - - // 初始化邻接矩阵 - memset(g, 0x3f, sizeof g); - for (int i = 1; i <= n; i++) g[i][i] = 0; // 邻接矩阵,自己到自己距离是0 - - while (m--) { - int a, b, c; - scanf("%d %d %d", &a, &b, &c); - g[a][b] = g[b][a] = min(g[a][b], c); // 求最短路之类,(a,b)之间多条边输入只保留最短边 - } - - // 把原始地图复制出来到生成最短距离dist - memcpy(dist, g, sizeof dist); - - for (int k = 1; k <= n; k++) { // 枚举每一个引入点k来连接缩短i,j的距离 - /* - Q1:为什么循环的时候i和j都需要小于k? - A:为了避免经过相同的点,比如i == k时,三个点就变成两个点了。 - 其实循环到n也是可以的,不过当i, j, k中有两个相同时就要continue一下 - - Q2:为什么非得把DP的这段代码嵌入到Floyd的整体代码中,不能先Floyd后再进行DP吗? - A:是不可以的。因为在进行插入节点号为k时,其实dist[i][j]中记录的是1~k-1插点后的最小距离, - 而不是全部插入点后的最短距离。 - */ - for (int i = 1; i < k; i++) - for (int j = i + 1; j < k; j++) - if (g[i][k] + g[k][j] < ans - dist[i][j]) { // 减法防止爆INT - ans = dist[i][j] + g[i][k] + g[k][j]; - // 找到更小的环,需要记录路径,并且要求: 最小环的所有节点(按顺序输出) - // 顺序 - // 1. 上面的i,j枚举逻辑是j>i,所以i是第一个 - // 2. i->j 中间的路线不明,需要用get_path进行查询出i->j的最短路径怎么走,当然,也是在 dist[i][k] + dist[k][j]) { - dist[i][j] = dist[i][k] + dist[k][j]; - mid[i][j] = k; // 记录路径i->j 是通过k进行转移的 - } - } - - if (ans == INF) - puts("No solution."); - else - for (int i = 0; i < idx; i++) cout << path[i] << ' '; - - return 0; -} \ No newline at end of file diff --git a/TangDou/AcWing/Floyd/344.md b/TangDou/AcWing/Floyd/344.md deleted file mode 100644 index 6384c32..0000000 --- a/TangDou/AcWing/Floyd/344.md +++ /dev/null @@ -1,131 +0,0 @@ -## [$AcWing$ $344$. 观光之旅](https://www.acwing.com/problem/content/346/) - -### 一、题目描述 -给定一张无向图,求图中一个 **至少包含 $3$ 个点** 的环,环上的节点不重复,并且环上的边的长度之和最小。 - -该问题称为 **无向图的最小环问题**。 - -**你需要输出最小环的方案**,若最小环不唯一,输出任意一个均可。 - -**输入格式** -第一行包含两个整数 $N$ 和 $M$,表示无向图有 $N$ 个点,$M$ 条边。 - -接下来 $M$ 行,每行包含三个整数 $u,v,l$,表示点 $u$ 和点 $v$ 之间有一条边,边长为 $l$。 - -**输出格式** -输出占一行,包含最小环的所有节点(按顺序输出),如果不存在则输出 `No solution.`。 - -**数据范围** -$1≤N≤100,1≤M≤10000,1≤l<500$ - -**输入样例**: -```cpp {.line-numbers} -5 7 -1 4 1 -1 3 300 -3 1 10 -1 2 16 -2 3 100 -2 5 15 -5 3 20 -``` - -**输出样例**: -```cpp {.line-numbers} -1 3 5 2 -``` - -### 二、$floyd + dp$ 求最小环模板(最少三点) - -![](https://cdn.acwing.com/media/article/image/2021/12/18/85607_ee5522ae60-g.png) - - -$floyd$是 **插点** 算法,在点$k$被 **插入前** 可计算$i->x>j,x \in [1 \sim k-1]$这样的最短路,当然,也可以不选择任何一个中间点,$dist[i][j]$天生最小。 - -枚举所有以$k$为环中 **最大节点** 的环即可。 - -> **解释**:$k$是从$1\sim n$的,说它是最大节点,是指每次插入的节点号最大,并不表示在环中它一定比$i,j$还大。 - - -### 三、$floyd+dp$ -```cpp {.line-numbers} -#include - -using namespace std; -const int N = 110, INF = 0x3f3f3f3f; -int n, m; -int g[N][N], dist[N][N]; -int path[N], idx; -int mid[N][N]; -int ans = INF; - -// i->j之间的最短路径中途经点有哪些 -void get_path(int i, int j) { - int k = mid[i][j]; // 获取中间转移点 - if (!k) return; // 如果i,j之间没有中间点,停止 - get_path(i, k); // 递归前半段 - path[idx++] = k; // 记录k节点 - get_path(k, j); // 递归后半段 -} - -int main() { - // n个顶点,m条边 - scanf("%d %d", &n, &m); - - // 初始化邻接矩阵 - memset(g, 0x3f, sizeof g); - for (int i = 1; i <= n; i++) g[i][i] = 0; // 邻接矩阵,自己到自己距离是0 - - while (m--) { - int a, b, c; - scanf("%d %d %d", &a, &b, &c); - g[a][b] = g[b][a] = min(g[a][b], c); // 求最短路之类,(a,b)之间多条边输入只保留最短边 - } - - // 把原始地图复制出来到生成最短距离dist - memcpy(dist, g, sizeof dist); - - for (int k = 1; k <= n; k++) { // 枚举每一个引入点k来连接缩短i,j的距离 - /* - Q1:为什么循环的时候i和j都需要小于k? - A:为了避免经过相同的点,比如i == k时,三个点就变成两个点了。 - 其实循环到n也是可以的,不过当i, j, k中有两个相同时就要continue一下 - - Q2:为什么非得把DP的这段代码嵌入到Floyd的整体代码中,不能先Floyd后再进行DP吗? - A:是不可以的。因为在进行插入节点号为k时,其实dist[i][j]中记录的是1~k-1插点后的最小距离, - 而不是全部插入点后的最短距离。 - */ - for (int i = 1; i < k; i++) - for (int j = i + 1; j < k; j++) - if (g[i][k] + g[k][j] < ans - dist[i][j]) { // 减法防止爆INT - ans = dist[i][j] + g[i][k] + g[k][j]; - // 找到更小的环,需要记录路径,并且要求: 最小环的所有节点(按顺序输出) - // 顺序 - // 1. 上面的i,j枚举逻辑是j>i,所以i是第一个 - // 2. i->j 中间的路线不明,需要用get_path进行查询出i->j的最短路径怎么走,当然,也是在 dist[i][k] + dist[k][j]) { - dist[i][j] = dist[i][k] + dist[k][j]; - mid[i][j] = k; // 记录路径i->j 是通过k进行转移的 - } - } - - if (ans == INF) - puts("No solution."); - else - for (int i = 0; i < idx; i++) cout << path[i] << ' '; - - return 0; -} -``` \ No newline at end of file diff --git a/TangDou/AcWing/Math/GameTheory/1319.md b/TangDou/AcWing/Math/GameTheory/1319.md deleted file mode 100644 index 0e8c8c4..0000000 --- a/TangDou/AcWing/Math/GameTheory/1319.md +++ /dev/null @@ -1,126 +0,0 @@ -##[$AcWing$ $1319$. 移棋子游戏](https://www.acwing.com/problem/content/description/1321/) - -### 一、题目描述 -给定一个有 $N$ 个节点的 **有向无环图**,图中某些节点上有棋子,两名玩家交替移动棋子。 - -玩家每一步可将任意一颗棋子沿一条有向边移动到另一个点,无法移动者输掉游戏。 - -对于给定的图和棋子初始位置,双方都会采取最优的行动,询问先手必胜还是先手必败。 - -**输入格式** -第一行,三个整数 $N,M,K$,$N$ 表示图中节点总数,$M$ 表示图中边的条数,$K$ 表示棋子的个数。 - -接下来 $M$ 行,每行两个整数 $X,Y$ 表示有一条边从点 $X$ 出发指向点 $Y$。 - -接下来一行, $K$ 个空格间隔的整数,表示初始时,棋子所在的节点编号。 - -节点编号从 $1$ 到 $N$。 - -**输出格式** -若先手胜,输出 `win`,否则输出 `lose`。 - -**数据范围** -$1≤N≤2000,1≤M≤6000,1≤K≤N$ - -**输入样例:** -```cpp {.line-numbers} -6 8 4 -2 1 -2 4 -1 4 -1 5 -4 5 -1 3 -3 5 -3 6 -1 2 4 6 -``` - -**输出样例:** -```cpp {.line-numbers} -win -``` - -### 二、解题思路 - -首先定义 $mex$ 函数,这是施加于一个集合的函数,返回 **最小的不属于这个集合的非负整数** - -例:$mex({1,2})=0,mex({0,1})=2,mex({0,1,2,4})=3$ - -在一张有向无环图中,对于每个点 $u$,设其**所有能到的点**的 $SG$ 函数值集合为集合 $A$,那么 $u$ 的 $SG$ 函数值为 $mex(A)$,记做 $SG(u)=mex(A)$ -例图: -
- -例图解释: -$SG(5)=mex({\phi})=0$ -$SG(3)=mex({SG(5)})=mex({0})=1$ -$SG(4)=mex({SG(5),SG(3)})=mex({0,1})=2$ -$SG(2)=mex({SG(3)}=mex({1})=0$ -$SG(1)=mex({SG(2),SG(4)})=mex({0,2})=1$ - -#### 本题思路 -- 如果只有一个棋子(棋子位置是$s$): -先手必胜 $\Leftrightarrow $ `sg(s)!=0` - -- 存在多个棋子(其实可以看成存在多个相同的棋盘,棋子的位置是$s_1,…,s_k$ -先手必胜 $\Leftrightarrow $ `sg(s1)^sg(s2)^...^sg(sk) != 0` - -### 三、实现代码 -```cpp {.line-numbers} -#include - -using namespace std; -const int N = 2010, M = 6010; - -// SG函数模板题 -int n, m, k; -int f[N]; - -int h[N], e[M], ne[M], idx; -void add(int a, int b) { - e[idx] = b, ne[idx] = h[a], h[a] = idx++; -} - -int sg(int u) { - //记忆化搜索 - if (~f[u]) return f[u]; - - //找出当前结点u的所有出边,看看哪个sg值没有使用过 - set S; - for (int i = h[u]; ~i; i = ne[i]) - S.insert(sg(e[i])); - - //找到第一个没有出现的过的自然数, 0,1,2,3,4,... - for (int i = 0;; i++) - if (S.count(i) == 0) { - f[u] = i; - break; - } - return f[u]; -} - -int main() { - memset(h, -1, sizeof h); - cin >> n >> m >> k; - while (m--) { - int a, b; - cin >> a >> b; - add(a, b); - } - - memset(f, -1, sizeof f); //初始化sg函数的结果表 - int res = 0; - while (k--) { - int u; - cin >> u; - res ^= sg(u); //计算每个出发点的sg(u),然后异或在一起 - } - - if (res) //所有出发点的异或和不等于0,先手必胜 - puts("win"); - else //所有出发点的异或和等于0,先手必败 - puts("lose"); - - return 0; -} -``` \ No newline at end of file diff --git a/TangDou/AcWing/Math/GameTheory/1321.md b/TangDou/AcWing/Math/GameTheory/1321.md deleted file mode 100644 index 8c4deee..0000000 --- a/TangDou/AcWing/Math/GameTheory/1321.md +++ /dev/null @@ -1,231 +0,0 @@ -##[$AcWing$ $1321$. 取石子](https://www.acwing.com/problem/content/description/1323/) - -**[参考题解](https://www.cnblogs.com/ZJXXCN/p/11068490.html)** - -### 一、题目描述 -$Alice$ 和 $Bob$ 两个好朋友又开始玩取石子了。 - -游戏开始时,有 $N$ 堆石子排成一排,然后他们轮流操作($Alice$ 先手),每次操作时从下面的规则中任选一个: - -- 从某堆石子中取走一个; -- 合并任意两堆石子。 - -不能操作的人输。 - -$Alice$ 想知道,她是否能有必胜策略。 - -**输入格式** -第一行输入 $T$,表示数据组数。 - -对于每组测试数据,第一行读入 $N$; - -接下来 $N$ 个正整数 $a_1,a_2,⋯,a_N$ ,表示每堆石子的数量。 - -**输出格式** -对于每组测试数据,输出一行。 - -输出 $YES$ 表示 $Alice$ 有必胜策略,输出 $NO$ 表示 $Alice$ 没有必胜策略。 - -**数据范围** -$1≤T≤100,1≤N≤50,1≤a_i≤1000$ - -**输入样例:** -```cpp {.line-numbers} -3 -3 -1 1 2 -2 -3 4 -3 -2 3 5 -``` - -**输出样例:** -```cpp {.line-numbers} -YES -NO -NO -``` - -### 二、博弈论总结 - - 必胜态 $\Rightarrow$ 选择合适方案 $\Rightarrow$ 必败态 - 必败态 $\Rightarrow$ 选择任何路线 $\Rightarrow$ 必胜态 - - -### 三、简单情况 -为什么会想到讨论简单情况呢?我们来思考一下:如果某一堆石子只有$1$个,随着我们执行拿走$1$个的操作,它的堆就没了,这样石子个数变了,堆数也变了,两个变量,问题变复杂了,我们上来就想难题,怕是搞不定。 - -既然这样,我们就思考一下 **子空间** :只考虑所有个数大于等于$2$的那些堆,其它可能存在石子数等于$1$的,等我们想明白这个简单问题再研究扩展的事,由易到难。 - -同时,我们需要思考博弈的胜负与什么因素相关呢?因为只有两种操作:**拿走一个石子、合并两堆**,很显然,**两个关键因素:石子个数、堆数** - -同时,两个操作同一时间只能执行一个,所以可以理解为拿走一个石子对结果影响一下,合并两堆石子对结果也是影响一下,初步考虑应该堆个数与石子总数的加法关系相关。 - - - -**子空间:当每堆的石子个数都是大于等于$2$时** - -
设$b$ = 堆数 + 石子总数 - $1$
-
结论:$b$是奇数⟺先手必胜,$b$是偶数⟺先手必败
- -**证明:** - -1、边界:当我们只有一堆石子且该堆石子个数为$1$个时,$b=1$,先手必胜。 - -2、当$b$为奇数,一定可以通过某种操作将$b$变成偶数 -* 如果堆数大于$1$,合并两堆让$b$变为偶数 -* 如果堆数等于$1$,从该堆石子中取出一个就可以让$b$变为偶数 - -3、当$b$为偶数,无论如何操作,$b$都必将变为奇数 -* 合并两堆,则$b$变为奇数 -* 从某一堆中取走一个石子: - * 若该堆石子个数大于$2$,则$b$变为奇数,且所有堆石子数量严格大于$1$ - * 若该堆石子个数等于$2$,取一个石子后,$b$变为奇数,该堆石子个数变为$1$个,此时就再是子空间范围内了,因为出现某堆的石子个数为$1$,而不是每一堆都大于等于$2$了!需要继续分类讨论: - -#### 特殊情况 -此时为了保证所有堆的石子个数大于$1$,**足够聪明的对手** 可以进行的操作分为两类: - ① 如果只有这一堆石子,此时 **对手必胜** - ② 如果有多堆石子,可以将这一个石子合并到其他堆中,这样每对石子个数都大于$1$ - - **$Q$:对手为什么一定要采用合并的操作,而不是从别的堆中取石子呢?** - 我来举两个简单的栗子: - - * **只有一堆石子** - 石子个数是$2$个。你拿走一个,对手直接拿走另一个,游戏结束,**对手赢了**!你也是足够聪明的,你会在这种情况下这么拿吗?不能吧~,啥时候可能遇到这个情况呢?就是你被 **逼到** 这个场景下,也就是一直处于必败态! - - * **两堆石子** - 每堆石子个数是$2$个。**我是先手**,可以有两种选择: - - (1)、从任意一堆中拿走$1$个, 现在的局面是$\{2,1\}$ - $$\large 后手选择(对手) \Rightarrow - \left\{\begin{matrix} - 从2中取一个 & \Rightarrow \{1,1\} & \Rightarrow - \large \left\{\begin{matrix} - 先手合并 \Rightarrow \{2\}& 剩下一个一个取,先手胜 \\ - 先手后手一个一个取 \Rightarrow 先手败 & - \end{matrix}\right. - \\ - 从1中取一个& \Rightarrow \{2,0\} & 剩下一个一个取,先手败\\ - 合并两堆 & \Rightarrow \{3\} & 剩下一个一个取,先手胜 \\ - \end{matrix}\right. - $$ - 指望对手出错我才有赢的机会,人家要是聪明,我就废了! - - 我是先手,我肯定不能把自己的命运交到别人手中!我选择合并两堆,这样我保准赢! - - (2)、把两堆直接合并,现在的状态$\{4\}$ - 这下进入了我的套路,你取吧,你取一个,我也取一个;你再取一个,我也再取一个,结果,没有了,**对手必败**。 - - 上面的例子可能不能描述所有场景,我现在$b$是奇数,我在必胜态,我不会让自己陷入到$b$可能是偶数的状态中去,如果我选择了 - - 合并操作减少$1$个堆 - - 拿走操作减少$1$个石子 - 都会把$b-1$这个偶数态给对方 - - 我不会傻到一个操作,即可能造成堆也变化,让石子个数也变化,这样就得看对方怎么选择了,而他还那么聪明,我不能犯这样的错误。 - -### 四、本题情况 -本题中可能存在一些堆的石子个数等于$1$: -* 假设有$a$堆石子,其中每堆石子个数为$1$ -* 剩余堆的石子个数都严格大于$1$ - -根据这些数量大于$1$的堆的石子可以求出上述定义出的$b$,我们使用$f(a, b)$表示此时先手必胜还是必败,因为博弈论在本质上是可以递推的,我们可以想出起点,再想出递推关系,就可以递推得到更大数据情况下的递推值,也就是博弈论本质上是$dp$。 - -
- -相关疑问 -$Q1:$**情况**$3$**为什么是两个表达式?** -答: -①当右侧存在时,合并左边两堆石子,则右侧多出一堆石子,并且,石子个数增加$2$,也就是$b+=3$ - -②当右侧一个都没有的时候,左边送来了一堆,两个石子,按$b$的定义,是堆数+石子个数$-1=2$,即$b+=2$ - -$Q2$:**为什么用一个奇数来描述简单情况的状态,而不是用偶数呢?** -答:因为要通过递推式进行计算,最终的边界是需要我们考虑的: - -- 如果用奇数,那么边界就是$b=1$,表示只有$1$堆,石子数量只有$1$个,此时当然必胜。 - -- 如果用偶数,比如边界是$b=0$,表示目前$0$堆,$0$个石子,这都啥也没有了,还必胜态,不符合逻辑,说不清道不明。 - -- 那要是不用$b=0$做边界,用$b=2$呢?表示只有$1$堆,石子数量只有$1$个,这个应该也是可以,但没有再仔细想了。 - -$Q3:$**情况**$2$**从右边取一个石子,如果此时右侧存在某一堆中石子个数是$2$,取走$1$个后,变成了$1$,不就是右侧减少了一个堆,减少了两个石子,即$b-=3$;同时,此堆石子个数变为$1$,左侧个数$a+=1$,为什么没有看到这个状态变化呢?** - -答:这是因为聪明人不会从右侧某个石子数量大于$2$的堆中取走石子! - -看一下 **讨论简单情况** 中第$3$点后面的 **特殊情况**: - - 如果右侧只有一堆,石子数量为$2$,拿走$1$个,剩$1$个,一堆一个,对方必胜,此为必败态 - - - 如果右侧大于一堆,某一堆只有$2$个石子,拿走$1$个,剩$1$个,对手足够聪明,会采用右侧两堆合并的办法,此时 石子数量减$1$,堆数减$1$,对$b$的影响是减$2$,对$b$的奇偶性没有影响,换句话说,如果你现在处在必败态,你这么整完,还是必败态 - - -### 五、时间复杂度 -这里因为$a$最大取$50$,$b$最大取$50050$,因此计算这些状态的计算量为$2.5×10^6$,虽然有最多$100$次查询,但是这些状态每个只会计算一遍,因此不会超时。 - - -### 六、实现代码 -```cpp {.line-numbers} -#include -using namespace std; -const int N = 55, M = 50050; // M 包括了 50 * 1000 + 50个石子数量为1的堆数 -int f[N][M]; - -int dfs(int a, int b) { - int &v = f[a][b]; - if (~v) return v; - // 简单情况: 即所有堆的石子个数都是严格大于1,此时a是0 - if (!a) return v = b % 2; // 奇数为先手必胜,偶数为先手必败 - - // 一般5个情况 + 1个特殊情况 - - // 特殊情况: 如果操作后出现b中只有一堆,且堆中石子个数为1 - // 那么应该归入到a中,并且b为0 - // 以下所有情况,如果能进入必败态,先手则必胜! - if (b == 1) return dfs(a + 1, 0); - - // 情况1:有a,从a中取一个 - if (a && !dfs(a - 1, b)) return v = 1; - - // 情况2, 4:有b,从b中取1个(石子总数 - 1) or 合并b中两堆(堆数 - 1), - if (b && !dfs(a, b - 1)) return v = 1; - - // 情况3:有a >= 2, 合并a中两个 - // 如果b的堆数不为0, a - 2, b + 1堆 + 2个石子(只需要加delta) ====> b + 3 - // 如果b的堆数为0, a - 2, 0 + 2个石子 + 1堆 - 1 ====> b + 2 - if (a >= 2 && !dfs(a - 2, b + (b ? 3 : 2))) return v = 1; - - // 情况5:有a,有b, 合并a中1个b中1个, a - 1, b的堆数无变化 + 1个石子(只加delta) - if (a && b && !dfs(a - 1, b + 1)) return v = 1; - - // 其他情况,则先手处于必败状态 - return v = 0; -} - -int main() { - memset(f, -1, sizeof f); - int T, n; - cin >> T; - while (T--) { - cin >> n; - int a = 0, b = 0; - for (int i = 0; i < n; i++) { - int x; - cin >> x; - if (x == 1) a++; - // b != 0时 加1堆 + 加x石子 = 原来的 + x + 1 (其实就是区别一开始的时候) - // 当b != 0时, 我们往后加的delta - // b == 0时 加1堆 + 加x石子 = 0 + 1 + x - 1 = x - // 注意操作符优先级 - else - b += b ? x + 1 : x; - } - - // 1 为先手必胜, 0为先手必败 - if (dfs(a, b)) - puts("YES"); - else - puts("NO"); - } - return 0; -} -``` \ No newline at end of file diff --git a/TangDou/AcWing/Math/GameTheory/1321_1.cpp b/TangDou/AcWing/Math/GameTheory/1321_1.cpp deleted file mode 100644 index 84ce99a..0000000 --- a/TangDou/AcWing/Math/GameTheory/1321_1.cpp +++ /dev/null @@ -1,63 +0,0 @@ -#include -using namespace std; -const int N = 55, M = 50050; // M 包括了 50 * 1000 + 50个石子数量为1的堆数 -int f[N][M]; - -int dfs(int a, int b) { - int &v = f[a][b]; - if (~v) return v; - // 简单情况: 即所有堆的石子个数都是严格大于1,此时a是0 - if (!a) return v = b % 2; // 奇数为先手必胜,偶数为先手必败 - - // 一般5个情况 + 1个特殊情况 - - // 特殊情况: 如果操作后出现b中只有一堆,且堆中石子个数为1 - // 那么应该归入到a中,并且b为0 - // 以下所有情况,如果能进入必败态,先手则必胜! - if (b == 1) return dfs(a + 1, 0); - - // 情况1:有a,从a中取一个 - if (a && !dfs(a - 1, b)) return v = 1; - - // 情况2, 4:有b,从b中取1个(石子总数 - 1) or 合并b中两堆(堆数 - 1), - if (b && !dfs(a, b - 1)) return v = 1; - - // 情况3:有a >= 2, 合并a中两个 - // 如果b的堆数不为0, a - 2, b + 1堆 + 2个石子(只需要加delta) ====> b + 3 - // 如果b的堆数为0, a - 2, 0 + 2个石子 + 1堆 - 1 ====> b + 2 - if (a >= 2 && !dfs(a - 2, b + (b ? 3 : 2))) return v = 1; - - // 情况5:有a,有b, 合并a中1个b中1个, a - 1, b的堆数无变化 + 1个石子(只加delta) - if (a && b && !dfs(a - 1, b + 1)) return v = 1; - - // 其他情况,则先手处于必败状态 - return v = 0; -} - -int main() { - memset(f, -1, sizeof f); - int T, n; - cin >> T; - while (T--) { - cin >> n; - int a = 0, b = 0; - for (int i = 0; i < n; i++) { - int x; - cin >> x; - if (x == 1) a++; - // b != 0时 加1堆 + 加x石子 = 原来的 + x + 1 (其实就是区别一开始的时候) - // 当b != 0时, 我们往后加的delta - // b == 0时 加1堆 + 加x石子 = 0 + 1 + x - 1 = x - // 注意操作符优先级 - else - b += b ? x + 1 : x; - } - - // 1 为先手必胜, 0为先手必败 - if (dfs(a, b)) - puts("YES"); - else - puts("NO"); - } - return 0; -} \ No newline at end of file diff --git a/TangDou/AcWing/Math/GameTheory/1321_2.cpp b/TangDou/AcWing/Math/GameTheory/1321_2.cpp deleted file mode 100644 index e9e0c1c..0000000 --- a/TangDou/AcWing/Math/GameTheory/1321_2.cpp +++ /dev/null @@ -1,45 +0,0 @@ -#include -using namespace std; - -int nums[60]; - -void process(int n) { - int a = 0, b = -1; - for (int i = 0; i < n; i++) { - if (nums[i] == 1) - a++; - else - b += nums[i] + 1; - } - if (a == 0 && b > 0) { - if (b % 2) - puts("YES"); - else - puts("NO"); - } - if (a > 0 && b > 2) { - if (a % 2 || b % 2) - puts("YES"); - else - puts("NO"); - } - if (a > 0 && b <= 2) { - if (a % 3) - puts("YES"); - else - puts("NO"); - } -} - -int main() { - int t, n; - cin >> t; - while (t--) { - memset(nums, 0, sizeof(nums)); - cin >> n; - for (int i = 0; i < n; i++) - cin >> nums[i]; - process(n); - } - return 0; -} \ No newline at end of file diff --git a/TangDou/AcWing/Math/GameTheory/1322.cpp b/TangDou/AcWing/Math/GameTheory/1322.cpp deleted file mode 100644 index 2a282ae..0000000 --- a/TangDou/AcWing/Math/GameTheory/1322.cpp +++ /dev/null @@ -1,40 +0,0 @@ -#include -using namespace std; -const int N = 1e3 + 5; -int a[N], L[N][N], R[N][N]; -int main() { - int T; - cin >> T; - while (T--) { - int n; - cin >> n; - for (int i = 1; i <= n; i++) { - cin >> a[i]; - L[i][i] = R[i][i] = a[i]; - } - for (int len = 2; len <= n; len++) - for (int i = 1; i + len - 1 <= n; i++) { - int j = i + len - 1, l = L[i][j - 1], r = R[i][j - 1], x = a[j]; - if (x == r) - L[i][j] = 0; - else if (x >= l && x < r) - L[i][j] = x + 1; - else if (x > r && x <= l) - L[i][j] = x - 1; - else - L[i][j] = x; - - l = L[i + 1][j], r = R[i + 1][j], x = a[i]; - if (x == l) - R[i][j] = 0; - else if (x >= r && x < l) - R[i][j] = x + 1; - else if (x > l && x <= r) - R[i][j] = x - 1; - else - R[i][j] = x; - } - puts(L[2][n] == a[1] ? "0" : "1"); - } - return 0; -} \ No newline at end of file diff --git a/TangDou/AcWing/Math/GameTheory/1322.md b/TangDou/AcWing/Math/GameTheory/1322.md deleted file mode 100644 index cfd80b7..0000000 --- a/TangDou/AcWing/Math/GameTheory/1322.md +++ /dev/null @@ -1,295 +0,0 @@ - - - - -##[$AcWing 1322$. 取石子游戏](https://www.acwing.com/problem/content/1324/) - -### 一、题目描述 -在研究过 $Nim$ 游戏及各种变种之后,$Orez$ 又发现了一种全新的取石子游戏,这个游戏是这样的: - -有 $n$ 堆石子,将这 $n$ 堆石子摆成一排。 - -游戏由两个人进行,两人轮流操作,每次操作者都可以从 **最左** 或 **最右** 的一堆中取出若干颗石子,可以将那一堆全部取掉,但不能不取,**不能操作的人就输了**。 - -$Orez$ 问:对于任意给出的一个初始局面,是否存在先手必胜策略。 - -**输入格式** -第一行为一个整数 $T$,表示有 $T$ 组测试数据。 - -对于每组测试数据,第一行为一个整数 $n$,表示有 $n$ 堆石子,第二行为 $n$ 个整数 $a_i$ ,依次表示每堆石子的数目。 - -**输出格式** -对于每组测试数据仅输出一个整数 $0$ 或 $1$,占一行。 - -其中 $1$ 表示有先手必胜策略,$0$ 表示没有。 - -**数据范围** -$1≤T≤10,1≤n≤1000,1≤a_i≤10^9$ - -**输入样例**: -```cpp {.line-numbers} -1 -4 -3 1 9 4 -``` -输出样例: -```cpp {.line-numbers} -0 -``` - -### 二、状态定义 - -**① 原来每堆数量是长成这个样的,可能是必胜状态,也可能是必败状态,都可以:** - -
- -| $a_i$ | $a_{i+1}$ | ... | $a_{j-1}$ | $a_{j}$ | -| ---- | ---- |---- | ---- | ---- | ---- | ---- | - -
- - -- 设 $left[i][j]$ 表示在 必胜区间 $[i,j]$ 区间的 **左侧** 放上一堆数量为 $left[i][j]$ 的石子后,**先手必败** -- 设 $right[i][j]$ 表示在 必胜区间 $[i,j]$ 区间的 **右侧** 放上一堆数量为 $right[i][j]$ 的石子后,**先手必败** - -**② 假如原来$a_i \sim a_j$为必胜态,那么你前面添上啥都是必败的** -**③ 假如原来$a_i \sim a_j$为必败态,那么你前面添上$left[i][j]=0$ 也还是必败的** - -**总结**:不管原来$a_i \sim a_j$是啥状态,反正,都可以通过向左边添加一个堆的方法(堆的厂子数量可以为$0$)使得状态改为 **先手必败** - -
- -| $left[i][j]$ | $a_i$ | $a_{i+1}$ | ... | $a_{j-1}$ | $a_{j}$ | $right[i][j]$ | -| ---- | ---- |---- | ---- | ---- | ---- | ---- | - -
- - - -即:$(left[i][j],\underbrace{a_i,a_{i+1},\cdots,a_j}_{a[i]\sim a[j]})$,$(\underbrace{a_i,a_{i+1},\cdots,a_j}_{a[i]\sim a[j]},right[i][j])$ 为 **先手必败** 局面 - -### 三、$left[i][j]$ 的**存在性证明** - -博弈论的题,时刻要记得 - -**转化关系** - -$$ -必胜态 \rightarrow -\large \left\{\begin{matrix} - 合适的办法 & \rightarrow & 必败态(让对手必败) \\ - 走错了(傻了) & \rightarrow & 必胜态(让对手必胜) -\end{matrix}\right. -$$ - -$$ -必败态 \rightarrow -\large \left\{\begin{matrix} - 无论怎么走(绝望) & \rightarrow & 必胜态(让对手必胜) \\ - 永远无法(绝望) & \rightarrow & 必败态(让对手必败) -\end{matrix}\right. -$$ - - -($right[i][j]$ 同理,下同): - -反证法: -假设不存在满足定义的 $left[i][j]$,则对于 **任意非负整数** $x$,有形如: - -$$\large \underbrace{x,a_i,a_{i+1},\cdots,a_j}_{A(x)}$$ 都为**必胜局面**,记为 $A(x)$ 局面。 - -由于 $A(x)$ 为必胜局面,故从 $A(x)$ 局面 必然存在$M$种一步可达必败局面。 - -若从最左边一堆中拿,因为假设原因,不可能变成必败局面,因为这样得到的局面仍形如 $A(x)$。 - -注意包括此行在内的接下来几行默认 $x \neq 0$ - -左边拿没用,只能考虑从右边拿: -于是设 $A(x)$ 一步可达的(某个)**必败局面**为 $(x,a_i,a_{i+1},\cdots,a_{j-1},y)$,显然有 $0 \le y < a_j$。 - -**由于 $x$ 有无限个,但 $y$ 只有 $a_j$种——根据抽屉原理,必存在 $x_1,x_2(x_1 \neq x_2),y$ 满足 $(x_1,a_i,a_{i+1},\cdots,a_{j-1},y)$ 和 $(x_2,a_i,a_{i+1},\cdots,a_{j-1},y)$ 都是必败局面**。但这两个必败局面之间 **实际一步可达**,故矛盾,进而原命题成立。 - -### 四、$left[i][j]$ 的唯一性证明 -反证法: -假设 $left(i,j)$ 不唯一,则存在非负整数 $x_1,x_2(x_1 \neq x_2)$,使得$(x_1,a_i,a_{i+1},⋯,a_{j−1},a_j)$ 和 $(x_2,a_i,a_{i+1},\cdots,a_{j-1},a_j)$ 均为必败局面,而这两个必败局面之间 **实际一步可达** ,故矛盾,进而原命题成立。 - - -### 五、状态转移 - -#### 1、边界情况 -$$\LARGE left[i][i]=a_i$$ - -当只有一堆石子时,我在这堆前面添加一堆,个数和这堆一样多,对于**两堆相同的石子**,**后手进行和先手对称的操作**,你咋干我就咋干,我拿完,你瞪眼~, **先手必败** - -#### 2、递推关系 -* 变化方法:从左侧拿走一些石子或者从右侧拿走一些石子 -* 让我们使用$left[i][j-1]$和$right[i][j-1]$来表示$left[i][j]$和$right[i][j]$,形成$DP$递推关系 - - > 前面动作都按要求整完了,问我们:本步骤,我们有哪些变化,根据这些变化,怎么样用前面动作积累下来的数据来完成本步骤数据变化的填充,这不就是动态规划吗? - -![](http://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/2023/02/cccc1a0f80d1475adcf8096aa9e35ff0.png) - -#### 3、推论 -有了上面推的$left[i][j]$唯一性,得出一个有用的推论: -**对于任意非负整数 $x \neq left(i,j)$,$\large (x,a_i,a_{i+1},\cdots,a_j)$为必胜局面** - -#### 4、特殊情况:$L=R=0$ -为方便叙述,下文记 $left[i][j-1]$ 为 $L$,记 $right[i][j-1]$ 为 $R$,并令 $\displaystyle \large x=a_j(x>0)$ - - -若 $R=0$ 则 $L=R=0$,此时 $x>\max\{L,R\}$,也就是说 $L=0$ 和 $R=0$ 都属于 $Case$ $5$,故其它 $Case$ 满足 $L,R>0$。 - -注:因$R=0$,表示在[$i$,$j-1$]确定后,右侧为$0$就能满足[$i$,$j-1$]这一段为先手必败,此时,左侧增加那堆个数为$0$就可以继续保持原来的先手必败,即$L=0$。 - - -#### 5、分类讨论 - -* $x=R$($Case$ $1$) - 最简单的情况——根据 $R=right[i][j-1]$ 的定义,区间 $[i,j]$ 本来就是必败局面,因此左边啥也不能添,添了反而错,故 - $$\large left[i][j]=0$$ - -* $x - - * $x \geq L$,即 $L \leq x < R$($Case$ $3$) - * **结论**:$$\large left[i][j]=x+1$$ - * **证明**: - 即 **求证** $(x+1,a_i,a_{i+1},\cdots,a_{j-1},x)$为 **必败局面** ,其中 $L \leq x L$,则后手将最右堆拿成 $z-1$ 个石子($z-1 \ge L>0$),**保证左侧比右侧多$1$个石子**,就能回到 $Case$ $3$ 本身,递归证明即可 - * 若 $z=L$,则后手将最右堆拿完,根据 $L[i][j-1]$ 定义知此时局面必败 - * 若 $0 - * 若先手拿最右边一堆,设拿了以后 **还剩 $z$ 个石子** - * 若 $z \ge L$,则后手将最左堆拿成 $z+1$个石子,就能回到 $Case$ $3$ 本身,递归证明即可 - * 若 $0R$ - * $x≤L$,即 $R < x \leq L$($Case$ $4$) - * 结论:$$\large left[i][j]=x-1$$ - * **证明**: - * 若先手拿最左边一堆,设拿了以后还剩 $z$ 个石子。 - * 若 $z \geq R$,则后手将最右堆拿成 $z+1$ 个石子,保证左侧比右侧多$1$个石子,就能回到 $Case$ $4$ 本身,递归证明即可。 - * 若 $0R$),由 $right[i][j-1])$ 的定义知此时是必败局面。 - -
- - * 若先手拿最右边一堆,设拿了以后还剩 $z$ 个石子。 - * 若 $z>R$,则后手将最左边一堆拿成 $z-1$ 个石子(注意 $z-1 \ge R >0$),递归证明即可。保证右侧比左侧多$1$个石子。 - * 若 $z=R$,则后手把最左堆拿完,根据 $right[i][j-1]$的定义可知得到了必败局面。 - * 若 $0 - - * $x>L$,即 $x>\max\{L,R\}$($Case$ $5$) - * **结论**:$$\large left[i][j]=x$$ - * **证明**: - 设先手将其中一堆拿成了 $z$ 个石子。 - * 若 $z>\max\{L,R\}$,后手将另一堆也拿成$z$个,回到 $Case$ $5$,递归证明。 - * 若 $0温馨提示:**请看清楚 $L$ 取不取等,乱取等是错的!** - -同理可求 $R(i,j)$。 - -回到原题,**先手必败当且仅当** $L[2][n]=a_1$ ,于是我们就做完啦! - -时间复杂度 $O(n^2)$。 - -### 六、实现代码 -```cpp {.line-numbers} -#include - -using namespace std; -const int N = 1010; -int n; -int a[N], l[N][N], r[N][N]; - -int main() { - int T; - scanf("%d", &T); - while (T--) { - scanf("%d", &n); - for (int i = 1; i <= n; i++) scanf("%d", &a[i]); - - for (int len = 1; len <= n; len++) - for (int i = 1; i + len - 1 <= n; i++) { - int j = i + len - 1; - if (len == 1) - l[i][j] = r[i][j] = a[i]; - else { - int L = l[i][j - 1], R = r[i][j - 1], x = a[j]; - if (R == x) - l[i][j] = 0; - else if (x < L && x < R || x > L && x > R) - l[i][j] = x; - else if (L > R) - l[i][j] = x - 1; - else - l[i][j] = x + 1; - - // 与上述情况对称的四种情况 - L = l[i + 1][j], R = r[i + 1][j], x = a[i]; - if (L == x) - r[i][j] = 0; - else if (x < L && x < R || x > L && x > R) - r[i][j] = x; - else if (R > L) - r[i][j] = x - 1; - else - r[i][j] = x + 1; - } - } - - if (n == 1) - puts("1"); - else - printf("%d\n", l[2][n] != a[1]); - } - - return 0; -} -``` \ No newline at end of file diff --git a/TangDou/AcWing/Math/GameTheory/test.cpp b/TangDou/AcWing/Math/GameTheory/test.cpp deleted file mode 100644 index 1d15929..0000000 --- a/TangDou/AcWing/Math/GameTheory/test.cpp +++ /dev/null @@ -1,27 +0,0 @@ -#include - -using namespace std; -const int INF = 0x3f3f3f3f; -typedef long long LL; -const int N = 1e5 + 10; -int q[N]; -int n; -void quick_sort(int q[], int l, int r) { - if (l >= r) return; - int i = l - 1, j = r + 1, x = q[(l + r) >> 1]; - while (i < j) { - do i++; - while (q[i] < x); - do j--; - while (q[j] > x); - if (i < j) swap(q[i], q[j]); - } - quick_sort(q, l, j), quick_sort(q, j + 1, r); -} -int main() { - scanf("%d", &n); - for (int i = 1; i <= n; i++) scanf("%d", &q[i]); - quick_sort(q, 1, n); - for (int i = 1; i <= n; i++) printf("%d ", q[i]); - return 0; -} \ No newline at end of file diff --git a/TangDou/AcWing/Math/QiWang/217.drawio b/TangDou/AcWing/Math/QiWang/217.drawio deleted file mode 100644 index d9a22c8..0000000 --- a/TangDou/AcWing/Math/QiWang/217.drawio +++ /dev/null @@ -1 +0,0 @@ -5VpLc9owEP41zLSHMtbLj2NDHj00M+mkbdLePFiAEmNRI4rJr6+MZbAlB0gCVgkzHLwraWV9+3m1WtFBvXF2lYaT0TWPaNyBTpR10HkHQoAh7OQ/J1oUGt8jhWKYskh1Witu2RNVSkdpZyyi01pHwXks2KSu7PMkoX1R04Vpyuf1bgMe12edhENqKG77YWxq71gkRkrrErxu+ELZcFRODdygaBmHZW+1lOkojPi8okIXHdRLORfF0zjr0ThHrwSmGHf5TOvqzVKaiF0GDB7cy3F2+fPp4QpP/izI7Ovi/pOy8jeMZ2rFD+ptxaLEQJqRcEvhbD5igt5Own7eMpcel7qRGMdSAvIxnE4KHwxYRuWsZ1OR8scVcFhqBjwRyssQlbKaqmFF5evRVNCsolIrvKJ8TEW6kF1Ua4AV2opvIFDyfO09olSjit9KXaj4MlxZXiMqHxSozQBf9JxfT78ZFL9/XH0PxfUw/PatAeAM2EbYgLMB9GcRdoM6wtCxjDA0EUZHjbDneTWEEbKMMDIRhkeNcKBxGNvmMDYRfjxqhH2/jjCxzWFiIJwcNcAu1AC2TWHXANjEN4k+5zmZlPpxOJ2yfh3WlM+SKMfz3NEQ9KRMMybuVVv+/Ct/7hIlnWeVpvNFKSRyaffK/FKojMrF9bClVI4z/VkshkZGumgkK1M+S/t0e84lwnRIxbbUwSTAjg5OaRwK9rf+uk1eVzPccCYXst6EHC1EehpximWqUdW0UzdENENQM1TgYBhaknC17Nfz0jN4edeQgckPUdTJWFCgx2OeSk3CkzwuDFgca6owZsMkZ7TkAZX6s/yzZvIE8Vk1jFkUxc8Flc2U30cg1uIE8M044TbQCB0qTvitxYku9CuhArwmULw4TORGb2jKJFY5GZa9LIQOaDN0BNoXLw3vJ3RApBk6cOgI2qMq2Zmp/8XWhK3yy8V1Wug5za788n3YdT3PdQMijWpWEfa6aHn4klsfQq7jtsq9sgB2ePKBN1BvFTYrKdUugdMCZZFVypZlz7dSNgBBTlnfgW7g5QyumSXY6WIMCCI+CggO2mWsWeo6VLh8wQlgA2e1MwHcwtmltJ+9fVOVZSuRiVUiBxtCpsztq40owK8jOXBg/XPBoN0zAzBrinum8u603HZU3SMD8TEwEDj6yVTPCnelmavXqFo+mQKzrnqogPmC/PK1W/q+8tJNtyj/NzNPIy81S9Ut7PJvyEs9WI+nEi+7+zw8Bi6/oyhrVv7vGi4IT6YACD3LBUBg3hTcNdwnvluHGJ8Wari6adcjTTVy8/7x3XrE9zSHNPxppF2HmEXywYcMfDwdl7hAcwm27RKzGCxdgk7IJZ7216rVRZ4tl5Tz110CT8gl+t6Obe/t0KwBSpc8npBL9DsrQg7mEimu/z1apMvrP+Gii38= \ No newline at end of file diff --git a/TangDou/AcWing/Math/QiWang/217.md b/TangDou/AcWing/Math/QiWang/217.md deleted file mode 100644 index 83cfec7..0000000 --- a/TangDou/AcWing/Math/QiWang/217.md +++ /dev/null @@ -1,206 +0,0 @@ -##[$AcWing$ $217$. 绿豆蛙的归宿](https://www.acwing.com/problem/content/description/219/) - -### 一、题目描述 -给出一个有向无环的连通图,起点为 $1$ ,终点为 $N$,每条边都有一个长度。 - -数据保证从起点出发能够到达图中所有的点,图中所有的点也都能够到达终点。 - -绿豆蛙从起点出发,走向终点。 - -到达每一个顶点时,如果有 $K$ 条离开该点的道路,绿豆蛙可以选择任意一条道路离开该点,并且走向每条路的概率为 $1/K$。 - -现在绿豆蛙想知道,从起点走到终点所经过的路径总长度的 **期望** 是多少? - -**输入格式** -第一行: 两个整数 $N,M$,代表图中有 $N$ 个点、$M$ 条边。 - -第二行到第 $1+M$ 行: 每行 $3$ 个整数 $a,b,c$,代表从 $a$ 到 $b$ 有一条长度为 $c$ 的有向边。 - -**输出格式** -输出从起点到终点路径总长度的 **期望值**,结果四舍五入保留两位小数。 - -**数据范围** -$1≤N≤10^5,1≤M≤2N$ - -**输入样例:** -```cpp {.line-numbers} -4 4 -1 2 1 -1 3 2 -2 3 3 -3 4 4 -``` - -**输出样例:** -```cpp {.line-numbers} -7.00 -``` - -### 二、数学期望 - -**[视频讲解 数学期望及性质](https://www.ixigua.com/6978816201023554061)** - -**[参考题解](https://www.acwing.com/solution/content/63508/)** - -首先明白一点:到达某个结果的期望值 = 这个结果 * 从起始状态到这个状态的概率 - -$Q:$什么意思呢? - -如图: -
- -我们计算从$1$号点到$3$号点的期望距离 - -路径$1$. $\displaystyle 1−>3:E_1=2×\frac{1}{2}=1$ - -路径$2$. $\displaystyle 1−>2−>3:E_2=1×\frac{1}{2}+3×\frac{1}{2}×1=2$ - -这里路径$2$中从$1$到$2$概率为$\displaystyle \frac{1}{2}$,但单看从$2$到$3$概率就是$1$,但是从$1$到$3$那就是从($1$到$2$的概率)$\displaystyle \frac{1}{2}$×$1$($2$到$3$的概率)=$\displaystyle \frac{1}{2}$。  - -所以从 点$1$ 到 点$3$ 的数学期望值=$1+2=3$ - -

总结:概率是叠乘的

- - -本题有 **正推** 和 **倒推** 两种写法: - -### 二、正推法 -
- -设: -- $a_1, a_2, a_3 … a_k$ 到 $j$ 的权值为 $w_1, w_2, w_3 … w_k$, -- 从起点到这$k$个点的概率为:$p_1, p_2, p_3 … p_k$ -- 每个点的出度为:$out_1, out_2, out_3, … , out_k$ - -这里的$1\sim k$个点的从起点的到该点的概率一定是确定的,也就是说这个点的概率是被更新完的,即此时这个点的入度为$0$! - -那么就有: -$$f(i):表示从起点到i点的期望距离$$ -$$f(j)=\frac{f(1)+w_1\times p_1}{out_1}+\frac{f(2)+w_2\times p_2}{out_2}+\frac{f(3)+w_3\times p_3}{out_3}+...+\frac{f(k)+w_k\times p_k}{out_k} $$ - -#### 正推代码 -```cpp {.line-numbers} -#include -using namespace std; -const int N = 1e5 + 10, M = 2 * N; - -//邻接表 -int h[N], e[M], ne[M], w[M], idx; -void add(int a, int b, int c) { - e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++; -} - -int n, m; // n个顶点,m条边 - -int out[N], in[N]; //出度,入度 -double f[N], g[N]; // f:数学期望结果 g:概率 - -void topsort() { - queue q; - //起点为1,起点的概率为100% - q.push(1); - g[1] = 1.0; - f[1] = 0.0; - - // DAG,执行拓扑序,以保证计算的顺序正确,确保递归过程中,前序数据都已处理完毕 - while (q.size()) { - auto u = q.front(); - q.pop(); - - for (int i = h[u]; ~i; i = ne[i]) { //枚举的是每边相邻边 - int j = e[i]; //此边,一端是t,另一端是j - //此边边条w[i] - f[j] += (f[u] + w[i] * g[u]) / out[u]; - g[j] += g[u] / out[u]; // p[j]也需要概率累加 - //拓扑序的标准套路 - in[j]--; - if (!in[j]) q.push(j); - } - } -} - -int main() { - //初始化邻接表 - memset(h, -1, sizeof h); - cin >> n >> m; - - while (m--) { - int a, b, c; - cin >> a >> b >> c; - add(a, b, c); - //维护出度,入度 - out[a]++, in[b]++; - } - //拓扑序 - topsort(); - - //正向递推,输出结果,保留两位小数 - printf("%.2lf", f[n]); - - return 0; -} -``` -### 三、倒推法 -现在学会了正推,来看看 **逆推**,即 **从终点找到起点** - - -
- -设 $f[x]$ 表示结点 $x$ 走到终点所经过的路径的期望长度。显然 $f[n]=0$ ,最后要求 $f[1]$ 。 - -一般来说,**初始状态确定时可用顺推,终止状态确定时可用逆推**。 - -设 $x$ 出发有 $k$ 条边,分别到达 $y_1,y_2...y_k$ ,边长分别为 $z_1,z_2...z_k$ ,根据数学期望的定义和性质,有: - -$$f[x]=\frac 1 k\times (f[y_1]+z_1)+\frac 1 k\times (f[y_2]+z_2)+...+\frac 1 k\times (f[y_k]+z_k)=\frac 1 k \times \sum_{i=1}^k(f[y_i]+z_i)$$ -根据设定已经确定是能够到达 $n$ 点了,概率为 $1$ 。 - -$f[n]$ 已知,需要求解 $f[1]$ ,建立 **反向图**,按照 **拓扑序** 求解。 - -#### 倒推代码 -```cpp {.line-numbers} -#include -using namespace std; -const int N = 100010, M = N << 1; -int n, m; -int in[N], g[N]; //入度,入度的备份数组,原因:in在topsort中会不断变小受破坏 - -double f[N]; - -//链式前向星 -int e[M], h[N], idx, w[M], ne[M]; -void add(int a, int b, int c = 0) { - e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++; -} - -void topsort() { - queue q; - q.push(n); - f[n] = 0; // n到n的距离期望是0 - while (q.size()) { - int u = q.front(); - q.pop(); - for (int i = h[u]; ~i; i = ne[i]) { //枚举每条入边(因为是反向图) - int j = e[i]; - f[j] += (f[u] + w[i]) / g[j]; - in[j]--; - if (in[j] == 0) q.push(j); - } - } -} -int main() { - memset(h, -1, sizeof(h)); - cin >> n >> m; - - while (m--) { - int a, b, c; - cin >> a >> b >> c; - add(b, a, c); //反向图,计算从n到1 - in[a]++; //入度 - g[a] = in[a]; //入度数量 - } - topsort(); - printf("%.2lf\n", f[1]); - return 0; -} -``` \ No newline at end of file diff --git a/TangDou/AcWing/Math/QiWang/217_DaoTui.cpp b/TangDou/AcWing/Math/QiWang/217_DaoTui.cpp deleted file mode 100644 index ba0fe13..0000000 --- a/TangDou/AcWing/Math/QiWang/217_DaoTui.cpp +++ /dev/null @@ -1,44 +0,0 @@ -#include -using namespace std; -const int N = 100010, M = N << 1; -int n, m; -int in[N], g[N]; //入度,入度的备份数组,原因:in在topsort中会不断变小受破坏 - -double f[N]; - -//链式前向星 -int e[M], h[N], idx, w[M], ne[M]; -void add(int a, int b, int c = 0) { - e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++; -} - -void topsort() { - queue q; - q.push(n); - f[n] = 0; // n到n的距离期望是0 - while (q.size()) { - int u = q.front(); - q.pop(); - for (int i = h[u]; ~i; i = ne[i]) { //枚举每条入边(因为是反向图) - int j = e[i]; - f[j] += (f[u] + w[i]) / g[j]; - in[j]--; - if (in[j] == 0) q.push(j); - } - } -} -int main() { - memset(h, -1, sizeof(h)); - cin >> n >> m; - - while (m--) { - int a, b, c; - cin >> a >> b >> c; - add(b, a, c); //反向图,计算从n到1 - in[a]++; //入度 - g[a] = in[a]; //入度数量 - } - topsort(); - printf("%.2lf\n", f[1]); - return 0; -} \ No newline at end of file diff --git a/TangDou/AcWing/Math/QiWang/217_ZhengTui.cpp b/TangDou/AcWing/Math/QiWang/217_ZhengTui.cpp deleted file mode 100644 index 05e64ee..0000000 --- a/TangDou/AcWing/Math/QiWang/217_ZhengTui.cpp +++ /dev/null @@ -1,59 +0,0 @@ -#include -using namespace std; -const int N = 1e5 + 10, M = 2 * N; - -//邻接表 -int h[N], e[M], ne[M], w[M], idx; -void add(int a, int b, int c) { - e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++; -} - -int n, m; // n个顶点,m条边 - -int out[N], in[N]; //出度,入度 -double f[N], g[N]; // f:数学期望结果 g:概率 - -void topsort() { - queue q; - //起点为1,起点的概率为100% - q.push(1); - g[1] = 1.0; - f[1] = 0.0; - - // DAG,执行拓扑序,以保证计算的顺序正确,确保递归过程中,前序数据都已处理完毕 - while (q.size()) { - auto u = q.front(); - q.pop(); - - for (int i = h[u]; ~i; i = ne[i]) { //枚举的是每边相邻边 - int j = e[i]; //此边,一端是t,另一端是j - //此边边条w[i] - f[j] += (f[u] + w[i] * g[u]) / out[u]; - g[j] += g[u] / out[u]; // p[j]也需要概率累加 - //拓扑序的标准套路 - in[j]--; - if (!in[j]) q.push(j); - } - } -} - -int main() { - //初始化邻接表 - memset(h, -1, sizeof h); - cin >> n >> m; - - while (m--) { - int a, b, c; - cin >> a >> b >> c; - add(a, b, c); - //维护出度,入度 - out[a]++, in[b]++; - } - //拓扑序 - topsort(); - - //正向递推,输出结果,保留两位小数 - printf("%.2lf", f[n]); - - return 0; -} \ No newline at end of file diff --git a/TangDou/AcWing/Math/QiWang/218.md b/TangDou/AcWing/Math/QiWang/218.md deleted file mode 100644 index a4c716a..0000000 --- a/TangDou/AcWing/Math/QiWang/218.md +++ /dev/null @@ -1,152 +0,0 @@ -##[$AcWing$ $218$. 扑克牌 ](https://www.acwing.com/problem/content/description/220/) - -### 一、题目描述 -$Admin$ 生日那天,$Rainbow$ 来找 $Admin$ 玩扑克牌。 - -玩着玩着 $Rainbow$ 觉得太没意思了,于是决定给 $Admin$ 一个考验。 - -$Rainbow$ 把一副扑克牌($54$张)随机洗开,倒扣着放成一摞。 - -然后 $Admin$ 从上往下依次翻开每张牌,每翻开一张黑桃、红桃、梅花或者方块,就把它放到对应花色的堆里去。 - -$Rainbow$ 想问问 $Admin$,得到 $A$ 张黑桃、$B$ 张红桃、$C$ 张梅花、$D$ 张方块需要翻开的牌的张数的期望值 $E$ 是多少? - -特殊地,如果翻开的牌是大王或者小王,$Admin$ 将会把它作为某种花色的牌放入对应堆中,使得放入之后 $E$的值尽可能小。 - -由于 $Admin$ 和 $Rainbow$ 还在玩扑克,所以这个程序就交给你来写了。 - -**输入格式** -输入仅由一行,包含四个用空格隔开的整数,$A,B,C,D$。 - -**输出格式** -输出需要翻开的牌数的期望值 $E$,四舍五入保留 $3$ 位小数。 - -如果不可能达到输入的状态,输出 `-1.000`。 - -**数据范围** -$0≤A,B,C,D≤15$ - -**输入样例:** -```cpp {.line-numbers} -1 2 3 4 -``` - -**输出样例:** -```cpp {.line-numbers} -16.393 -``` - -### 二、题意分析 - -$Q$:为什么从终止状态向起始状态递推? - -**答**:满足条件的终止状态较多,而起始状态唯一。考虑以终止状态为初值,起始状态为目标,进行动态规划。 - -#### 状态表示 -$f[a][b][c][d][x][y]$ : 当前已翻开状态下,还需翻开牌的数量 **期望数**。 - -- $a,b,c,d$ 为已翻开的各类牌 (黑红花片) 的数量 -- $x,y$代表大、小王的状态($0$为未翻开,$1$代表已翻开且当做黑桃,以此类推), 设 $rst$ 为剩余牌的数量。 -$$rst=54-a-b-c-d-(x!=0)-(y!=0)$$ - -若 $a < 13$,则当前抽到黑桃的贡献为 - -$$\frac{13−a}{rst} \times f[a+1][b][c][d][x][y]$$ - -其余花色同理。若小王被抽取,取可转移状态期望最小的一个进行状态转移,其贡献为: - -$$\frac{1}{rst} \times \min_{1≤i≤4}f[a][b][c][d][i][y]$$ - -大王同理。 - -​记忆化搜索求解,若无牌可抽仍未到达 $a > = A \&\& b > = B \&\& c > = C \&\& d > = D$ 的终止状态,则期望为正无穷,代表不合法的状态。 - - -#### 三、实现代码 - -```cpp {.line-numbers} -#include -using namespace std; -const int N = 15; -const int INF = 0x3f3f3f3f; -double f[N][N][N][N][5][5]; -int st[N][N][N][N][5][5]; -int A, B, C, D; - -//如果大小王翻出来放1里,则a++,放2里b++,... -void add(int &a, int &b, int &c, int &d, int x) { - if (x == 1) a++; - if (x == 2) b++; - if (x == 3) c++; - if (x == 4) d++; -} - -/* -功能:计算当前状态f(a,b,c,d,x,y)下的期望值 -*/ -double dfs(int a, int b, int c, int d, int x, int y) { - //记忆化,同时因为f为double类型,不能使用传统的memset(0x3f)之类 - //进行初始化并判断是否修改过,只能再开一个st数组 - if (st[a][b][c][d][x][y]) return f[a][b][c][d][x][y]; - st[a][b][c][d][x][y] = 1; - - //递归出口:当前状态是否到达目标状态,目标状态的期望值是0 - int ta = a, tb = b, tc = c, td = d; //抄出来 - add(ta, tb, tc, td, x), add(ta, tb, tc, td, y); //大王小王会改变四个花色的数量 - if (ta >= A && tb >= B && tc >= C && td >= D) return 0; - - //当前状态下的剩余牌数量 - int rst = 54 - ta - tb - tc - td; - if (rst == 0) return INF; //还没有完成目标,没有剩余的牌了,无解 - - //当前状态可以向哪些状态转移 - // Q:v为什么要初始化为1? - // A:看题解内容 - double v = 1; - if (a < 13) //黑桃有剩余,可能选出的是黑桃 - v += dfs(a + 1, b, c, d, x, y) * (13 - a) / rst; - if (b < 13) //红桃有剩余,可能选出的是红桃 - v += dfs(a, b + 1, c, d, x, y) * (13 - b) / rst; - if (c < 13) //梅花有剩余,可能选出的是梅花 - v += dfs(a, b, c + 1, d, x, y) * (13 - c) / rst; - if (d < 13) //方块有剩余,可能选出的是方块 - v += dfs(a, b, c, d + 1, x, y) * (13 - d) / rst; - - //如果小王没有被选出 - if (x == 0) - v += min(min(dfs(a, b, c, d, 1, y), dfs(a, b, c, d, 2, y)), min(dfs(a, b, c, d, 3, y), dfs(a, b, c, d, 4, y))) / rst; - - //如果大王没有被选出 - if (y == 0) - v += min(min(dfs(a, b, c, d, x, 1), dfs(a, b, c, d, x, 2)), min(dfs(a, b, c, d, x, 3), dfs(a, b, c, d, x, 4))) / rst; - - return f[a][b][c][d][x][y] = v; -} - -int main() { - cin >> A >> B >> C >> D; - //① 终点状态不唯一,起点是唯的的,所以以起点为终点,以终点为起点,反着推 - //② AcWing 217. 绿豆蛙的归宿 需要建图,本题不用建图 - double res = dfs(0, 0, 0, 0, 0, 0); //四种花色、大小王都还没有被抽取 - - if (res > INF / 2) //因为是浮点数,不能用等号判断是不是相等,简单的办法就是INF/2 - puts("-1.000"); - else - printf("%.3f\n", res); - return 0; -} -``` - -### 四、期望值为什么初始化为$1$? - -$f[i]$: 从$i$卡牌状态到终点状态所需要的**期望卡牌数** - -每次抽一张牌变到下个状态,所以每条路径的权值为$1$ - -$$\large f[v]=p_1×(f[1]+1)+p_2×(f[2]+1)+p_3×(f[3]+1)+…+p_k×(f[k]+1) = \\ -\sum_{i=1}^{k}p_i+\sum_{i=1}^{k}p_i \times f[i] -$$ - - 因为$v$一定能到达下个局面,所以下个状态的概率和为$1$,这里的$\large \displaystyle \sum_{i=1}^{k}p_i=1$ 那么就有:$\displaystyle \large f[v]=1+\sum_{i=1}^{k}p_i \times f[i]$  -综上这里的$f[v]$可以初始化为$1$! -
\ No newline at end of file diff --git a/TangDou/AcWing/Math/QiWang/218_dfs.cpp b/TangDou/AcWing/Math/QiWang/218_dfs.cpp deleted file mode 100644 index cb9302e..0000000 --- a/TangDou/AcWing/Math/QiWang/218_dfs.cpp +++ /dev/null @@ -1,70 +0,0 @@ -#include -using namespace std; -const int N = 15; -const int INF = 0x3f3f3f3f; -double f[N][N][N][N][5][5]; -int st[N][N][N][N][5][5]; -int A, B, C, D; - -//如果大小王翻出来放1里,则a++,放2里b++,... -void add(int &a, int &b, int &c, int &d, int x) { - if (x == 1) a++; - if (x == 2) b++; - if (x == 3) c++; - if (x == 4) d++; -} - -/* -功能:计算当前状态f(a,b,c,d,x,y)下的期望值 -*/ -double dfs(int a, int b, int c, int d, int x, int y) { - //记忆化,同时因为f为double类型,不能使用传统的memset(0x3f)之类 - //进行初始化并判断是否修改过,只能再开一个st数组 - if (st[a][b][c][d][x][y]) return f[a][b][c][d][x][y]; - st[a][b][c][d][x][y] = 1; - - //递归出口:当前状态是否到达目标状态,目标状态的期望值是0 - int ta = a, tb = b, tc = c, td = d; //抄出来 - add(ta, tb, tc, td, x), add(ta, tb, tc, td, y); //大王小王会改变四个花色的数量 - if (ta >= A && tb >= B && tc >= C && td >= D) return 0; - - //当前状态下的剩余牌数量 - int rst = 54 - ta - tb - tc - td; - if (rst == 0) return INF; //还没有完成目标,没有剩余的牌了,无解 - - //当前状态可以向哪些状态转移 - // Q:v为什么要初始化为1? - // A:看题解内容 - double v = 1; - if (a < 13) //黑桃有剩余,可能选出的是黑桃 - v += dfs(a + 1, b, c, d, x, y) * (13 - a) / rst; - if (b < 13) //红桃有剩余,可能选出的是红桃 - v += dfs(a, b + 1, c, d, x, y) * (13 - b) / rst; - if (c < 13) //梅花有剩余,可能选出的是梅花 - v += dfs(a, b, c + 1, d, x, y) * (13 - c) / rst; - if (d < 13) //方块有剩余,可能选出的是方块 - v += dfs(a, b, c, d + 1, x, y) * (13 - d) / rst; - - //如果小王没有被选出 - if (x == 0) - v += min(min(dfs(a, b, c, d, 1, y), dfs(a, b, c, d, 2, y)), min(dfs(a, b, c, d, 3, y), dfs(a, b, c, d, 4, y))) / rst; - - //如果大王没有被选出 - if (y == 0) - v += min(min(dfs(a, b, c, d, x, 1), dfs(a, b, c, d, x, 2)), min(dfs(a, b, c, d, x, 3), dfs(a, b, c, d, x, 4))) / rst; - - return f[a][b][c][d][x][y] = v; -} - -int main() { - cin >> A >> B >> C >> D; - //① 终点状态不唯一,起点是唯的的,所以以起点为终点,以终点为起点,反着推 - //② AcWing 217. 绿豆蛙的归宿 需要建图,本题不用建图 - double res = dfs(0, 0, 0, 0, 0, 0); //四种花色、大小王都还没有被抽取 - - if (res > INF / 2) //因为是浮点数,不能用等号判断是不是相等,简单的办法就是INF/2 - puts("-1.000"); - else - printf("%.3f\n", res); - return 0; -} \ No newline at end of file diff --git a/TangDou/AcWing/Math/QiWang/218_dp.cpp b/TangDou/AcWing/Math/QiWang/218_dp.cpp deleted file mode 100644 index ca614d3..0000000 --- a/TangDou/AcWing/Math/QiWang/218_dp.cpp +++ /dev/null @@ -1,48 +0,0 @@ -#include -using namespace std; -const int INF = 0x3f3f3f3f; -const int N = 15; -double f[N][N][N][N][5][5]; -int a, b, c, d; -int main() { - memset(f, -1, sizeof f); - cin >> a >> b >> c >> d; - - for (int i = 13; i >= 0; i--) - for (int j = 13; j >= 0; j--) - for (int k = 13; k >= 0; k--) - for (int w = 13; w >= 0; w--) - for (int x = 4; x >= 0; x--) - for (int y = 4; y >= 0; y--) { - double &v = f[i][j][k][w][x][y]; - if (i + (x == 1) + (y == 1) >= a && j + (x == 2) + (y == 2) >= b - && k + (x == 3) + (y == 3) >= c && w + (x == 4) + (y == 4) >= d) { - v = 0; - continue; - } - - v = 1; - int sum = i + j + k + w + (x != 0) + (y != 0); - if (i < 13) v += f[i + 1][j][k][w][x][y] * (13 - i) / (54 - sum); - if (j < 13) v += f[i][j + 1][k][w][x][y] * (13 - j) / (54 - sum); - if (k < 13) v += f[i][j][k + 1][w][x][y] * (13 - k) / (54 - sum); - if (w < 13) v += f[i][j][k][w + 1][x][y] * (13 - w) / (54 - sum); - if (x == 0) { - double t = INF; - for (int u = 1; u <= 4; u++) t = min(t, f[i][j][k][w][u][y] / (54 - sum)); - v += t; - } - if (y == 0) { - double t = INF; - for (int u = 1; u <= 4; u++) t = min(t, f[i][j][k][w][x][u] / (54 - sum)); - v += t; - } - } - - if (f[0][0][0][0][0][0] > 54) - printf("-1.000"); - else - printf("%.3lf", f[0][0][0][0][0][0]); - - return 0; -} \ No newline at end of file diff --git a/TangDou/AcWing/MinimalPath/1076.md b/TangDou/AcWing/MiniPath/1076.md similarity index 100% rename from TangDou/AcWing/MinimalPath/1076.md rename to TangDou/AcWing/MiniPath/1076.md diff --git a/TangDou/AcWing/MinimalPath/1076_1.cpp b/TangDou/AcWing/MiniPath/1076_1.cpp similarity index 100% rename from TangDou/AcWing/MinimalPath/1076_1.cpp rename to TangDou/AcWing/MiniPath/1076_1.cpp diff --git a/TangDou/AcWing/MinimalPath/1076_2.cpp b/TangDou/AcWing/MiniPath/1076_2.cpp similarity index 100% rename from TangDou/AcWing/MinimalPath/1076_2.cpp rename to TangDou/AcWing/MiniPath/1076_2.cpp diff --git a/TangDou/AcWing/MinimalPath/1100.cpp b/TangDou/AcWing/MiniPath/1100.cpp similarity index 100% rename from TangDou/AcWing/MinimalPath/1100.cpp rename to TangDou/AcWing/MiniPath/1100.cpp diff --git a/TangDou/AcWing/MinimalPath/1100.md b/TangDou/AcWing/MiniPath/1100.md similarity index 100% rename from TangDou/AcWing/MinimalPath/1100.md rename to TangDou/AcWing/MiniPath/1100.md diff --git a/TangDou/AcWing/MinimalPath/1129.md b/TangDou/AcWing/MiniPath/1129.md similarity index 100% rename from TangDou/AcWing/MinimalPath/1129.md rename to TangDou/AcWing/MiniPath/1129.md diff --git a/TangDou/AcWing/MinimalPath/188.cpp b/TangDou/AcWing/MiniPath/188.cpp similarity index 100% rename from TangDou/AcWing/MinimalPath/188.cpp rename to TangDou/AcWing/MiniPath/188.cpp diff --git a/TangDou/AcWing/MinimalPath/188.md b/TangDou/AcWing/MiniPath/188.md similarity index 100% rename from TangDou/AcWing/MinimalPath/188.md rename to TangDou/AcWing/MiniPath/188.md diff --git a/TangDou/AcWing/MinimalPath/188.xlsx b/TangDou/AcWing/MiniPath/188.xlsx similarity index 100% rename from TangDou/AcWing/MinimalPath/188.xlsx rename to TangDou/AcWing/MiniPath/188.xlsx diff --git a/TangDou/AcWing/MinimalPath/CSP_J2.md b/TangDou/AcWing/MiniPath/CSP_J2.md similarity index 100% rename from TangDou/AcWing/MinimalPath/CSP_J2.md rename to TangDou/AcWing/MiniPath/CSP_J2.md diff --git a/TangDou/AcWing/MinimalPath/P4568.md b/TangDou/AcWing/MiniPath/P4568.md similarity index 100% rename from TangDou/AcWing/MinimalPath/P4568.md rename to TangDou/AcWing/MiniPath/P4568.md diff --git a/TangDou/AcWing/MinimalPath/P4568_1.cpp b/TangDou/AcWing/MiniPath/P4568_1.cpp similarity index 100% rename from TangDou/AcWing/MinimalPath/P4568_1.cpp rename to TangDou/AcWing/MiniPath/P4568_1.cpp diff --git a/TangDou/AcWing/MinimalPath/P4568_2.cpp b/TangDou/AcWing/MiniPath/P4568_2.cpp similarity index 100% rename from TangDou/AcWing/MinimalPath/P4568_2.cpp rename to TangDou/AcWing/MiniPath/P4568_2.cpp diff --git a/TangDou/AcWing/MinimalPath/POJ上的奇奇怪怪的CompileError.md b/TangDou/AcWing/MiniPath/POJ上的奇奇怪怪的CompileError.md similarity index 100% rename from TangDou/AcWing/MinimalPath/POJ上的奇奇怪怪的CompileError.md rename to TangDou/AcWing/MiniPath/POJ上的奇奇怪怪的CompileError.md diff --git a/TangDou/AcWing/MinimalPath/SPFA与BFS的区别.md b/TangDou/AcWing/MiniPath/SPFA与BFS的区别.md similarity index 100% rename from TangDou/AcWing/MinimalPath/SPFA与BFS的区别.md rename to TangDou/AcWing/MiniPath/SPFA与BFS的区别.md diff --git a/TangDou/AcWing/MinimalPath/Test.in b/TangDou/AcWing/MiniPath/Test.in similarity index 100% rename from TangDou/AcWing/MinimalPath/Test.in rename to TangDou/AcWing/MiniPath/Test.in diff --git a/TangDou/AcWing/MinimalPath/Test.out b/TangDou/AcWing/MiniPath/Test.out similarity index 100% rename from TangDou/AcWing/MinimalPath/Test.out rename to TangDou/AcWing/MiniPath/Test.out diff --git a/TangDou/AcWing/MinimalPath/Test1.cpp b/TangDou/AcWing/MiniPath/Test1.cpp similarity index 100% rename from TangDou/AcWing/MinimalPath/Test1.cpp rename to TangDou/AcWing/MiniPath/Test1.cpp diff --git a/TangDou/AcWing/MinimalPath/Test2.cpp b/TangDou/AcWing/MiniPath/Test2.cpp similarity index 100% rename from TangDou/AcWing/MinimalPath/Test2.cpp rename to TangDou/AcWing/MiniPath/Test2.cpp diff --git a/TangDou/AcWing/MinimalPath/Test2.in b/TangDou/AcWing/MiniPath/Test2.in similarity index 100% rename from TangDou/AcWing/MinimalPath/Test2.in rename to TangDou/AcWing/MiniPath/Test2.in diff --git a/TangDou/AcWing/MinimalPath/Test2.out b/TangDou/AcWing/MiniPath/Test2.out similarity index 100% rename from TangDou/AcWing/MinimalPath/Test2.out rename to TangDou/AcWing/MiniPath/Test2.out diff --git a/TangDou/AcWing/MinimalPath/关于优先队列priority_queue大小根堆、重载操作符的说明.md b/TangDou/AcWing/MiniPath/关于优先队列priority_queue大小根堆、重载操作符的说明.md similarity index 100% rename from TangDou/AcWing/MinimalPath/关于优先队列priority_queue大小根堆、重载操作符的说明.md rename to TangDou/AcWing/MiniPath/关于优先队列priority_queue大小根堆、重载操作符的说明.md diff --git a/TangDou/AcWing/MinimalPath/383.cpp b/TangDou/AcWing/MinimalPath/383.cpp deleted file mode 100644 index c182de6..0000000 --- a/TangDou/AcWing/MinimalPath/383.cpp +++ /dev/null @@ -1,89 +0,0 @@ -#include -using namespace std; - -const int N = 1010; -const int M = 10010; -int n, m; - -int dist[N][2]; -int cnt[N][2]; -bool st[N][2]; - -// 链式前向星 -int e[M], h[N], idx, w[M], ne[M]; -void add(int a, int b, int c = 0) { - e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++; -} - -// 本题需要一个三个属性的对象:最短距离d,最短、次短k,id:节点号 -struct Node { - int d, k, id; - // 小顶堆需要重载大于号,大顶堆需要重载小于号 - bool const operator>(Node b) const { - return d > b.d; - } -}; - -void dijkstra(int S) { - memset(dist, 0x3f, sizeof dist); - memset(st, false, sizeof st); - memset(cnt, 0, sizeof cnt); - priority_queue, greater<>> pq; // 小顶堆 - dist[S][0] = 0; - cnt[S][0] = 1; - pq.push({0, 0, S}); - - while (pq.size()) { - auto t = pq.top(); - pq.pop(); - int u = t.id; - int k = t.k; - - if (st[u][k]) continue; - st[u][k] = true; - - for (int i = h[u]; ~i; i = ne[i]) { - int v = e[i]; - int d = dist[u][k] + w[i]; - - if (dist[v][0] > d) { // 比最短路还要短 - dist[v][1] = dist[v][0]; // 最短降为次短 - cnt[v][1] = cnt[v][0]; // 次短路数量被更新 - pq.push({dist[v][1], 1, v}); // 次短被更新,次短入队列 - - dist[v][0] = d; // 替换最短路 - cnt[v][0] = cnt[u][k]; // 替换最短路数量 - pq.push({dist[v][0], 0, v}); // 最短路入队列 - } else if (dist[v][0] == d) // 增加最短路的数量 - cnt[v][0] += cnt[u][k]; - else if (dist[v][1] > d) { // 替换次短路 - dist[v][1] = d; - cnt[v][1] = cnt[u][k]; - pq.push({dist[v][1], 1, v}); // 次短路入队列 - } else if (dist[v][1] == d) - cnt[v][1] += cnt[u][k]; - } - } -} -int main() { - int T; - scanf("%d", &T); - while (T--) { - scanf("%d %d", &n, &m); - memset(h, -1, sizeof h); - idx = 0; - while (m--) { - int a, b, c; - scanf("%d %d %d", &a, &b, &c); - add(a, b, c); - } - int S, F; - scanf("%d %d", &S, &F); - dijkstra(S); - int ans = cnt[F][0]; // 最短路 - // 在正常处理完最短路和次短路后,在最后的逻辑中,增加本题的中特殊要求部分 - if (dist[F][0] == dist[F][1] - 1) ans += cnt[F][1]; - printf("%d\n", ans); - } - return 0; -} \ No newline at end of file diff --git a/TangDou/AcWing/MinimalPath/POJ3464.cpp b/TangDou/AcWing/MinimalPath/POJ3464.cpp deleted file mode 100644 index fb9b641..0000000 --- a/TangDou/AcWing/MinimalPath/POJ3464.cpp +++ /dev/null @@ -1,102 +0,0 @@ -#include -#include -#include -#include -using namespace std; -#define x first -#define y second - -const int N = 1e3 + 13; -const int M = 1e6 + 10; -int n, m, u, v, s, f; -int dist[N][2], cnt[N][2]; -bool st[N][2]; - -//链式前向星 -int e[M], h[N], idx, w[M], ne[M]; -void add(int a, int b, int c = 0) { - e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++; -} - -struct Node { - // u: 节点号 - // d:目前结点v的路径长度 - // k:是最短路0还是次短路1 - int u, d, k; - // POJ中结构体,没有构造函数,直接报编译错误 - Node(int u, int d, int k) { - this->u = u, this->d = d, this->k = k; - } - const bool operator<(Node x) const { - return d > x.d; - } -}; - -void dijkrsta() { - priority_queue q; //通过定义结构体小于号,实现小顶堆 - memset(dist, 0x3f, sizeof(dist)); //清空最小距离与次小距离数组 - memset(cnt, 0, sizeof(cnt)); //清空最小距离路线个数与次小距离路线个数数组 - memset(st, 0, sizeof(st)); //清空是否出队过数组 - - cnt[s][0] = 1; //起点s,0:最短路,1:有一条 - cnt[s][1] = 0; //次短路,路线数为0 - - dist[s][0] = 0; //最短路从s出发到s的距离是0 - dist[s][1] = 0; //次短路从s出发到s的距离是0 - - q.push(Node(s, 0, 0)); //入队列 - - while (q.size()) { - Node x = q.top(); - q.pop(); - - int u = x.u, k = x.k, d = x.d; - - if (st[u][k]) continue; //① - st[u][k] = true; - - for (int i = h[u]; ~i; i = ne[i]) { - int j = e[i]; - int dj = d + w[i]; //原长度+到节点j的边长 - - if (dj == dist[j][0]) //与到j的最短长度相等,则更新路径数量 - cnt[j][0] += cnt[u][k]; - else if (dj < dist[j][0]) { //找到更小的路线,需要更新 - dist[j][1] = dist[j][0]; //次短距离被最短距离覆盖 - cnt[j][1] = cnt[j][0]; //次短个数被最短个数覆盖 - - dist[j][0] = dj; //更新最短距离 - cnt[j][0] = cnt[u][k]; //更新最短个数 - - q.push(Node(j, dist[j][1], 1)); //② - q.push(Node(j, dist[j][0], 0)); - } else if (dj == dist[j][1]) //如果等于次短 - cnt[j][1] += cnt[u][k]; //更新次短的方案数,累加 - else if (dj < dist[j][1]) { //如果大于最短,小于次短,两者中间 - dist[j][1] = dj; //更新次短距离 - cnt[j][1] = cnt[u][k]; //更新次短方案数 - q.push(Node(j, dist[j][1], 1)); //次短入队列 - } - } - } -} -int main() { - int T; - scanf("%d", &T); - while (T--) { - memset(h, -1, sizeof h); - scanf("%d %d", &n, &m); - while (m--) { - int a, b, c; - scanf("%d %d %d", &a, &b, &c); - add(a, b, c); - } - //起点和终点 - scanf("%d %d", &s, &f); - //计算最短路 - dijkrsta(); - //输出 - printf("%d\n", cnt[f][0] + (dist[f][1] == dist[f][0] + 1 ? cnt[f][1] : 0)); - } - return 0; -} diff --git a/TangDou/AcWing/Floyd/1125.cpp b/TangDou/AcWing_TiGao/T3/Floyd/1125.cpp similarity index 56% rename from TangDou/AcWing/Floyd/1125.cpp rename to TangDou/AcWing_TiGao/T3/Floyd/1125.cpp index bd2dba8..36254ae 100644 --- a/TangDou/AcWing/Floyd/1125.cpp +++ b/TangDou/AcWing_TiGao/T3/Floyd/1125.cpp @@ -1,15 +1,18 @@ #include using namespace std; + +typedef pair PII; #define x first #define y second -typedef pair PII; + const int N = 160; const int INF = 0x3f3f3f3f; - -PII q[N]; // 每个点的坐标 -char g[N][N]; // 邻接矩阵,记录是否中间有边 -double dist[N][N]; // 每两个牧区之间的距离 -double maxd[N]; // 距离牧区i最远的最短距离是多少 +PII q[N]; // 每个点的坐标 +char g[N][N]; // 邻接矩阵,记录是否中间有边 +double dis[N][N]; // 每两个牧区(点)之间的距离 +double maxd[N]; // maxd[i]:由i点出发,可以到达的最远的最短距离是多少 +// Q:什么是最远的最短距离? +// 答:举个不太恰当的例子,比如A->B->C->D,边权都是1 ,同时存在一条A->D,边权是1。此时,有短的不取长的,所以A->D的距离是1,不是3。 // 欧几里得距离 double get(PII a, PII b) { @@ -25,49 +28,50 @@ int main() { // 邻接矩阵,描述点与点之间的连通关系 // 这个用int还没法读入,因为它的输入是连续的,中间没有空格,讨厌啊~ + // 字符数组与scanf("%s",g[i])相结合,直接写入二维数组g的每一行上,这个技巧是值得我们学习的。 for (int i = 0; i < n; i++) scanf("%s", g[i]); // 遍历行与列,计算出每两个点之间的距离 // ① 距离只在同一连通块中存在,不同的连通块间的距离是INF // ② 自己与自己的距离是0 // ③ 两个牧区相连,距离=sqrt((x1-x2)^2+(y1-y2)^2) - // 本质: g + q => dist - for (int i = 0; i < n; i++) { + // 本质: g + q => dis + for (int i = 0; i < n; i++) for (int j = 0; j < n; j++) { // 1. double数组,在全局变量区,默认值是0 // 2. 当i==j时,自己到自己的距离是0,所以没动作,直接使用默认值,即d[i][i]=0,自己到自己没有距离 // 3. 当g[i][j]=='1'时,说明两者之间存在一条边,距离就是欧几里得距离计算办法 // 4. 否则就是没有路径 if (i == j) - dist[i][j] = 0; + dis[i][j] = 0; else if (g[i][j] == '1') - dist[i][j] = get(q[i], q[j]); - else // 注意:由于dist数组是一个double类型,不能用memset(0x3f)进行初始化正无穷 - dist[i][j] = INF; + dis[i][j] = get(q[i], q[j]); + else // 注意:由于dis数组是一个double类型,不能用memset(0x3f)进行初始化正无穷 + dis[i][j] = INF; } - } // ① Floyd算法 k,i,j // 原始各连通块内的多源最短路径 for (int k = 0; k < n; k++) for (int i = 0; i < n; i++) for (int j = 0; j < n; j++) - dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]); + dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]); - // ② 未建设两个连通块之间线路前,每个点的最长 最短路径 + // ② (1)求出未建设两个连通块之间线路前,所有连通块的直径最大值res1 + // (2)求出未建设两个连通块之间线路前,每个点的可以到达的最远最短距离,下一步做模拟连线时会用到 double res1 = 0; for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) // 求i到离i(最短路径) 最长距离 - if (dist[i][j] < INF) maxd[i] = max(maxd[i], dist[i][j]); - // res1保持原来(两个牧场中)任意两个牧区间的最大距离(直径) + if (dis[i][j] < INF) maxd[i] = max(maxd[i], dis[i][j]); + // 所有点的最远距离PK,获取所有连通块的最大直径 res1 = max(res1, maxd[i]); } - // ③ 连线操作,更新新牧场直径 + // ③ 模拟连线操作,看看这样连线后生成的新牧场直径会不会刷新原来的记录 double res2 = INF; for (int i = 0; i < n; i++) for (int j = 0; j < n; j++) - if (dist[i][j] == INF) // 如果i,j不在同一个连通块内 + if (dis[i][j] == INF) // 如果i,j不在同一个连通块内 // 连接原来不在同一连通块中的两个点后,可以取得的最小直径 res2 = min(res2, maxd[i] + maxd[j] + get(q[i], q[j])); // PK一下 diff --git a/TangDou/AcWing/Floyd/1125.drawio b/TangDou/AcWing_TiGao/T3/Floyd/1125.drawio similarity index 100% rename from TangDou/AcWing/Floyd/1125.drawio rename to TangDou/AcWing_TiGao/T3/Floyd/1125.drawio diff --git a/TangDou/AcWing/Floyd/1125.md b/TangDou/AcWing_TiGao/T3/Floyd/1125.md similarity index 80% rename from TangDou/AcWing/Floyd/1125.md rename to TangDou/AcWing_TiGao/T3/Floyd/1125.md index 190b864..a62d048 100644 --- a/TangDou/AcWing/Floyd/1125.md +++ b/TangDou/AcWing_TiGao/T3/Floyd/1125.md @@ -138,16 +138,19 @@ maxd[i] + maxd[j] + get(q[i], q[j]) ```cpp {.line-numbers} #include using namespace std; + +typedef pair PII; #define x first #define y second -typedef pair PII; + const int N = 160; const int INF = 0x3f3f3f3f; - -PII q[N]; // 每个点的坐标 -char g[N][N]; // 邻接矩阵,记录是否中间有边 -double dist[N][N]; // 每两个牧区之间的距离 -double maxd[N]; // 距离牧区i最远的最短距离是多少 +PII q[N]; // 每个点的坐标 +char g[N][N]; // 邻接矩阵,记录是否中间有边 +double dis[N][N]; // 每两个牧区(点)之间的距离 +double maxd[N]; // maxd[i]:由i点出发,可以到达的最远的最短距离是多少 +// Q:什么是最远的最短距离? +// 答:举个不太恰当的例子,比如A->B->C->D,边权都是1 ,同时存在一条A->D,边权是1。此时,有短的不取长的,所以A->D的距离是1,不是3。 // 欧几里得距离 double get(PII a, PII b) { @@ -163,49 +166,50 @@ int main() { // 邻接矩阵,描述点与点之间的连通关系 // 这个用int还没法读入,因为它的输入是连续的,中间没有空格,讨厌啊~ + // 字符数组与scanf("%s",g[i])相结合,直接写入二维数组g的每一行上,这个技巧是值得我们学习的。 for (int i = 0; i < n; i++) scanf("%s", g[i]); // 遍历行与列,计算出每两个点之间的距离 // ① 距离只在同一连通块中存在,不同的连通块间的距离是INF // ② 自己与自己的距离是0 // ③ 两个牧区相连,距离=sqrt((x1-x2)^2+(y1-y2)^2) - // 本质: g + q => dist - for (int i = 0; i < n; i++) { + // 本质: g + q => dis + for (int i = 0; i < n; i++) for (int j = 0; j < n; j++) { // 1. double数组,在全局变量区,默认值是0 // 2. 当i==j时,自己到自己的距离是0,所以没动作,直接使用默认值,即d[i][i]=0,自己到自己没有距离 // 3. 当g[i][j]=='1'时,说明两者之间存在一条边,距离就是欧几里得距离计算办法 // 4. 否则就是没有路径 if (i == j) - dist[i][j] = 0; + dis[i][j] = 0; else if (g[i][j] == '1') - dist[i][j] = get(q[i], q[j]); - else // 注意:由于dist数组是一个double类型,不能用memset(0x3f)进行初始化正无穷 - dist[i][j] = INF; + dis[i][j] = get(q[i], q[j]); + else // 注意:由于dis数组是一个double类型,不能用memset(0x3f)进行初始化正无穷 + dis[i][j] = INF; } - } // ① Floyd算法 k,i,j // 原始各连通块内的多源最短路径 for (int k = 0; k < n; k++) for (int i = 0; i < n; i++) for (int j = 0; j < n; j++) - dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]); + dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]); - // ② 未建设两个连通块之间线路前,每个点的最长 最短路径 + // ② (1)求出未建设两个连通块之间线路前,所有连通块的直径最大值res1 + // (2)求出未建设两个连通块之间线路前,每个点的可以到达的最远最短距离,下一步做模拟连线时会用到 double res1 = 0; for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) // 求i到离i(最短路径) 最长距离 - if (dist[i][j] < INF) maxd[i] = max(maxd[i], dist[i][j]); - // res1保持原来(两个牧场中)任意两个牧区间的最大距离(直径) + if (dis[i][j] < INF) maxd[i] = max(maxd[i], dis[i][j]); + // 所有点的最远距离PK,获取所有连通块的最大直径 res1 = max(res1, maxd[i]); } - // ③ 连线操作,更新新牧场直径 + // ③ 模拟连线操作,看看这样连线后生成的新牧场直径会不会刷新原来的记录 double res2 = INF; for (int i = 0; i < n; i++) for (int j = 0; j < n; j++) - if (dist[i][j] == INF) // 如果i,j不在同一个连通块内 + if (dis[i][j] == INF) // 如果i,j不在同一个连通块内 // 连接原来不在同一连通块中的两个点后,可以取得的最小直径 res2 = min(res2, maxd[i] + maxd[j] + get(q[i], q[j])); // PK一下 diff --git a/TangDou/AcWing_TiGao/T3/Floyd/343.eddx b/TangDou/AcWing_TiGao/T3/Floyd/343.eddx new file mode 100644 index 0000000..303043a Binary files /dev/null and b/TangDou/AcWing_TiGao/T3/Floyd/343.eddx differ diff --git a/TangDou/AcWing_TiGao/T3/Floyd/343.md b/TangDou/AcWing_TiGao/T3/Floyd/343.md new file mode 100644 index 0000000..90f283a --- /dev/null +++ b/TangDou/AcWing_TiGao/T3/Floyd/343.md @@ -0,0 +1,340 @@ +## [$AcWing$ $343$. 排序](https://www.acwing.com/problem/content/345/) + +### 一、题目描述 +给定 $n$ 个变量和 $m$ 个不等式。其中 $n$ 小于等于 $26$,变量分别用前 $n$ 的大写英文字母表示。 + +不等式之间具有传递性,即若 $A>B$ 且 $B>C$,则 $A>C$。 + +请从前往后遍历每对关系,每次遍历时判断: + +* 如果能够确定全部关系且无矛盾,则结束循环,输出确定的次序; +* 如果发生矛盾,则结束循环,输出有矛盾; +* 如果循环结束时没有发生上述两种情况,则输出无定解。 + +**输入格式** +输入包含多组测试数据。 + +每组测试数据,第一行包含两个整数 $n$ 和 $m$。 + +接下来 $m$ 行,每行包含一个不等式,不等式全部为 **小于** 关系。 + +当输入一行 `0 0` 时,表示输入终止。 + +**输出格式** +每组数据输出一个占一行的结果。 + +结果可能为下列三种之一: + +* 如果可以确定两两之间的关系,则输出 `Sorted sequence determined after t relations: yyy...y.`,其中`t`指 **迭代次数**,`yyy...y`是指 **升序排列** 的所有变量。 + +* 如果有矛盾,则输出: `Inconsistency found after t relations.`,其中`t`指迭代次数。 + +* 如果没有矛盾,且不能确定两两之间的关系,则输出 `Sorted sequence cannot be determined.`。 + +**数据范围** +$2≤n≤26$,变量只可能为大写字母 $A$∼$Z$。 + +**输入样例$1$**: +```cpp {.line-numbers} +4 6 +A**解释**:比如$a < b,b < c$,就可以推导出$a < c$,如果用图形表示出这种大小关系,就是$a$到$b$有一条有向边 【小的向大的连一条边】,$b$到$c$有一条有向边,可以推出$a$可以到达$c$,找出图中各点能够到达点的集合,**类似** 于$floyd$算法求图中任意两点间的最短距离 【魔改版的$floyd$】。 + +**模板** +```cpp {.line-numbers} +//传递闭包 +void floyd(){ + for(int k = 0;k < n;k++) + for(int i = 0;i < n;i++) + for(int j = 0;j < n;j++) + f[i][j] |= f[i][k] & f[k][j]; +} +// 原始版本 +/* +for (int k = 0; k < n; k++) + for (int i = 0; i < n; i++) + for (int j = 0; j < n; j++) + d[i][j] = min(d[i][j], d[i][k] + d[k][j]); +*/ +``` + +**回到本题** + +- 题目描述要求按顺序遍历二元关系,一旦前$i$个二元关系可以确定次序了就不再遍历了,即使第$i + 1$对二元关系就会出现矛盾也不去管它了。 +- 题目字母只会在$A$到$Z$间,因此可以映射为$0$到$25$这$26$个元素 +- $A < B$,则$f[0][1]=1$。如果$f[0][1] = f[1][0] = 1$,推出$f[0][0] = 1$,此时$A < B$并且$B < A$发生矛盾,即$f[i][i]= 1$时表示发生矛盾。 + +**算法步骤** +每读取一对二元关系,就执行一遍$floyd$算法求 **传递闭包**,然后执行$check$函数判断: +* ① 如果发生矛盾终止遍历 +* ② 如果次序全部被确定终止遍历 +* ③ 两者都没有,继续遍历 + +在确定所有的次序后,需要 **输出大小关系**,需要一个$getorder$函数。 + +>**注意**: +终止遍历仅仅是不再针对新增的二元关系去求传递闭包,循环还是要继续的,需要读完数据才能继续读下一组数据。 + +下面设计$check$函数和$getorder$函数。 +```cpp {.line-numbers} +// 1:可以确定两两之间的关系,2:矛盾,3:不能确定两两之间的关系 +int check() { + // 如果i **解释**:确定所有元素次序后如何判断元素`i`在第几个位置呢?`f[i][j] = 1`表示`i < j`,因此计算下`i`小于元素的个数`cnt`,就可以判定`i`是第`cnt + 1`大的元素了 + + +#### $Code$ $O(N^3)$ +```cpp {.line-numbers} +#include +using namespace std; + +const int N = 26; + +int n, m; +int g[N][N]; +bool st[N]; + +// 求传递闭包 +void floyd() { + for (int k = 0; k < n; k++) + for (int i = 0; i < n; i++) + for (int j = 0; j < n; j++) + g[i][j] |= g[i][k] && g[k][j]; +} + +int check() { + for (int i = 0; i < n; i++) + if (g[i][i]) return 2; // 矛盾 + + for (int i = 0; i < n; i++) + for (int j = 0; j < i; j++) + if (!g[i][j] && !g[j][i]) // 待继续 + return 0; + + return 1; // 找到顺序 +} + +string getorder() { // 升序输出所有变量 + char s[26]; + for (int i = 0; i < n; i++) { + int cnt = 0; + // f[i][j] = 1表示i可以到达j (i< j) + for (int j = 0; j < n; j++) cnt += g[i][j]; // 比i大的有多少个 + // 举个栗子:i=0,表示字符A + // 比如比i大的有5个,共6个字符:ABCDEF + // n - cnt - 1 = 6-5-1 = 0,也就是A放在第一个输出的位置上, 之所以再-1,是因为下标从0开始 + s[n - cnt - 1] = i + 'A'; + } + + // 转s字符数组为字符串 + string res; + for (int i = 0; i < n; i++) res = res + s[i]; + return res; +} + +int main() { + while (cin >> n >> m, n || m) { + memset(g, 0, sizeof g); // 邻接矩阵 + int type = 0, t; // type: 0=还需要继续给出条件 1=找到了顺序 2=存在冲突 + // t:在第几次输入后找到了顺序,不能中间break,因为那样会造成数据无法完成读入,后续的操作无法进行,只能记录下来当时的i + for (int i = 1; i <= m; i++) { + char s[5]; + cin >> s; + int a = s[0] - 'A', b = s[2] - 'A'; // A->0,B->1,...,Z->25完成映射关系 + + if (!type) { // 如果不存在矛盾,就尝试找出大小的顺序 + g[a][b] = 1; // 有边 + floyd(); // 求传递闭包 + type = check(); // 检查是不是存在矛盾,或者找到了完整的顺序 + if (type > 0) t = i; // 如果找到了顺序,或者发现了矛盾,记录是第几次输入后发现的 + } + // 即使存在矛盾,也需要继续读入,直到本轮数据读入完成 + } + + if (!type) + puts("Sorted sequence cannot be determined."); + else if (type == 2) + printf("Inconsistency found after %d relations.\n", t); + else { + string ans = getorder(); // 输出升序排列的所有变量 + printf("Sorted sequence determined after %d relations: %s.\n", t, ans.c_str()); + } + } + return 0; +} +``` + +### 三、优化版本 +$O(N^2)$ + +其实,由于每次新增加的一对$(a,b)$,只会更新与$a,b$有边连接的点,其它的无关点是没有影响的,如果加上一对$(a,b)$就去全新计算,无疑是存在浪费的,可以优化的。 + +怎么优化呢?核心思路就是$(a,b)$做为$floyd$算法的中继点即可,其它点不再被遍历做为中继点。 + +说人话就是: +① 遍历所有节点,找出所有小于$a$的节点$x$,那么$x$一定小于$b$。 +② 遍历所有节点,找出所有大于$b$的节点$x$,那么$a$一定小于$x$。 +③ 遍历所有节点,如果$x +using namespace std; + +const int N = 26; + +int n, m; +bool g[N][N]; +bool st[N]; + +int check() { + for (int i = 0; i < n; i++) + if (g[i][i]) return 2; // 矛盾 + + for (int i = 0; i < n; i++) + for (int j = 0; j < i; j++) + if (!g[i][j] && !g[j][i]) // 待继续 + return 0; + + return 1; // 找到顺序 +} +string getorder() { // 升序输出所有变量 + char s[26]; + for (int i = 0; i < n; i++) { + int cnt = 0; + // f[i][j] = 1表示i可以到达j (i< j) + for (int j = 0; j < n; j++) cnt += g[i][j]; // 比i大的有多少个 + // 举个栗子:i=0,表示字符A + // 比如比i大的有5个,共6个字符:ABCDEF + // n - cnt - 1 = 6-5-1 = 0,也就是A放在第一个输出的位置上, 之所以再-1,是因为下标从0开始 + s[n - cnt - 1] = i + 'A'; + } + + // 转s字符数组为字符串 + string res; + for (int i = 0; i < n; i++) res = res + s[i]; + return res; +} + +int main() { + while (cin >> n >> m, n || m) { + memset(g, 0, sizeof g); + + int type = 0, t; + for (int i = 1; i <= m; i++) { + char str[5]; + cin >> str; + int a = str[0] - 'A', b = str[2] - 'A'; + + // a +using namespace std; + +const int N = 26; + +int n, m; +int g[N][N]; +bool st[N]; + +// 求传递闭包 +void floyd() { + for (int k = 0; k < n; k++) + for (int i = 0; i < n; i++) + for (int j = 0; j < n; j++) + g[i][j] |= g[i][k] && g[k][j]; +} + +int check() { + for (int i = 0; i < n; i++) + if (g[i][i]) return 2; // 矛盾 + + for (int i = 0; i < n; i++) + for (int j = 0; j < i; j++) + if (!g[i][j] && !g[j][i]) // 待继续 + return 0; + + return 1; // 找到顺序 +} + +string getorder() { // 升序输出所有变量 + char s[26]; + for (int i = 0; i < n; i++) { + int cnt = 0; + // f[i][j] = 1表示i可以到达j (i< j) + for (int j = 0; j < n; j++) cnt += g[i][j]; // 比i大的有多少个 + // 举个栗子:i=0,表示字符A + // 比如比i大的有5个,共6个字符:ABCDEF + // n - cnt - 1 = 6-5-1 = 0,也就是A放在第一个输出的位置上, 之所以再-1,是因为下标从0开始 + s[n - cnt - 1] = i + 'A'; + } + + // 转s字符数组为字符串 + string res; + for (int i = 0; i < n; i++) res = res + s[i]; + return res; +} + +int main() { + while (cin >> n >> m, n || m) { + memset(g, 0, sizeof g); // 邻接矩阵 + int type = 0, t; // type: 0=还需要继续给出条件 1=找到了顺序 2=存在冲突 + // t:在第几次输入后找到了顺序,不能中间break,因为那样会造成数据无法完成读入,后续的操作无法进行,只能记录下来当时的i + for (int i = 1; i <= m; i++) { + char s[5]; + cin >> s; + int a = s[0] - 'A', b = s[2] - 'A'; // A->0,B->1,...,Z->25完成映射关系 + + if (!type) { // 如果不存在矛盾,就尝试找出大小的顺序 + g[a][b] = 1; // 有边 + floyd(); // 求传递闭包 + type = check(); // 检查是不是存在矛盾,或者找到了完整的顺序 + if (type > 0) t = i; // 如果找到了顺序,或者发现了矛盾,记录是第几次输入后发现的 + } + // 即使存在矛盾,也需要继续读入,直到本轮数据读入完成 + } + + if (!type) + puts("Sorted sequence cannot be determined."); + else if (type == 2) + printf("Inconsistency found after %d relations.\n", t); + else { + string ans = getorder(); // 输出升序排列的所有变量 + printf("Sorted sequence determined after %d relations: %s.\n", t, ans.c_str()); + } + } + return 0; +} \ No newline at end of file diff --git a/TangDou/AcWing_TiGao/T3/Floyd/343_2.cpp b/TangDou/AcWing_TiGao/T3/Floyd/343_2.cpp new file mode 100644 index 0000000..abfa472 --- /dev/null +++ b/TangDou/AcWing_TiGao/T3/Floyd/343_2.cpp @@ -0,0 +1,75 @@ +#include +using namespace std; + +const int N = 26; + +int n, m; +bool g[N][N]; +bool st[N]; + +int check() { + for (int i = 0; i < n; i++) + if (g[i][i]) return 2; // 矛盾 + + for (int i = 0; i < n; i++) + for (int j = 0; j < i; j++) + if (!g[i][j] && !g[j][i]) // 待继续 + return 0; + + return 1; // 找到顺序 +} +string getorder() { // 升序输出所有变量 + char s[26]; + for (int i = 0; i < n; i++) { + int cnt = 0; + // f[i][j] = 1表示i可以到达j (i< j) + for (int j = 0; j < n; j++) cnt += g[i][j]; // 比i大的有多少个 + // 举个栗子:i=0,表示字符A + // 比如比i大的有5个,共6个字符:ABCDEF + // n - cnt - 1 = 6-5-1 = 0,也就是A放在第一个输出的位置上, 之所以再-1,是因为下标从0开始 + s[n - cnt - 1] = i + 'A'; + } + + // 转s字符数组为字符串 + string res; + for (int i = 0; i < n; i++) res = res + s[i]; + return res; +} + +int main() { + while (cin >> n >> m, n || m) { + memset(g, 0, sizeof g); + + int type = 0, t; + for (int i = 1; i <= m; i++) { + char str[5]; + cin >> str; + int a = str[0] - 'A', b = str[2] - 'A'; + + // a + +using namespace std; +const int N = 110, INF = 0x3f3f3f3f; +int n, m; +int g[N][N], dis[N][N]; +vector path; +int mid[N][N]; +int ans = INF; + +// i->j之间的最短路径中途经点有哪些 +void get_path(int i, int j) { + int k = mid[i][j]; // 获取中间转移点 + if (!k) return; // 如果i,j之间没有中间点,停止 + get_path(i, k); // 递归前半段 + path.push_back(k); // 记录k节点 + get_path(k, j); // 递归后半段 +} + +int main() { + // n个顶点,m条边 + cin >> n >> m; + + // 初始化邻接矩阵 + memset(g, 0x3f, sizeof g); + for (int i = 1; i <= n; i++) g[i][i] = 0; // 邻接矩阵,自己到自己距离是0 + + while (m--) { + int a, b, c; + cin >> a >> b >> c; + g[a][b] = g[b][a] = min(g[a][b], c); // 求最短路之类,(a,b)之间多条边输入只保留最短边 + } + + // 把原始地图复制出来到生成最短距离dis + memcpy(dis, g, sizeof dis); + + for (int k = 1; k <= n; k++) { + // DP + for (int i = 1; i < k; i++) + for (int j = i + 1; j < k; j++) + if (g[i][k] + g[k][j] < ans - dis[i][j]) { // 减法防止爆INT + ans = dis[i][j] + g[i][k] + g[k][j]; + // 包含最小环的所有节点(按顺序输出) + // 找到长度更小的环,需要记录路径,并且要求: 最小环的所有节点(按顺序输出) + path.clear(); // 每次找到新的最小环,那path就是为最小的环而准备的,以前的都作废掉 + path.push_back(i); // 序号:i < j < k + get_path(i, j); + path.push_back(j); + path.push_back(k); + } + + // 正常floyd + for (int i = 1; i <= n; i++) + for (int j = 1; j <= n; j++) + if (dis[i][j] > dis[i][k] + dis[k][j]) { + dis[i][j] = dis[i][k] + dis[k][j]; + mid[i][j] = k; // 记录路径i->j 是通过k进行转移的 + } + } + + if (ans == INF) + puts("No solution."); + else + for (int i = 0; i < path.size(); i++) cout << path[i] << ' '; + + return 0; +} \ No newline at end of file diff --git a/TangDou/AcWing_TiGao/T3/Floyd/344.md b/TangDou/AcWing_TiGao/T3/Floyd/344.md new file mode 100644 index 0000000..15b72de --- /dev/null +++ b/TangDou/AcWing_TiGao/T3/Floyd/344.md @@ -0,0 +1,152 @@ +## [$AcWing$ $344$. 观光之旅](https://www.acwing.com/problem/content/346/) + +### 一、题目描述 +给定一张无向图,求图中一个至少包含 $3$ 个点的环,环上的节点不重复,并且环上的边的长度之和最小。 + +该问题称为 **无向图的最小环问题**。 + +**你需要输出最小环的方案**,若最小环不唯一,输出任意一个均可。 + +**输入格式** +第一行包含两个整数 $N$ 和 $M$,表示无向图有 $N$ 个点,$M$ 条边。 + +接下来 $M$ 行,每行包含三个整数 $u,v,l$,表示点 $u$ 和点 $v$ 之间有一条边,边长为 $l$。 + +**输出格式** +输出占一行,包含最小环的所有节点(按顺序输出),如果不存在则输出 `No solution.`。 + +### 二、$floyd + dp$求最小环模板题 + +**最优化问题**,**从集合角度考虑($DP$)**,**将所有环按编号最大的点** 分成 $n$ 类,**求出每类最小**,最后在类间取 $min$ + +分类的标准是 **可重、不漏**。(对于求数量的问题,分类的标准是 **不重不漏**) + + +#### 集合划分 +![](https://cdn.acwing.com/media/article/image/2021/08/08/52559_50494c4bf7-%E5%B1%8F%E5%B9%95%E6%88%AA%E5%9B%BE-2021-08-08-101518.jpg) + +对于最大编号是 $k$ 的所有环,记点 $k$ 逆时针方向的前一点为 $i$,顺时针方向的下个点为 $j$。由于 $dis[i,k]=g[i,k], dis[k,j]=g[k,j]$ 为定值,要使整个环最小,就要使 $dis[i,j]$ 最小。 + + +$floyd$ 第一层循环到 $k$ 时的 $dis[i,j]$ 恰好是中间点只包含 $1\sim k−1$ 的最短距离。因此第 $k$ 类最小值可在此时得到。 + + +#### 状态表示 +![](https://cdn.acwing.com/media/article/image/2021/08/08/52559_1791f992f8-5.png) + + +#### 求方案 +$DP$ 求方案一般要 **记录转移前驱的所有维**。但 $floyd$ 转移方程中的 $k$ 表示路径的中间点,由于路径可以被两端和中间点覆盖,只要记下中间点,就能递归出路径。 + +### 三、$floyd+dp+$递归输出路径 +```cpp {.line-numbers} +#include +#include + +using namespace std; +const int N = 110, INF = 0x3f3f3f3f; +int n, m; +int g[N][N], d[N][N]; +int path[N], idx; +int mid[N][N]; + +void get_path(int i, int j) { + int k = mid[i][j]; //获取中间转移点 + if (!k) return; //如果i,j之间没有中间点,停止 + get_path(i, k); // i->k + path[idx++] = k; //记录k节点 + get_path(k, j); // k->j +} + +int main() { + cin >> n >> m; + + memset(g, 0x3f, sizeof g); + for (int i = 1; i <= n; i++) g[i][i] = 0; + + while (m--) { + int a, b, c; + cin >> a >> b >> c; + g[a][b] = g[b][a] = min(g[a][b], c); + } + + int ans = INF; + memcpy(d, g, sizeof d); + for (int k = 1; k <= n; k++) { + //插入DP计算 + /* + Q:为什么循环的时候i和j都需要小于k啊,Floyd不是只需要经过的点小于k就可以了吗 + A:只是为了避免经过相同的点,比如i == k时,三个点就变成两个点了。 + 其实循环到n也是可以的,不过当i, j, k中有两个相同时就要continue一下 + */ + for (int i = 1; i < k; i++) + for (int j = i + 1; j < k; j++) + + if (g[j][k] + g[k][i] < ans - d[i][j]) { + ans = d[i][j] + g[j][k] + g[k][i]; + + //找到更小的环,需要记录路径 + //最小环的所有节点(按顺序输出) + //下面的记录顺序很重要: + // 1. 上面的i,j枚举逻辑是j>i,所以i是第一个 + // 2. i->j 中间的路线不明,需要用get_path进行探索 + // 3. 记录j + // 4. 记录k + idx = 0; + path[idx++] = i; + get_path(i, j); // i是怎么到达j的?就是问dist[i,j]是怎么获取到的,这是在求最短路径过程中的一个路径记录问题 + path[idx++] = j; + path[idx++] = k; + } + + //正常的floyd + for (int i = 1; i <= n; i++) + for (int j = 1; j <= n; j++) + if (d[i][j] > d[i][k] + d[k][j]) { + d[i][j] = d[i][k] + d[k][j]; + mid[i][j] = k; //记录路径i->j 是通过k进行转移的 + } + } + + if (ans == INF) + puts("No solution."); + else + for (int i = 0; i < idx; i++) cout << path[i] << ' '; + + return 0; +} +``` + +### 四、关于三个$INF$相加爆$INT$的应对之道 +$Q1$:为什么这里是用$ans-dis[i,j]$,而不是写成 $ans> dis[i,j]+g[j,k]+g[k,i]$? +$A$: $g[j][k],g[k][i] ∈ l$,$l$是小于$500$的,所在 $g[j][k]+g[k][i]<1000$,肯定没问题 + $dis[i,j]$的初始值是$INF$,$g[i,j]$的初始值也是$INF$,如果都写在左边,如果$i,j,k$三者之间没有边,就是三个$INF$,累加和会爆掉$INT$,就会进入判断条件,错误. 而两个$INF$相加不会爆$INT$(想想松弛操作~) + +$Q2:(LL) dis[i][j] + g[j][k] + g[k][i] < ans$ 为什么是正确的?而 + $(LL) (dis[i][j] + g[j][k] + g[k][i]) < ans$为什么就是错误的? +$A$: + `INT_MAX = 2147483647` + `LONG LONG MAX=9223372036854775807ll` + + `INF = 0x3f3f3f3f = 1061109567` + `INF * 3 =1061109567 * 3 = 3183328701` 大于`INT_MAX`,即会爆`INT`,需要开`LONG LONG` + + `(LL)a + b + c` 将`a`转为`LL`,然后再加`b`加`c`,都是`LL+int`,在`LL`范围内,结果正确 + `(LL)(a + b + c)` 是先计算`a+b+c`,先爆`INT`,再转换`LL`,结果错误。 + +$Q3$: 所有数据全开$LL$为什么一样不对呢? +$A:$ +```c++ +memset(q, 0x3f, sizeof q); +cout << q[0] << endl; // 4557430888798830399 +cout << q[0] * 3 << endl; //-4774451407313060419 +``` + +因为问题出在$LL$的初始$memset$上,比如`memset(q,0x3f,sizeof q);` +此时,每个数组位置上的值是:$4557430888798830399$ +如果$i,j,k$三者之间没有关系,就会出现 类似于 `g[i,k]+g[k,j]+d[i,j]=3* 4557430888798830399`的情况,这个值太大,$LL$也装不下,值为`-4774451407313060419`,而此时$ans$等于$INF$,肯定满足小于条件,就进入了错误的判断逻辑。 + + +解决的办法有两种: +* `g[j][k] + g[k][i] < ans - dis[i][j]` 以减法避开三个$INF$相加,两个$INF$相加是$OK$的,不会爆$INT$ +* 将运算前的$dis[i][j]$转为$LL$,这样,三个$INF$不会爆$LL$ \ No newline at end of file diff --git a/TangDou/AcWing/Floyd/345.cpp b/TangDou/AcWing_TiGao/T3/Floyd/345.cpp similarity index 100% rename from TangDou/AcWing/Floyd/345.cpp rename to TangDou/AcWing_TiGao/T3/Floyd/345.cpp diff --git a/TangDou/AcWing/Floyd/345.eddx b/TangDou/AcWing_TiGao/T3/Floyd/345.eddx similarity index 100% rename from TangDou/AcWing/Floyd/345.eddx rename to TangDou/AcWing_TiGao/T3/Floyd/345.eddx diff --git a/TangDou/AcWing/Floyd/345.md b/TangDou/AcWing_TiGao/T3/Floyd/345.md similarity index 95% rename from TangDou/AcWing/Floyd/345.md rename to TangDou/AcWing_TiGao/T3/Floyd/345.md index 4d48988..63f73c5 100644 --- a/TangDou/AcWing/Floyd/345.md +++ b/TangDou/AcWing_TiGao/T3/Floyd/345.md @@ -101,9 +101,9 @@ t = [ 14, 11, 14 ] 矩阵乘法 **模板** 如下: ```cpp {.line-numbers} -for (int k = 1; k <= COLS_A; ++k) - for (int i = 1; i <= ROWS_A; ++i) - for (int j = 1; j <= COLS_B; ++j) +for (int k = 1; k <= COLS_A; k++) + for (int i = 1; i <= ROWS_A; i++) + for (int j = 1; j <= COLS_B; j++) t[i][j] += a[i][k] * b[k][j]; ``` diff --git a/TangDou/AcWing/MinimalPath/1126.cpp b/TangDou/AcWing_TiGao/T3/MiniPath/1126.cpp similarity index 100% rename from TangDou/AcWing/MinimalPath/1126.cpp rename to TangDou/AcWing_TiGao/T3/MiniPath/1126.cpp diff --git a/TangDou/AcWing/MinimalPath/1126.md b/TangDou/AcWing_TiGao/T3/MiniPath/1126.md similarity index 100% rename from TangDou/AcWing/MinimalPath/1126.md rename to TangDou/AcWing_TiGao/T3/MiniPath/1126.md diff --git a/TangDou/AcWing/MinimalPath/1127.cpp b/TangDou/AcWing_TiGao/T3/MiniPath/1127.cpp similarity index 100% rename from TangDou/AcWing/MinimalPath/1127.cpp rename to TangDou/AcWing_TiGao/T3/MiniPath/1127.cpp diff --git a/TangDou/AcWing/MinimalPath/1127.md b/TangDou/AcWing_TiGao/T3/MiniPath/1127.md similarity index 100% rename from TangDou/AcWing/MinimalPath/1127.md rename to TangDou/AcWing_TiGao/T3/MiniPath/1127.md diff --git a/TangDou/AcWing/MinimalPath/1128.cpp b/TangDou/AcWing_TiGao/T3/MiniPath/1128.cpp similarity index 100% rename from TangDou/AcWing/MinimalPath/1128.cpp rename to TangDou/AcWing_TiGao/T3/MiniPath/1128.cpp diff --git a/TangDou/AcWing/MinimalPath/1128.in b/TangDou/AcWing_TiGao/T3/MiniPath/1128.in similarity index 100% rename from TangDou/AcWing/MinimalPath/1128.in rename to TangDou/AcWing_TiGao/T3/MiniPath/1128.in diff --git a/TangDou/AcWing/MinimalPath/1128.md b/TangDou/AcWing_TiGao/T3/MiniPath/1128.md similarity index 100% rename from TangDou/AcWing/MinimalPath/1128.md rename to TangDou/AcWing_TiGao/T3/MiniPath/1128.md diff --git a/TangDou/AcWing/MinimalPath/1129.cpp b/TangDou/AcWing_TiGao/T3/MiniPath/1129.cpp similarity index 100% rename from TangDou/AcWing/MinimalPath/1129.cpp rename to TangDou/AcWing_TiGao/T3/MiniPath/1129.cpp diff --git a/TangDou/AcWing/MinimalPath/1129_Dijkstra.cpp b/TangDou/AcWing_TiGao/T3/MiniPath/1129_Dijkstra.cpp similarity index 100% rename from TangDou/AcWing/MinimalPath/1129_Dijkstra.cpp rename to TangDou/AcWing_TiGao/T3/MiniPath/1129_Dijkstra.cpp diff --git a/TangDou/AcWing/MinimalPath/1129_SPFA.cpp b/TangDou/AcWing_TiGao/T3/MiniPath/1129_SPFA.cpp similarity index 100% rename from TangDou/AcWing/MinimalPath/1129_SPFA.cpp rename to TangDou/AcWing_TiGao/T3/MiniPath/1129_SPFA.cpp diff --git a/TangDou/AcWing/MinimalPath/903.cpp b/TangDou/AcWing_TiGao/T3/MiniPath/903.cpp similarity index 100% rename from TangDou/AcWing/MinimalPath/903.cpp rename to TangDou/AcWing_TiGao/T3/MiniPath/903.cpp diff --git a/TangDou/AcWing/MinimalPath/903.md b/TangDou/AcWing_TiGao/T3/MiniPath/903.md similarity index 100% rename from TangDou/AcWing/MinimalPath/903.md rename to TangDou/AcWing_TiGao/T3/MiniPath/903.md diff --git a/TangDou/AcWing/MinimalPath/920.cpp b/TangDou/AcWing_TiGao/T3/MiniPath/920.cpp similarity index 100% rename from TangDou/AcWing/MinimalPath/920.cpp rename to TangDou/AcWing_TiGao/T3/MiniPath/920.cpp diff --git a/TangDou/AcWing/MinimalPath/920.md b/TangDou/AcWing_TiGao/T3/MiniPath/920.md similarity index 100% rename from TangDou/AcWing/MinimalPath/920.md rename to TangDou/AcWing_TiGao/T3/MiniPath/920.md diff --git a/TangDou/AcWing/MinimalPath/1131.cpp b/TangDou/AcWing_TiGao/T3/MiniPathExtend/1131.cpp similarity index 64% rename from TangDou/AcWing/MinimalPath/1131.cpp rename to TangDou/AcWing_TiGao/T3/MiniPathExtend/1131.cpp index a0551f5..8c5ef11 100644 --- a/TangDou/AcWing/MinimalPath/1131.cpp +++ b/TangDou/AcWing_TiGao/T3/MiniPathExtend/1131.cpp @@ -6,7 +6,7 @@ typedef pair PII; int g[N][N]; // 两个位置之间的间隔是什么,可能是某种门,或者是墙 int key[N]; // 某个坐标位置上有哪些钥匙,这是用数位压缩记录的,方便位运算 -int dist[N][1 << M]; // 哪个位置,在携带不同的钥匙情况下的状态 +int dis[N][1 << M]; // 哪个位置,在携带不同的钥匙情况下的状态 int n, m; // n行m列 int k; // 迷宫中门和墙的总数 int p; // p类钥匙 @@ -19,22 +19,23 @@ int get(int x, int y) { } int bfs() { - memset(dist, 0x3f, sizeof dist); // 初始化距离 - queue q; // bfs用的队列 + memset(dis, 0x3f, sizeof dis); // 初始化距离 + queue q; // bfs用的队列 - int t = get(1, 1); // 从编号1出发 - q.push({t, key[t]}); // 位置+携带钥匙的压缩状态 = 现在的真正状态 - dist[t][key[t]] = 0; // 初始状态的距离为0 + int S = get(1, 1); // 从编号1出发 + q.push({S, key[S]}); // 位置+携带钥匙的压缩状态 = 现在的真正状态 + dis[S][key[S]] = 0; // 初始状态的需要走的步数为0 while (q.size()) { PII x = q.front(); q.pop(); int u = x.first; // 出发点编号 - int st = x.second; // 钥匙状态 + int st = x.second; // 钥匙状态,为状态压缩的数字 - // 找到大兵瑞恩就结束了 - if (u == n * m) return dist[u][st]; + // dis[u][st]:到达了n*m,并且,当前状态是st: 找到大兵瑞恩就结束了,不用管最终的钥匙状态是什么 + // 是什么都是符合拯救大兵的目标的 + if (u == n * m) return dis[u][st]; // 四个方向 for (int i = 0; i < 4; i++) { @@ -42,27 +43,26 @@ int bfs() { int tx = (u - 1) / m + 1 + dx[i]; // 下一个位置 int ty = (u - 1) % m + 1 + dy[i]; - int tz = get(tx, ty); // 要去的坐标位置tz - int ts = st; // 复制出z结点携带过来的钥匙状态 + int T = get(tx, ty); // 要去的坐标位置T /* - g[z][tz] == 0 有墙,不能走 - g[z][tz] > 0 有门,有钥匙能走,无钥匙不能走 - g[z][tz] == -1 随便走 + g[z][T] == 0 有墙,不能走 + g[z][T] > 0 有门,有钥匙能走,无钥匙不能走 + g[z][T] == -1 随便走 */ - // 出界或有墙 - if (tx == 0 || ty == 0 || tx > n || ty > m || g[u][tz] == 0) continue; + // 出界或有墙,没有办法转移 + if (tx == 0 || ty == 0 || tx > n || ty > m || g[u][T] == 0) continue; - // 有门,并且, v这个状态中没有当前类型的钥匙 - if (g[u][tz] > 0 && !(st >> g[u][tz] & 1)) continue; + // 有门,并且, st这个状态中没有带过来当前类型的钥匙 + if (g[u][T] > 0 && !(st >> g[u][T] & 1)) continue; - // 捡起钥匙 - ts |= key[tz]; + // 捡起钥匙不会增加成本,所以,无条件捡起来钥匙 + int ST = st | key[T]; // 如果这个状态没有走过 - if (dist[tz][ts] == INF) { - q.push({tz, ts}); // 入队列 - dist[tz][ts] = dist[u][st] + 1; // 步数加1 + if (dis[T][ST] == INF) { + q.push({T, ST}); // 入队列 + dis[T][ST] = dis[u][st] + 1; // 步数加1 } } } diff --git a/TangDou/AcWing/MinimalPath/1131.md b/TangDou/AcWing_TiGao/T3/MiniPathExtend/1131.md similarity index 80% rename from TangDou/AcWing/MinimalPath/1131.md rename to TangDou/AcWing_TiGao/T3/MiniPathExtend/1131.md index 7e30689..8654c54 100644 --- a/TangDou/AcWing/MinimalPath/1131.md +++ b/TangDou/AcWing_TiGao/T3/MiniPathExtend/1131.md @@ -77,12 +77,12 @@ $1≤k≤150$ ### 二、解题思路 -试想下如果本题 **没有钥匙和门** 的条件,只要求从 **左上角** 走到 **右下角** 的最小步数,就是简单的迷宫问题了,可以使用$BFS$解决。 +试想下如果本题 **没有钥匙和门** 的条件,只要求从 **左上角** 走到 **右下角** 的最小步数,就是简单的迷宫问题了,可以使用$bfs$解决。 #### 状态表示 -加上钥匙和门的的条件,便是**类似于八数码问题**了。实际上$BFS$解决的最短路问题都可以看作**求从初始状态到结束状态需要的最小转移次数**: +加上钥匙和门的的条件,便是**类似于八数码问题**了。实际上$bfs$解决的最短路问题都可以看作 **求从初始状态到结束状态需要的最小转移次数**: -普通迷宫问题的 **状态** 就是 **当前所在的坐标**,八数码问题的 **状态** 就是**当前棋盘的局面**。 +普通迷宫问题的 **状态** 就是 **当前所在的坐标**,八数码问题的 **状态** 就是 **当前棋盘的局面**。 本题在迷宫问题上加上了 **钥匙和门** 的条件,显然,处在同一个坐标下,**持有钥匙和不持有钥匙就不是同一个状态了**,为了能够清楚的表示每个状态,除了当前坐标外还需要加上当前获得的钥匙信息,即$f[x][y][st]$表示当前处在$(x,y)$位置下持有钥匙状态为$st$,将二维坐标压缩成一维就得到$f[z][st]$这样的状态表示了,或者说,$z$是格子的编号,从上到下,从左而右的编号依次为$1$到$n*m$,$st$为$0110$时,表示持有第$1,2$类钥匙,这里注意我在 表示状态时抛弃了最右边的一位,因为钥匙编号从$1$开始,我想确定是否持有第$i$类钥匙时,只需要判断`st >> i & 1`是不是等于$1$即可。 @@ -108,7 +108,7 @@ typedef pair PII; int g[N][N]; // 两个位置之间的间隔是什么,可能是某种门,或者是墙 int key[N]; // 某个坐标位置上有哪些钥匙,这是用数位压缩记录的,方便位运算 -int dist[N][1 << M]; // 哪个位置,在携带不同的钥匙情况下的状态 +int dis[N][1 << M]; // 哪个位置,在携带不同的钥匙情况下的状态 int n, m; // n行m列 int k; // 迷宫中门和墙的总数 int p; // p类钥匙 @@ -121,22 +121,23 @@ int get(int x, int y) { } int bfs() { - memset(dist, 0x3f, sizeof dist); // 初始化距离 - queue q; // bfs用的队列 + memset(dis, 0x3f, sizeof dis); // 初始化距离 + queue q; // bfs用的队列 - int t = get(1, 1); // 从编号1出发 - q.push({t, key[t]}); // 位置+携带钥匙的压缩状态 = 现在的真正状态 - dist[t][key[t]] = 0; // 初始状态的距离为0 + int S = get(1, 1); // 从编号1出发 + q.push({S, key[S]}); // 位置+携带钥匙的压缩状态 = 现在的真正状态 + dis[S][key[S]] = 0; // 初始状态的需要走的步数为0 while (q.size()) { PII x = q.front(); q.pop(); int u = x.first; // 出发点编号 - int st = x.second; // 钥匙状态 + int st = x.second; // 钥匙状态,为状态压缩的数字 - // 找到大兵瑞恩就结束了 - if (u == n * m) return dist[u][st]; + // dis[u][st]:到达了n*m,并且,当前状态是st: 找到大兵瑞恩就结束了,不用管最终的钥匙状态是什么 + // 是什么都是符合拯救大兵的目标的 + if (u == n * m) return dis[u][st]; // 四个方向 for (int i = 0; i < 4; i++) { @@ -144,27 +145,26 @@ int bfs() { int tx = (u - 1) / m + 1 + dx[i]; // 下一个位置 int ty = (u - 1) % m + 1 + dy[i]; - int tz = get(tx, ty); // 要去的坐标位置tz - int ts = st; // 复制出z结点携带过来的钥匙状态 + int T = get(tx, ty); // 要去的坐标位置T /* - g[z][tz] == 0 有墙,不能走 - g[z][tz] > 0 有门,有钥匙能走,无钥匙不能走 - g[z][tz] == -1 随便走 + g[z][T] == 0 有墙,不能走 + g[z][T] > 0 有门,有钥匙能走,无钥匙不能走 + g[z][T] == -1 随便走 */ - // 出界或有墙 - if (tx == 0 || ty == 0 || tx > n || ty > m || g[u][tz] == 0) continue; + // 出界或有墙,没有办法转移 + if (tx == 0 || ty == 0 || tx > n || ty > m || g[u][T] == 0) continue; - // 有门,并且, v这个状态中没有当前类型的钥匙 - if (g[u][tz] > 0 && !(st >> g[u][tz] & 1)) continue; + // 有门,并且, st这个状态中没有带过来当前类型的钥匙 + if (g[u][T] > 0 && !(st >> g[u][T] & 1)) continue; - // 捡起钥匙 - ts |= key[tz]; + // 捡起钥匙不会增加成本,所以,无条件捡起来钥匙 + int ST = st | key[T]; // 如果这个状态没有走过 - if (dist[tz][ts] == INF) { - q.push({tz, ts}); // 入队列 - dist[tz][ts] = dist[u][st] + 1; // 步数加1 + if (dis[T][ST] == INF) { + q.push({T, ST}); // 入队列 + dis[T][ST] = dis[u][st] + 1; // 步数加1 } } } @@ -204,4 +204,5 @@ int main() { printf("%d\n", bfs()); return 0; } + ``` diff --git a/TangDou/AcWing/MinimalPath/1134.drawio b/TangDou/AcWing_TiGao/T3/MiniPathExtend/1134.drawio similarity index 100% rename from TangDou/AcWing/MinimalPath/1134.drawio rename to TangDou/AcWing_TiGao/T3/MiniPathExtend/1134.drawio diff --git a/TangDou/AcWing/MinimalPath/1134.md b/TangDou/AcWing_TiGao/T3/MiniPathExtend/1134.md similarity index 68% rename from TangDou/AcWing/MinimalPath/1134.md rename to TangDou/AcWing_TiGao/T3/MiniPathExtend/1134.md index 289128f..dc7ee6a 100644 --- a/TangDou/AcWing/MinimalPath/1134.md +++ b/TangDou/AcWing_TiGao/T3/MiniPathExtend/1134.md @@ -93,26 +93,26 @@ void add(int a, int b) { e[idx] = b, ne[idx] = h[a], h[a] = idx++; } -int cnt[N]; //从顶点1开始,到其他每个点的最短路有几条 -int dist[N]; //最短距离 +int cnt[N]; // 从顶点1开始,到其他每个点的最短路有几条 +int dis[N]; // 最短距离 int n, m; void bfs() { - memset(dist, 0x3f, sizeof dist); + memset(dis, 0x3f, sizeof dis); queue q; q.push(1); - cnt[1] = 1; //从顶点1开始,到顶点1的最短路有1条 - dist[1] = 0; //距离为0 + cnt[1] = 1; // 从顶点1开始,到顶点1的最短路有1条 + dis[1] = 0; // 距离为0 while (q.size()) { int u = q.front(); q.pop(); for (int i = h[u]; ~i; i = ne[i]) { int j = e[i]; - if (dist[j] > dist[u] + 1) { - dist[j] = dist[u] + 1; + if (dis[j] > dis[u] + 1) { + dis[j] = dis[u] + 1; q.push(j); cnt[j] = cnt[u]; - } else if (dist[j] == dist[u] + 1) + } else if (dis[j] == dis[u] + 1) cnt[j] = (cnt[j] + cnt[u]) % MOD; } } @@ -141,7 +141,7 @@ const int MOD = 100003; int n, m; int cnt[N]; -int dist[N]; +int dis[N]; bool st[N]; int h[N], e[M], ne[M], idx; void add(int a, int b) { @@ -149,30 +149,29 @@ void add(int a, int b) { } void dijkstra() { - memset(dist, 0x3f, sizeof dist); - dist[1] = 0; - // 出发点到自己的最短路径只能有1条 - cnt[1] = 1; + memset(dis, 0x3f, sizeof dis); + dis[1] = 0; + cnt[1] = 1; // 出发点到自己的最短路径有1条,长度是0 // 小顶堆q - priority_queue, greater> pq; - pq.push({0, 1}); + priority_queue, greater> q; + q.push({0, 1}); - while (pq.size()) { - auto t = pq.top(); - pq.pop(); - int u = t.second, d = t.first; + while (q.size()) { + auto t = q.top(); + q.pop(); + int u = t.second; if (st[u]) continue; st[u] = true; for (int i = h[u]; ~i; i = ne[i]) { int j = e[i]; - if (dist[j] > d + 1) { - dist[j] = d + 1; + if (dis[j] > dis[u] + 1) { + dis[j] = dis[u] + 1; cnt[j] = cnt[u]; - pq.push({dist[j], j}); - } else if (dist[j] == d + 1) + q.push({dis[j], j}); + } else if (dis[j] == dis[u] + 1) cnt[j] = (cnt[j] + cnt[u]) % MOD; } } @@ -204,7 +203,7 @@ int main() { **分析** 只需要计算出最短路的条数和距离、次短路的距离和条数,最后判断最短路和次短路的关系即可,在$dijkstra$求最短路的基础上,**加一维** 保存从起点到该点的 **最短路** 和 **次短路**,**同时记录相应的数量** -如果当前点的最短路或次短路更新了,那么这个点可能松弛其他点,加入优先队列;如果等于最短路或次短路,相应的数量就加 +如果当前点的最短路或次短路更新了,那么这个点可能松弛其他点,加入优先队列;如果等于最短路或次短路,相应的数量就加。 关于代码中①②的自我解释: @@ -225,10 +224,12 @@ using namespace std; const int N = 1e3 + 13; const int M = 1e6 + 10; int n, m, u, v, s, f; -int dist[N][2], cnt[N][2]; +// 将最短路扩展为二维,含义:最短路与次短路 +// dis:路径长度,cnt:路线数量,st:是否已经出队列 +int dis[N][2], cnt[N][2]; bool st[N][2]; -//链式前向星 +// 链式前向星 int e[M], h[N], idx, w[M], ne[M]; void add(int a, int b, int c = 0) { e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++; @@ -239,64 +240,68 @@ struct Node { // d:目前结点v的路径长度 // k:是最短路0还是次短路1 int u, d, k; - // POJ中结构体,没有构造函数,直接报编译错误 - Node(int u, int d, int k) { - this->u = u, this->d = d, this->k = k; - } const bool operator<(Node x) const { return d > x.d; } }; void dijkrsta() { - priority_queue q; //通过定义结构体小于号,实现小顶堆 - memset(dist, 0x3f, sizeof(dist)); //清空最小距离与次小距离数组 - memset(cnt, 0, sizeof(cnt)); //清空最小距离路线个数与次小距离路线个数数组 - memset(st, 0, sizeof(st)); //清空是否出队过数组 + priority_queue q; // 默认是大顶堆,通过定义结构体小于号,实现小顶堆。比如:认证的d值更大,谁就更小! + memset(dis, 0x3f, sizeof dis); // 清空最小距离与次小距离数组 + memset(cnt, 0, sizeof cnt); // 清空最小距离路线个数与次小距离路线个数数组 + memset(st, 0, sizeof st); // 清空是否出队过数组 - cnt[s][0] = 1; //起点s,0:最短路,1:有一条 - cnt[s][1] = 0; //次短路,路线数为0 + cnt[s][0] = 1; // 起点s,0:最短路,1:有一条 + cnt[s][1] = 0; // 次短路,路线数为0 - dist[s][0] = 0; //最短路从s出发到s的距离是0 - dist[s][1] = 0; //次短路从s出发到s的距离是0 + dis[s][0] = 0; // 最短路从s出发到s的距离是0 + dis[s][1] = 0; // 次短路从s出发到s的距离是0 - q.push(Node(s, 0, 0)); //入队列 + q.push({s, 0, 0}); // 入队列 while (q.size()) { Node x = q.top(); q.pop(); - int u = x.u, k = x.k, d = x.d; + int u = x.u, k = x.k; // u:节点号,k:是最短路还是次短路,d:路径长度(这个主要用于堆中排序,不用于实战,实战中可以使用dis[u][k]) - if (st[u][k]) continue; //① + if (st[u][k]) continue; // ① 和dijkstra标准版本一样的,只不过多了一个维度 st[u][k] = true; for (int i = h[u]; ~i; i = ne[i]) { int j = e[i]; - int dj = d + w[i]; //原长度+到节点j的边长 + int dj = dis[u][k] + w[i]; // 原长度+到节点j的边长 - if (dj == dist[j][0]) //与到j的最短长度相等,则更新路径数量 + if (dj == dis[j][0]) // 与到j的最短长度相等,则更新路径数量 cnt[j][0] += cnt[u][k]; - else if (dj < dist[j][0]) { //找到更小的路线,需要更新 - dist[j][1] = dist[j][0]; //次短距离被最短距离覆盖 - cnt[j][1] = cnt[j][0]; //次短个数被最短个数覆盖 - - dist[j][0] = dj; //更新最短距离 - cnt[j][0] = cnt[u][k]; //更新最短个数 - - q.push(Node(j, dist[j][1], 1)); //② - q.push(Node(j, dist[j][0], 0)); - } else if (dj == dist[j][1]) //如果等于次短 - cnt[j][1] += cnt[u][k]; //更新次短的方案数,累加 - else if (dj < dist[j][1]) { //如果大于最短,小于次短,两者中间 - dist[j][1] = dj; //更新次短距离 - cnt[j][1] = cnt[u][k]; //更新次短方案数 - q.push(Node(j, dist[j][1], 1)); //次短入队列 + else if (dj < dis[j][0]) { // 找到更小的路线,需要更新 + dis[j][1] = dis[j][0]; // 次短距离被最短距离覆盖 + cnt[j][1] = cnt[j][0]; // 次短个数被最短个数覆盖 + + dis[j][0] = dj; // 更新最短距离 + cnt[j][0] = cnt[u][k]; // 更新最短个数 + + q.push({j, dis[j][1], 1}); // ② + q.push({j, dis[j][0], 0}); + } else if (dj == dis[j][1]) // 如果等于次短 + cnt[j][1] += cnt[u][k]; // 更新次短的方案数,累加 + else if (dj < dis[j][1]) { // 如果大于最短,小于次短,两者中间 + dis[j][1] = dj; // 更新次短距离 + cnt[j][1] = cnt[u][k]; // 更新次短方案数 + q.push({j, dis[j][1], 1}); // 次短入队列 } } } } int main() { +#ifndef ONLINE_JUDGE + freopen("POJ3463.in", "r", stdin); + /* + 答案: + 3 + 2 + */ +#endif int T; scanf("%d", &T); while (T--) { @@ -307,13 +312,14 @@ int main() { scanf("%d %d %d", &a, &b, &c); add(a, b, c); } - //起点和终点 + // 起点和终点 scanf("%d %d", &s, &f); - //计算最短路 + // 计算最短路 dijkrsta(); - //输出 - printf("%d\n", cnt[f][0] + (dist[f][1] == dist[f][0] + 1 ? cnt[f][1] : 0)); + // 输出 + printf("%d\n", cnt[f][0] + (dis[f][1] == dis[f][0] + 1 ? cnt[f][1] : 0)); } return 0; } + ``` diff --git a/TangDou/AcWing/MinimalPath/1134_Bfs.cpp b/TangDou/AcWing_TiGao/T3/MiniPathExtend/1134_Bfs.cpp similarity index 66% rename from TangDou/AcWing/MinimalPath/1134_Bfs.cpp rename to TangDou/AcWing_TiGao/T3/MiniPathExtend/1134_Bfs.cpp index 240578b..8aeb9d8 100644 --- a/TangDou/AcWing/MinimalPath/1134_Bfs.cpp +++ b/TangDou/AcWing_TiGao/T3/MiniPathExtend/1134_Bfs.cpp @@ -7,26 +7,26 @@ void add(int a, int b) { e[idx] = b, ne[idx] = h[a], h[a] = idx++; } -int cnt[N]; //从顶点1开始,到其他每个点的最短路有几条 -int dist[N]; //最短距离 +int cnt[N]; // 从顶点1开始,到其他每个点的最短路有几条 +int dis[N]; // 最短距离 int n, m; void bfs() { - memset(dist, 0x3f, sizeof dist); + memset(dis, 0x3f, sizeof dis); queue q; q.push(1); - cnt[1] = 1; //从顶点1开始,到顶点1的最短路有1条 - dist[1] = 0; //距离为0 + cnt[1] = 1; // 从顶点1开始,到顶点1的最短路有1条 + dis[1] = 0; // 距离为0 while (q.size()) { int u = q.front(); q.pop(); for (int i = h[u]; ~i; i = ne[i]) { int j = e[i]; - if (dist[j] > dist[u] + 1) { - dist[j] = dist[u] + 1; + if (dis[j] > dis[u] + 1) { + dis[j] = dis[u] + 1; q.push(j); cnt[j] = cnt[u]; - } else if (dist[j] == dist[u] + 1) + } else if (dis[j] == dis[u] + 1) cnt[j] = (cnt[j] + cnt[u]) % MOD; } } diff --git a/TangDou/AcWing/MinimalPath/1134_Dijkstra.cpp b/TangDou/AcWing_TiGao/T3/MiniPathExtend/1134_Dijkstra.cpp similarity index 60% rename from TangDou/AcWing/MinimalPath/1134_Dijkstra.cpp rename to TangDou/AcWing_TiGao/T3/MiniPathExtend/1134_Dijkstra.cpp index 8d6e8eb..74d98ab 100644 --- a/TangDou/AcWing/MinimalPath/1134_Dijkstra.cpp +++ b/TangDou/AcWing_TiGao/T3/MiniPathExtend/1134_Dijkstra.cpp @@ -6,7 +6,7 @@ const int MOD = 100003; int n, m; int cnt[N]; -int dist[N]; +int dis[N]; bool st[N]; int h[N], e[M], ne[M], idx; void add(int a, int b) { @@ -14,30 +14,29 @@ void add(int a, int b) { } void dijkstra() { - memset(dist, 0x3f, sizeof dist); - dist[1] = 0; - // 出发点到自己的最短路径只能有1条 - cnt[1] = 1; + memset(dis, 0x3f, sizeof dis); + dis[1] = 0; + cnt[1] = 1; // 出发点到自己的最短路径有1条,长度是0 // 小顶堆q - priority_queue, greater> pq; - pq.push({0, 1}); + priority_queue, greater> q; + q.push({0, 1}); - while (pq.size()) { - auto t = pq.top(); - pq.pop(); - int u = t.second, d = t.first; + while (q.size()) { + auto t = q.top(); + q.pop(); + int u = t.second; if (st[u]) continue; st[u] = true; for (int i = h[u]; ~i; i = ne[i]) { int j = e[i]; - if (dist[j] > d + 1) { - dist[j] = d + 1; + if (dis[j] > dis[u] + 1) { + dis[j] = dis[u] + 1; cnt[j] = cnt[u]; - pq.push({dist[j], j}); - } else if (dist[j] == d + 1) + q.push({dis[j], j}); + } else if (dis[j] == dis[u] + 1) cnt[j] = (cnt[j] + cnt[u]) % MOD; } } diff --git a/TangDou/AcWing/MinimalPath/1137.md b/TangDou/AcWing_TiGao/T3/MiniPathExtend/1137.md similarity index 79% rename from TangDou/AcWing/MinimalPath/1137.md rename to TangDou/AcWing_TiGao/T3/MiniPathExtend/1137.md index cbfb451..d800308 100644 --- a/TangDou/AcWing/MinimalPath/1137.md +++ b/TangDou/AcWing_TiGao/T3/MiniPathExtend/1137.md @@ -87,32 +87,32 @@ int h[N], e[M], w[M], ne[M], idx; void add(int a, int b, int c) { e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++; } -int d[N]; // 最短距离数组 +int dis[N]; // 最短距离数组 bool st[N]; // 是否进过队列 // 迪杰斯特拉 void dijkstra() { - memset(d, 0x3f, sizeof d); // 初始化大 - memset(st, 0, sizeof st); // 初始化为未出队列过 - priority_queue, greater> pq; // 小顶堆 - pq.push({0, 0}); // 出发点入队列 - d[0] = 0; // 出发点距离0 - - while (pq.size()) { - auto t = pq.top(); - pq.pop(); + memset(dis, 0x3f, sizeof dis); // 初始化大 + memset(st, 0, sizeof st); // 初始化为未出队列过 + priority_queue, greater> q; // 小顶堆 + q.push({0, 0}); // 出发点入队列 + dis[0] = 0; // 出发点距离0 + + while (q.size()) { + auto t = q.top(); + q.pop(); int u = t.second; if (st[u]) continue; st[u] = true; for (int i = h[u]; ~i; i = ne[i]) { int j = e[i]; - if (d[j] > d[u] + w[i]) { - d[j] = d[u] + w[i]; - pq.push({d[j], j}); + if (dis[j] > dis[u] + w[i]) { + dis[j] = dis[u] + w[i]; + q.push({dis[j], j}); } } } // 注意:此处的S是终点,不是起点,不是起点,不是起点! - printf("%d\n", d[S] == INF ? -1 : d[S]); + printf("%d\n", dis[S] == INF ? -1 : dis[S]); } int main() { while (cin >> n >> m >> S) { @@ -126,11 +126,11 @@ int main() { add(a, b, c); } int T; - scanf("%d", &T); + cin >> T; while (T--) { int x; cin >> x; - add(0, x, 0); + add(0, x, 0); // 超级源点法 } dijkstra(); } @@ -153,25 +153,25 @@ void add(int a, int b, int c) { } int n, m; // n个点,m条边 int S; // 出发点 -int d[N]; // 距离数组 +int dis[N]; // 距离数组 bool st[N]; // Dijkstra是不是入过队列 void dijkstra() { priority_queue, greater> q; q.push({0, S}); - d[S] = 0; + dis[S] = 0; while (q.size()) { auto t = q.top(); - int u = t.second, dist = t.first; + int u = t.second; q.pop(); if (st[u]) continue; st[u] = true; - + for (int i = h[u]; ~i; i = ne[i]) { int j = e[i]; - if (d[j] > dist + w[i]) { - d[j] = dist + w[i]; - q.push({d[j], j}); + if (dis[j] > dis[u] + w[i]) { + dis[j] = dis[u] + w[i]; + q.push({dis[j], j}); } } } @@ -181,7 +181,7 @@ int main() { // 初始化 memset(st, 0, sizeof st); memset(h, -1, sizeof h); - memset(d, 0x3f, sizeof d); + memset(dis, 0x3f, sizeof dis); idx = 0; int ans = INF; @@ -197,7 +197,7 @@ int main() { cin >> T; while (T--) { cin >> x; - ans = min(ans, d[x]); + ans = min(ans, dis[x]); } printf("%d\n", ans == INF ? -1 : ans); } diff --git a/TangDou/AcWing/MinimalPath/1137_1.cpp b/TangDou/AcWing_TiGao/T3/MiniPathExtend/1137_1.cpp similarity index 57% rename from TangDou/AcWing/MinimalPath/1137_1.cpp rename to TangDou/AcWing_TiGao/T3/MiniPathExtend/1137_1.cpp index 766140e..d6e0524 100644 --- a/TangDou/AcWing/MinimalPath/1137_1.cpp +++ b/TangDou/AcWing_TiGao/T3/MiniPathExtend/1137_1.cpp @@ -11,32 +11,32 @@ int h[N], e[M], w[M], ne[M], idx; void add(int a, int b, int c) { e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++; } -int d[N]; // 最短距离数组 +int dis[N]; // 最短距离数组 bool st[N]; // 是否进过队列 // 迪杰斯特拉 void dijkstra() { - memset(d, 0x3f, sizeof d); // 初始化大 - memset(st, 0, sizeof st); // 初始化为未出队列过 - priority_queue, greater> pq; // 小顶堆 - pq.push({0, 0}); // 出发点入队列 - d[0] = 0; // 出发点距离0 + memset(dis, 0x3f, sizeof dis); // 初始化大 + memset(st, 0, sizeof st); // 初始化为未出队列过 + priority_queue, greater> q; // 小顶堆 + q.push({0, 0}); // 出发点入队列 + dis[0] = 0; // 出发点距离0 - while (pq.size()) { - auto t = pq.top(); - pq.pop(); + while (q.size()) { + auto t = q.top(); + q.pop(); int u = t.second; if (st[u]) continue; st[u] = true; for (int i = h[u]; ~i; i = ne[i]) { int j = e[i]; - if (d[j] > d[u] + w[i]) { - d[j] = d[u] + w[i]; - pq.push({d[j], j}); + if (dis[j] > dis[u] + w[i]) { + dis[j] = dis[u] + w[i]; + q.push({dis[j], j}); } } } // 注意:此处的S是终点,不是起点,不是起点,不是起点! - printf("%d\n", d[S] == INF ? -1 : d[S]); + printf("%d\n", dis[S] == INF ? -1 : dis[S]); } int main() { while (cin >> n >> m >> S) { @@ -50,11 +50,11 @@ int main() { add(a, b, c); } int T; - scanf("%d", &T); + cin >> T; while (T--) { int x; cin >> x; - add(0, x, 0); + add(0, x, 0); // 超级源点法 } dijkstra(); } diff --git a/TangDou/AcWing/MinimalPath/1137_2.cpp b/TangDou/AcWing_TiGao/T3/MiniPathExtend/1137_2.cpp similarity index 69% rename from TangDou/AcWing/MinimalPath/1137_2.cpp rename to TangDou/AcWing_TiGao/T3/MiniPathExtend/1137_2.cpp index 713b360..28a5a16 100644 --- a/TangDou/AcWing/MinimalPath/1137_2.cpp +++ b/TangDou/AcWing_TiGao/T3/MiniPathExtend/1137_2.cpp @@ -10,25 +10,25 @@ void add(int a, int b, int c) { } int n, m; // n个点,m条边 int S; // 出发点 -int d[N]; // 距离数组 +int dis[N]; // 距离数组 bool st[N]; // Dijkstra是不是入过队列 void dijkstra() { - priority_queue, greater> pq; - pq.push({0, S}); - d[S] = 0; - while (pq.size()) { - auto t = pq.top(); - int u = t.second, dist = t.first; - pq.pop(); + priority_queue, greater> q; + q.push({0, S}); + dis[S] = 0; + while (q.size()) { + auto t = q.top(); + int u = t.second; + q.pop(); if (st[u]) continue; st[u] = true; for (int i = h[u]; ~i; i = ne[i]) { int j = e[i]; - if (d[j] > dist + w[i]) { - d[j] = dist + w[i]; - pq.push({d[j], j}); + if (dis[j] > dis[u] + w[i]) { + dis[j] = dis[u] + w[i]; + q.push({dis[j], j}); } } } @@ -38,7 +38,7 @@ int main() { // 初始化 memset(st, 0, sizeof st); memset(h, -1, sizeof h); - memset(d, 0x3f, sizeof d); + memset(dis, 0x3f, sizeof dis); idx = 0; int ans = INF; @@ -54,7 +54,7 @@ int main() { cin >> T; while (T--) { cin >> x; - ans = min(ans, d[x]); + ans = min(ans, dis[x]); } printf("%d\n", ans == INF ? -1 : ans); } diff --git a/TangDou/AcWing_TiGao/T3/MiniPathExtend/383.cpp b/TangDou/AcWing_TiGao/T3/MiniPathExtend/383.cpp new file mode 100644 index 0000000..d854526 --- /dev/null +++ b/TangDou/AcWing_TiGao/T3/MiniPathExtend/383.cpp @@ -0,0 +1,97 @@ +#include +using namespace std; +#define x first +#define y second + +const int N = 1e3 + 13; +const int M = 1e6 + 10; +int n, m, u, v, s, f; +// 将最短路扩展为二维,含义:最短路与次短路 +// dis:路径长度,cnt:路线数量,st:是否已经出队列 +int dis[N][2], cnt[N][2]; +bool st[N][2]; + +// 链式前向星 +int e[M], h[N], idx, w[M], ne[M]; +void add(int a, int b, int c = 0) { + e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++; +} + +struct Node { + // u: 节点号 + // d:目前结点v的路径长度 + // k:是最短路0还是次短路1 + int u, d, k; + const bool operator<(Node x) const { + return d > x.d; + } +}; + +void dijkrsta() { + priority_queue q; // 默认是大顶堆,通过定义结构体小于号,实现小顶堆。比如:认证的d值更大,谁就更小! + memset(dis, 0x3f, sizeof dis); // 清空最小距离与次小距离数组 + memset(cnt, 0, sizeof cnt); // 清空最小距离路线个数与次小距离路线个数数组 + memset(st, 0, sizeof st); // 清空是否出队过数组 + + cnt[s][0] = 1; // 起点s,0:最短路,1:有一条 + cnt[s][1] = 0; // 次短路,路线数为0 + + dis[s][0] = 0; // 最短路从s出发到s的距离是0 + dis[s][1] = 0; // 次短路从s出发到s的距离是0 + + q.push({s, 0, 0}); // 入队列 + + while (q.size()) { + Node x = q.top(); + q.pop(); + + int u = x.u, k = x.k; // u:节点号,k:是最短路还是次短路,d:路径长度(这个主要用于堆中排序,不用于实战,实战中可以使用dis[u][k]) + + if (st[u][k]) continue; // ① 和dijkstra标准版本一样的,只不过多了一个维度 + st[u][k] = true; + + for (int i = h[u]; ~i; i = ne[i]) { + int j = e[i]; + int dj = dis[u][k] + w[i]; // 原长度+到节点j的边长 + + if (dj == dis[j][0]) // 与到j的最短长度相等,则更新路径数量 + cnt[j][0] += cnt[u][k]; + else if (dj < dis[j][0]) { // 找到更小的路线,需要更新 + dis[j][1] = dis[j][0]; // 次短距离被最短距离覆盖 + cnt[j][1] = cnt[j][0]; // 次短个数被最短个数覆盖 + + dis[j][0] = dj; // 更新最短距离 + cnt[j][0] = cnt[u][k]; // 更新最短个数 + + q.push({j, dis[j][1], 1}); // ② + q.push({j, dis[j][0], 0}); + } else if (dj == dis[j][1]) // 如果等于次短 + cnt[j][1] += cnt[u][k]; // 更新次短的方案数,累加 + else if (dj < dis[j][1]) { // 如果大于最短,小于次短,两者中间 + dis[j][1] = dj; // 更新次短距离 + cnt[j][1] = cnt[u][k]; // 更新次短方案数 + q.push({j, dis[j][1], 1}); // 次短入队列 + } + } + } +} +int main() { + int T; + scanf("%d", &T); + while (T--) { + memset(h, -1, sizeof h); + scanf("%d %d", &n, &m); + while (m--) { + int a, b, c; + scanf("%d %d %d", &a, &b, &c); + add(a, b, c); + } + // 起点和终点 + scanf("%d %d", &s, &f); + // 计算最短路 + dijkrsta(); + // 输出 + printf("%d\n", cnt[f][0] + (dis[f][1] == dis[f][0] + 1 ? cnt[f][1] : 0)); + } + return 0; +} diff --git a/TangDou/AcWing/MinimalPath/383.md b/TangDou/AcWing_TiGao/T3/MiniPathExtend/383.md similarity index 61% rename from TangDou/AcWing/MinimalPath/383.md rename to TangDou/AcWing_TiGao/T3/MiniPathExtend/383.md index 9ae09bd..d07724a 100644 --- a/TangDou/AcWing/MinimalPath/383.md +++ b/TangDou/AcWing_TiGao/T3/MiniPathExtend/383.md @@ -119,13 +119,15 @@ $dist[S][0]$=$0$,$cnt[S][0]$=$1$ ```cpp {.line-numbers} #include using namespace std; - -const int N = 1010; -const int M = 10010; -int n, m; - -int dist[N][2]; -int cnt[N][2]; +#define x first +#define y second + +const int N = 1e3 + 13; +const int M = 1e6 + 10; +int n, m, u, v, s, f; +// 将最短路扩展为二维,含义:最短路与次短路 +// dis:路径长度,cnt:路线数量,st:是否已经出队列 +int dis[N][2], cnt[N][2]; bool st[N][2]; // 链式前向星 @@ -134,53 +136,61 @@ void add(int a, int b, int c = 0) { e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++; } -// 本题需要一个三个属性的对象:最短距离d,最短、次短k,id:节点号 struct Node { - int d, k, id; - // 小顶堆需要重载大于号,大顶堆需要重载小于号 - bool const operator>(Node b) const { - return d > b.d; + // u: 节点号 + // d:目前结点v的路径长度 + // k:是最短路0还是次短路1 + int u, d, k; + const bool operator<(Node x) const { + return d > x.d; } }; -void dijkstra(int S) { - memset(dist, 0x3f, sizeof dist); - memset(st, false, sizeof st); - memset(cnt, 0, sizeof cnt); - priority_queue, greater<>> pq; // 小顶堆 - dist[S][0] = 0; - cnt[S][0] = 1; - pq.push({0, 0, S}); - - while (pq.size()) { - auto t = pq.top(); - pq.pop(); - int u = t.id; - int k = t.k; - - if (st[u][k]) continue; +void dijkrsta() { + priority_queue q; // 默认是大顶堆,通过定义结构体小于号,实现小顶堆。比如:认证的d值更大,谁就更小! + memset(dis, 0x3f, sizeof dis); // 清空最小距离与次小距离数组 + memset(cnt, 0, sizeof cnt); // 清空最小距离路线个数与次小距离路线个数数组 + memset(st, 0, sizeof st); // 清空是否出队过数组 + + cnt[s][0] = 1; // 起点s,0:最短路,1:有一条 + cnt[s][1] = 0; // 次短路,路线数为0 + + dis[s][0] = 0; // 最短路从s出发到s的距离是0 + dis[s][1] = 0; // 次短路从s出发到s的距离是0 + + q.push({s, 0, 0}); // 入队列 + + while (q.size()) { + Node x = q.top(); + q.pop(); + + int u = x.u, k = x.k; // u:节点号,k:是最短路还是次短路,d:路径长度(这个主要用于堆中排序,不用于实战,实战中可以使用dis[u][k]) + + if (st[u][k]) continue; // ① 和dijkstra标准版本一样的,只不过多了一个维度 st[u][k] = true; for (int i = h[u]; ~i; i = ne[i]) { - int v = e[i]; - int d = dist[u][k] + w[i]; - - if (dist[v][0] > d) { // 比最短路还要短 - dist[v][1] = dist[v][0]; // 最短降为次短 - cnt[v][1] = cnt[v][0]; // 次短路数量被更新 - pq.push({dist[v][1], 1, v}); // 次短被更新,次短入队列 - - dist[v][0] = d; // 替换最短路 - cnt[v][0] = cnt[u][k]; // 替换最短路数量 - pq.push({dist[v][0], 0, v}); // 最短路入队列 - } else if (dist[v][0] == d) // 增加最短路的数量 - cnt[v][0] += cnt[u][k]; - else if (dist[v][1] > d) { // 替换次短路 - dist[v][1] = d; - cnt[v][1] = cnt[u][k]; - pq.push({dist[v][1], 1, v}); // 次短路入队列 - } else if (dist[v][1] == d) - cnt[v][1] += cnt[u][k]; + int j = e[i]; + int dj = dis[u][k] + w[i]; // 原长度+到节点j的边长 + + if (dj == dis[j][0]) // 与到j的最短长度相等,则更新路径数量 + cnt[j][0] += cnt[u][k]; + else if (dj < dis[j][0]) { // 找到更小的路线,需要更新 + dis[j][1] = dis[j][0]; // 次短距离被最短距离覆盖 + cnt[j][1] = cnt[j][0]; // 次短个数被最短个数覆盖 + + dis[j][0] = dj; // 更新最短距离 + cnt[j][0] = cnt[u][k]; // 更新最短个数 + + q.push({j, dis[j][1], 1}); // ② + q.push({j, dis[j][0], 0}); + } else if (dj == dis[j][1]) // 如果等于次短 + cnt[j][1] += cnt[u][k]; // 更新次短的方案数,累加 + else if (dj < dis[j][1]) { // 如果大于最短,小于次短,两者中间 + dis[j][1] = dj; // 更新次短距离 + cnt[j][1] = cnt[u][k]; // 更新次短方案数 + q.push({j, dis[j][1], 1}); // 次短入队列 + } } } } @@ -188,22 +198,21 @@ int main() { int T; scanf("%d", &T); while (T--) { - scanf("%d %d", &n, &m); memset(h, -1, sizeof h); - idx = 0; + scanf("%d %d", &n, &m); while (m--) { int a, b, c; scanf("%d %d %d", &a, &b, &c); add(a, b, c); } - int S, F; - scanf("%d %d", &S, &F); - dijkstra(S); - int ans = cnt[F][0]; // 最短路 - // 在正常处理完最短路和次短路后,在最后的逻辑中,增加本题的中特殊要求部分 - if (dist[F][0] == dist[F][1] - 1) ans += cnt[F][1]; - printf("%d\n", ans); + // 起点和终点 + scanf("%d %d", &s, &f); + // 计算最短路 + dijkrsta(); + // 输出 + printf("%d\n", cnt[f][0] + (dis[f][1] == dis[f][0] + 1 ? cnt[f][1] : 0)); } return 0; } + ``` \ No newline at end of file diff --git a/TangDou/AcWing_TiGao/T3/MiniPathExtend/POJ3463.cpp b/TangDou/AcWing_TiGao/T3/MiniPathExtend/POJ3463.cpp new file mode 100644 index 0000000..172785f --- /dev/null +++ b/TangDou/AcWing_TiGao/T3/MiniPathExtend/POJ3463.cpp @@ -0,0 +1,108 @@ +#include +#include +#include +#include +using namespace std; +#define x first +#define y second + +const int N = 1e3 + 13; +const int M = 1e6 + 10; +int n, m, u, v, s, f; +// 将最短路扩展为二维,含义:最短路与次短路 +// dis:路径长度,cnt:路线数量,st:是否已经出队列 +int dis[N][2], cnt[N][2]; +bool st[N][2]; + +// 链式前向星 +int e[M], h[N], idx, w[M], ne[M]; +void add(int a, int b, int c = 0) { + e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++; +} + +struct Node { + // u: 节点号 + // d:目前结点v的路径长度 + // k:是最短路0还是次短路1 + int u, d, k; + const bool operator<(Node x) const { + return d > x.d; + } +}; + +void dijkrsta() { + priority_queue q; // 默认是大顶堆,通过定义结构体小于号,实现小顶堆。比如:认证的d值更大,谁就更小! + memset(dis, 0x3f, sizeof dis); // 清空最小距离与次小距离数组 + memset(cnt, 0, sizeof cnt); // 清空最小距离路线个数与次小距离路线个数数组 + memset(st, 0, sizeof st); // 清空是否出队过数组 + + cnt[s][0] = 1; // 起点s,0:最短路,1:有一条 + cnt[s][1] = 0; // 次短路,路线数为0 + + dis[s][0] = 0; // 最短路从s出发到s的距离是0 + dis[s][1] = 0; // 次短路从s出发到s的距离是0 + + q.push({s, 0, 0}); // 入队列 + + while (q.size()) { + Node x = q.top(); + q.pop(); + + int u = x.u, k = x.k; // u:节点号,k:是最短路还是次短路,d:路径长度(这个主要用于堆中排序,不用于实战,实战中可以使用dis[u][k]) + + if (st[u][k]) continue; // ① 和dijkstra标准版本一样的,只不过多了一个维度 + st[u][k] = true; + + for (int i = h[u]; ~i; i = ne[i]) { + int j = e[i]; + int dj = dis[u][k] + w[i]; // 原长度+到节点j的边长 + + if (dj == dis[j][0]) // 与到j的最短长度相等,则更新路径数量 + cnt[j][0] += cnt[u][k]; + else if (dj < dis[j][0]) { // 找到更小的路线,需要更新 + dis[j][1] = dis[j][0]; // 次短距离被最短距离覆盖 + cnt[j][1] = cnt[j][0]; // 次短个数被最短个数覆盖 + + dis[j][0] = dj; // 更新最短距离 + cnt[j][0] = cnt[u][k]; // 更新最短个数 + + q.push({j, dis[j][1], 1}); // ② + q.push({j, dis[j][0], 0}); + } else if (dj == dis[j][1]) // 如果等于次短 + cnt[j][1] += cnt[u][k]; // 更新次短的方案数,累加 + else if (dj < dis[j][1]) { // 如果大于最短,小于次短,两者中间 + dis[j][1] = dj; // 更新次短距离 + cnt[j][1] = cnt[u][k]; // 更新次短方案数 + q.push({j, dis[j][1], 1}); // 次短入队列 + } + } + } +} +int main() { +#ifndef ONLINE_JUDGE + freopen("POJ3463.in", "r", stdin); + /* + 答案: + 3 + 2 + */ +#endif + int T; + scanf("%d", &T); + while (T--) { + memset(h, -1, sizeof h); + scanf("%d %d", &n, &m); + while (m--) { + int a, b, c; + scanf("%d %d %d", &a, &b, &c); + add(a, b, c); + } + // 起点和终点 + scanf("%d %d", &s, &f); + // 计算最短路 + dijkrsta(); + // 输出 + printf("%d\n", cnt[f][0] + (dis[f][1] == dis[f][0] + 1 ? cnt[f][1] : 0)); + } + return 0; +} diff --git a/TangDou/AcWing_TiGao/T3/MiniPathExtend/POJ3463.in b/TangDou/AcWing_TiGao/T3/MiniPathExtend/POJ3463.in new file mode 100644 index 0000000..e0fe637 --- /dev/null +++ b/TangDou/AcWing_TiGao/T3/MiniPathExtend/POJ3463.in @@ -0,0 +1,19 @@ +2 +5 8 +1 2 3 +1 3 2 +1 4 5 +2 3 1 +2 5 3 +3 4 2 +3 5 4 +4 5 3 +1 5 +5 6 +2 3 1 +3 2 1 +3 1 10 +4 5 2 +5 2 7 +5 2 7 +4 1 \ No newline at end of file diff --git a/TangDou/AcWing/MinimalPath/1135.cpp b/TangDou/AcWing_TiGao/T3/MiniPathYingYong/1135.cpp similarity index 82% rename from TangDou/AcWing/MinimalPath/1135.cpp rename to TangDou/AcWing_TiGao/T3/MiniPathYingYong/1135.cpp index 841e270..df720b8 100644 --- a/TangDou/AcWing/MinimalPath/1135.cpp +++ b/TangDou/AcWing_TiGao/T3/MiniPathYingYong/1135.cpp @@ -15,7 +15,7 @@ void add(int a, int b, int c) { e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++; } -int dist[6][N]; +int dis[6][N]; int id[6]; // 0号索引:佳佳的家,其它5个亲戚,分别下标为1~5,值为所在的车站编号 /* @@ -26,13 +26,13 @@ bool st[N]; /* S:出发车站编号 - dist[]:是全局变量dist[6][N]的某一个二维,其实是一个一维数组 + dis[]:是全局变量dis[6][N]的某一个二维,其实是一个一维数组 C++的特点:如果数组做参数传递的话,将直接修改原地址的数据 此数组传值方式可以让我们深入理解C++的二维数组本质:就是多个一维数组,给数组头就可以顺序找到其它相关数据 - 计算的结果:获取到S出发到其它各个站点的最短距离,记录到dist[S][站点号]中 + 计算的结果:获取到S出发到其它各个站点的最短距离,记录到dis[S][站点号]中 */ -void dijkstra(int S, int dist[]) { - dist[S] = 0; +void dijkstra(int S, int dis[]) { + dis[S] = 0; memset(st, false, sizeof st); priority_queue, greater> q; q.push({0, S}); @@ -45,9 +45,9 @@ void dijkstra(int S, int dist[]) { st[u] = true; for (int i = h[u]; ~i; i = ne[i]) { int v = e[i]; - if (dist[v] > dist[u] + w[i]) { - dist[v] = dist[u] + w[i]; - q.push({dist[v], v}); + if (dis[v] > dis[u] + w[i]) { + dis[v] = dis[u] + w[i]; + q.push({dis[v], v}); } } } @@ -66,10 +66,10 @@ void dfs(int u, int pre, int sum) { for (int i = 1; i <= 5; i++) // 在当前位置上,枚举每个可能出现在亲戚站点 if (!st[i]) { // 如果这个亲戚没走过 st[i] = true; // 走它 - // 本位置填充完,下一个位置,需要传递前序是i,走过的路径和是sum+dist[pre][id[i]].因为提前打好表了,所以肯定是最小值,直接用就行了  + // 本位置填充完,下一个位置,需要传递前序是i,走过的路径和是sum+dis[pre][id[i]].因为提前打好表了,所以肯定是最小值,直接用就行了  // 需要注意的是一维是 6的上限,也就是 佳佳家+五个亲戚 ,而不是 车站号(佳佳家+五个亲戚) !因为这样的话,数据就很大,数组开起来麻烦,可能会MLE // 要注意学习使用小的数据标号进行事情描述的思想 - dfs(u + 1, i, sum + dist[pre][id[i]]); + dfs(u + 1, i, sum + dis[pre][id[i]]); st[i] = false; // 回溯 } } @@ -91,10 +91,10 @@ int main() { // 计算从某个亲戚所在的车站出发,到达其它几个点的最短路径 // 因为这样会产生多组最短距离,需要一个二维的数组进行存储 - memset(dist, 0x3f, sizeof dist); - for (int i = 0; i < 6; i++) dijkstra(id[i], dist[i]); + memset(dis, 0x3f, sizeof dis); + for (int i = 0; i < 6; i++) dijkstra(id[i], dis[i]); // 枚举每个亲戚所在的车站站点,多次Dijkstra,分别计算出以id[i]这个车站出发,到达其它点的最短距离,相当于打表 - // 将结果距离保存到给定的二维数组dist的第二维中去,第一维是指从哪个车站点出发的意思 + // 将结果距离保存到给定的二维数组dis的第二维中去,第一维是指从哪个车站点出发的意思 // dfs还要用这个st数组做其它用途,所以,需要再次的清空 memset(st, 0, sizeof st); diff --git a/TangDou/AcWing_TiGao/T3/MiniPathYingYong/1135.eddx b/TangDou/AcWing_TiGao/T3/MiniPathYingYong/1135.eddx new file mode 100644 index 0000000..1a75bb9 Binary files /dev/null and b/TangDou/AcWing_TiGao/T3/MiniPathYingYong/1135.eddx differ diff --git a/TangDou/AcWing/MinimalPath/1135.md b/TangDou/AcWing_TiGao/T3/MiniPathYingYong/1135.md similarity index 85% rename from TangDou/AcWing/MinimalPath/1135.md rename to TangDou/AcWing_TiGao/T3/MiniPathYingYong/1135.md index ddb4a8b..bb233b8 100644 --- a/TangDou/AcWing/MinimalPath/1135.md +++ b/TangDou/AcWing_TiGao/T3/MiniPathYingYong/1135.md @@ -57,22 +57,24 @@ $id[1]=8$ 表示第$1$个亲戚家住在$8$号车站附近,记录每个亲戚与 #### 2、思考过程 -① 必须由佳佳的家出发,也就出发点肯定是$1$号车站 -② 现在想求佳佳去$5$个亲戚家,每一家都需要走到,不能漏掉任何一家,但顺序可以任意。这里要要用一个关系数组$id[]$来把亲戚家的编号与车站号挂接一下。 -③ 看到是最短路径问题,而且权值是正整数,考虑唯一可能性就是$Dijkstra$。 +① 必须由佳佳的家出发,也就是出发点肯定是$1$号车站 +② 现在想求佳佳去$5$个亲戚家,每一家都需要走到,不能漏掉任何一家,但顺序可以任意。这里要用一个关系数组$id[]$来把亲戚家的编号与车站号挂接一下。 +③ 看到是最短路径问题,而且权值是正整数,考虑$Dijkstra$。 ④ 但$Dijkstra$只能是单源最短路径求解,比如佳佳去二姨家,最短路径是多少。佳佳去三舅家,最短路径是多少。本题不是问某一家,问的是佳佳全去到,总的路径和最短是多少,这样的话,直接使用$Dijkstra$就无效了。 -⑤ 继续思考:因为亲戚家只有$5$个,可以从这里下手,通过全排列的办法,枚举出所有的可能顺序,此时,计算次数=$5*4*3*2*1=120$次。 就算是跑个$120$次的$Dijkstra$也不是啥大问题,就是常数大一点呗,可以试试。 +⑤ 继续思考:因为亲戚家只有$5$个,可以从这里下手,通过全排列的办法,枚举出所有的可能顺序,此时,计算次数=$5*4*3*2*1=120$次。 ⑥ 跑多次$Dijkstra$是在干什么呢?就是在分别以二姨,三舅,四大爷家为出发点,分别计算出到其它亲戚家的最短距离,如果我们把顺序分别枚举出来,每次查一下已经预处理出来的两个亲戚家的最短距离,再加在一起,不就是可以进行$PK$最小值了吗? 至此,整体思路完成。 +![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/202312281603744.png) + #### 3.编码步骤 * **$6$次最短路** 分别以佳佳家、五个亲戚家为出发点($id[i]~ i\in[0,5]$),求$6$次最短路,相当于打表,一会要查 * **求全排列** - 因为佳佳所有的亲戚都要拜访到,现在不知道的是什么样顺序拜访才是时间最少的。 把所有可能顺序都 **枚举** 出来,通过查表,找出两个亲戚家之间的最小时间,累加结果的和,再$PJ$最小就是答案 + 因为佳佳所有的亲戚都要拜访到,现在不知道的是什么样顺序拜访才是时间最少的。 把所有可能顺序都 **枚举** 出来,通过查表,找出两个亲戚家之间的最小时间,累加结果的和,再$PK$最小就是答案 #### 4.实现细节 通过前面的$6$次打表预处理,可以求出$6$个$dist$数组,当我们需要查找 $1->5$的最短路径时,直接查$dist[1][5]$ @@ -102,7 +104,7 @@ void add(int a, int b, int c) { e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++; } -int dist[6][N]; +int dis[6][N]; int id[6]; // 0号索引:佳佳的家,其它5个亲戚,分别下标为1~5,值为所在的车站编号 /* @@ -113,13 +115,13 @@ bool st[N]; /* S:出发车站编号 - dist[]:是全局变量dist[6][N]的某一个二维,其实是一个一维数组 + dis[]:是全局变量dis[6][N]的某一个二维,其实是一个一维数组 C++的特点:如果数组做参数传递的话,将直接修改原地址的数据 此数组传值方式可以让我们深入理解C++的二维数组本质:就是多个一维数组,给数组头就可以顺序找到其它相关数据 - 计算的结果:获取到S出发到其它各个站点的最短距离,记录到dist[S][站点号]中 + 计算的结果:获取到S出发到其它各个站点的最短距离,记录到dis[S][站点号]中 */ -void dijkstra(int S, int dist[]) { - dist[S] = 0; +void dijkstra(int S, int dis[]) { + dis[S] = 0; memset(st, false, sizeof st); priority_queue, greater> q; q.push({0, S}); @@ -132,9 +134,9 @@ void dijkstra(int S, int dist[]) { st[u] = true; for (int i = h[u]; ~i; i = ne[i]) { int v = e[i]; - if (dist[v] > dist[u] + w[i]) { - dist[v] = dist[u] + w[i]; - q.push({dist[v], v}); + if (dis[v] > dis[u] + w[i]) { + dis[v] = dis[u] + w[i]; + q.push({dis[v], v}); } } } @@ -153,10 +155,10 @@ void dfs(int u, int pre, int sum) { for (int i = 1; i <= 5; i++) // 在当前位置上,枚举每个可能出现在亲戚站点 if (!st[i]) { // 如果这个亲戚没走过 st[i] = true; // 走它 - // 本位置填充完,下一个位置,需要传递前序是i,走过的路径和是sum+dist[pre][id[i]].因为提前打好表了,所以肯定是最小值,直接用就行了  + // 本位置填充完,下一个位置,需要传递前序是i,走过的路径和是sum+dis[pre][id[i]].因为提前打好表了,所以肯定是最小值,直接用就行了  // 需要注意的是一维是 6的上限,也就是 佳佳家+五个亲戚 ,而不是 车站号(佳佳家+五个亲戚) !因为这样的话,数据就很大,数组开起来麻烦,可能会MLE // 要注意学习使用小的数据标号进行事情描述的思想 - dfs(u + 1, i, sum + dist[pre][id[i]]); + dfs(u + 1, i, sum + dis[pre][id[i]]); st[i] = false; // 回溯 } } @@ -178,10 +180,10 @@ int main() { // 计算从某个亲戚所在的车站出发,到达其它几个点的最短路径 // 因为这样会产生多组最短距离,需要一个二维的数组进行存储 - memset(dist, 0x3f, sizeof dist); - for (int i = 0; i < 6; i++) dijkstra(id[i], dist[i]); + memset(dis, 0x3f, sizeof dis); + for (int i = 0; i < 6; i++) dijkstra(id[i], dis[i]); // 枚举每个亲戚所在的车站站点,多次Dijkstra,分别计算出以id[i]这个车站出发,到达其它点的最短距离,相当于打表 - // 将结果距离保存到给定的二维数组dist的第二维中去,第一维是指从哪个车站点出发的意思 + // 将结果距离保存到给定的二维数组dis的第二维中去,第一维是指从哪个车站点出发的意思 // dfs还要用这个st数组做其它用途,所以,需要再次的清空 memset(st, 0, sizeof st); diff --git a/TangDou/AcWing/MinimalPath/340.cpp b/TangDou/AcWing_TiGao/T3/MiniPathYingYong/340.cpp similarity index 53% rename from TangDou/AcWing/MinimalPath/340.cpp rename to TangDou/AcWing_TiGao/T3/MiniPathYingYong/340.cpp index b095692..e359dfe 100644 --- a/TangDou/AcWing/MinimalPath/340.cpp +++ b/TangDou/AcWing_TiGao/T3/MiniPathYingYong/340.cpp @@ -8,39 +8,44 @@ int idx, h[N], e[M], w[M], ne[M]; void add(int a, int b, int c) { e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++; } -int n; //点数 -int m; //边数 -bool st[N]; //记录是不是在队列中 -int k; //不超过K条电缆,由电话公司免费提供升级服务 -int dist[N]; //记录最短距离 -// u指的是我们现在选最小花费 -bool check(int x) { +int n; // 点数 +int m; // 边数 + +int k; // 不超过K条电缆,由电话公司免费提供升级服务 + +bool st[N]; // 记录是不是在队列中 +int dis[N]; // 记录最短距离 + +// mid指的是我们现在选最小花费 +bool check(int mid) { + // 需要跑多次dijkstra,所以需要清空状态数组 memset(st, false, sizeof st); - memset(dist, 0x3f, sizeof dist); + memset(dis, 0x3f, sizeof dis); priority_queue, greater> q; - dist[1] = 0; + dis[1] = 0; q.push({0, 1}); while (q.size()) { PII t = q.top(); q.pop(); - int d = t.first, u = t.second; + int u = t.second; if (st[u]) continue; st[u] = true; for (int i = h[u]; ~i; i = ne[i]) { - int j = e[i], v = w[i] > x; //如果有边比我们现在选的这条边大,那么这条边对方案的贡献为1,反之为0 - if (dist[j] > d + v) { - dist[j] = d + v; - q.push({dist[j], j}); + int j = e[i]; + int v = w[i] > mid; // 如果有边比我们现在选的这条边大,那么这条边对方案的贡献为1,反之为0 + if (dis[j] > dis[u] + v) { + dis[j] = dis[u] + v; + q.push({dis[j], j}); } } } - //如果按上面的方法计算后,n结点没有被松弛操作修改距离,则表示n不可达 - if (dist[n] == INF) { - puts("-1"); //不可达,直接输出-1 + // 如果按上面的方法计算后,n结点没有被松弛操作修改距离,则表示n不可达 + if (dis[n] == INF) { + puts("-1"); // 不可达,直接输出-1 exit(0); } - return dist[n] <= k; //如果有k+1条边比我们现在这条边大,那么这个升级方案就是不合法的,反之就合法 + return dis[n] <= k; // 如果有k+1条边比我们现在这条边大,那么这个升级方案就是不合法的,反之就合法 } int main() { memset(h, -1, sizeof h); @@ -50,11 +55,12 @@ int main() { cin >> a >> b >> c; add(a, b, c), add(b, a, c); } - /*这里二分的是直接面对答案设问:最少花费 + /* + 这里二分的是直接面对答案设问: 至少用多少钱 可以完成升级 依题意,最少花费其实是所有可能的路径中,第k+1条边的花费 如果某条路径不存在k+1条边(边数小于k+1),此时花费为0 - 同时,任意一条边的花费不会大于1e6 - 整理一下,这里二分枚举的值其实是0 ~ 1e6*/ + 同时,任意一条边的花费不会大于1e6,所以,这里二分枚举范围:0 ~ 1e6 + */ int l = 0, r = 1e6; while (l < r) { int mid = (l + r) >> 1; diff --git a/TangDou/AcWing/MinimalPath/340.eddx b/TangDou/AcWing_TiGao/T3/MiniPathYingYong/340.eddx similarity index 100% rename from TangDou/AcWing/MinimalPath/340.eddx rename to TangDou/AcWing_TiGao/T3/MiniPathYingYong/340.eddx diff --git a/TangDou/AcWing/MinimalPath/340.md b/TangDou/AcWing_TiGao/T3/MiniPathYingYong/340.md similarity index 77% rename from TangDou/AcWing/MinimalPath/340.md rename to TangDou/AcWing_TiGao/T3/MiniPathYingYong/340.md index 9685aed..e5ccee9 100644 --- a/TangDou/AcWing/MinimalPath/340.md +++ b/TangDou/AcWing_TiGao/T3/MiniPathYingYong/340.md @@ -75,7 +75,7 @@ $0≤K using namespace std; @@ -87,39 +87,44 @@ int idx, h[N], e[M], w[M], ne[M]; void add(int a, int b, int c) { e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++; } -int n; //点数 -int m; //边数 -bool st[N]; //记录是不是在队列中 -int k; //不超过K条电缆,由电话公司免费提供升级服务 -int dist[N]; //记录最短距离 -// u指的是我们现在选最小花费 -bool check(int x) { +int n; // 点数 +int m; // 边数 + +int k; // 不超过K条电缆,由电话公司免费提供升级服务 + +bool st[N]; // 记录是不是在队列中 +int dis[N]; // 记录最短距离 + +// mid指的是我们现在选最小花费 +bool check(int mid) { + // 需要跑多次dijkstra,所以需要清空状态数组 memset(st, false, sizeof st); - memset(dist, 0x3f, sizeof dist); + memset(dis, 0x3f, sizeof dis); priority_queue, greater> q; - dist[1] = 0; + dis[1] = 0; q.push({0, 1}); while (q.size()) { PII t = q.top(); q.pop(); - int d = t.first, u = t.second; + int u = t.second; if (st[u]) continue; st[u] = true; for (int i = h[u]; ~i; i = ne[i]) { - int j = e[i], v = w[i] > x; //如果有边比我们现在选的这条边大,那么这条边对方案的贡献为1,反之为0 - if (dist[j] > d + v) { - dist[j] = d + v; - q.push({dist[j], j}); + int j = e[i]; + int v = w[i] > mid; // 如果有边比我们现在选的这条边大,那么这条边对方案的贡献为1,反之为0 + if (dis[j] > dis[u] + v) { + dis[j] = dis[u] + v; + q.push({dis[j], j}); } } } - //如果按上面的方法计算后,n结点没有被松弛操作修改距离,则表示n不可达 - if (dist[n] == INF) { - puts("-1"); //不可达,直接输出-1 + // 如果按上面的方法计算后,n结点没有被松弛操作修改距离,则表示n不可达 + if (dis[n] == INF) { + puts("-1"); // 不可达,直接输出-1 exit(0); } - return dist[n] <= k; //如果有k+1条边比我们现在这条边大,那么这个升级方案就是不合法的,反之就合法 + return dis[n] <= k; // 如果有k+1条边比我们现在这条边大,那么这个升级方案就是不合法的,反之就合法 } int main() { memset(h, -1, sizeof h); @@ -129,11 +134,12 @@ int main() { cin >> a >> b >> c; add(a, b, c), add(b, a, c); } - /*这里二分的是直接面对答案设问:最少花费 + /* + 这里二分的是直接面对答案设问: 至少用多少钱 可以完成升级 依题意,最少花费其实是所有可能的路径中,第k+1条边的花费 如果某条路径不存在k+1条边(边数小于k+1),此时花费为0 - 同时,任意一条边的花费不会大于1e6 - 整理一下,这里二分枚举的值其实是0 ~ 1e6*/ + 同时,任意一条边的花费不会大于1e6,所以,这里二分枚举范围:0 ~ 1e6 + */ int l = 0, r = 1e6; while (l < r) { int mid = (l + r) >> 1; diff --git a/TangDou/AcWing/MinimalPath/341.cpp b/TangDou/AcWing_TiGao/T3/MiniPathYingYong/341.cpp similarity index 79% rename from TangDou/AcWing/MinimalPath/341.cpp rename to TangDou/AcWing_TiGao/T3/MiniPathYingYong/341.cpp index e94f362..00d9819 100644 --- a/TangDou/AcWing/MinimalPath/341.cpp +++ b/TangDou/AcWing_TiGao/T3/MiniPathYingYong/341.cpp @@ -5,7 +5,7 @@ const int INF = 0x3f3f3f3f; const int N = 100010, M = 2000010; int n, m; -int dist1[N], dist2[N]; +int dis1[N], dis2[N]; // 正反建图,传入头数组指针 int h1[N], h2[N], e[M], ne[M], w[M], idx; @@ -17,10 +17,10 @@ void add(int *h, int a, int b, int c = 0) { int v[N]; void dijkstra1() { - memset(dist1, 0x3f, sizeof dist1); + memset(dis1, 0x3f, sizeof dis1); priority_queue, greater> q; - dist1[1] = v[1]; - q.push({dist1[1], 1}); + dis1[1] = v[1]; + q.push({dis1[1], 1}); while (q.size()) { int u = q.top().second; @@ -28,28 +28,29 @@ void dijkstra1() { for (int i = h1[u]; ~i; i = ne[i]) { int j = e[i]; - if (dist1[j] > min(dist1[u], v[j])) { - dist1[j] = min(dist1[u], v[j]); - q.push({dist1[j], j}); + if (dis1[j] > min(dis1[u], v[j])) { + dis1[j] = min(dis1[u], v[j]); + q.push({dis1[j], j}); } } } } void dijkstra2() { - memset(dist2, -0x3f, sizeof dist2); + memset(dis2, -0x3f, sizeof dis2); priority_queue q; - dist2[n] = v[n]; - q.push({dist2[n], n}); + dis2[n] = v[n]; + q.push({dis2[n], n}); while (q.size()) { int u = q.top().second; q.pop(); + for (int i = h2[u]; ~i; i = ne[i]) { int j = e[i]; - if (dist2[j] < max(dist2[u], v[j])) { - dist2[j] = max(dist2[u], v[j]); - q.push({dist2[j], j}); + if (dis2[j] < max(dis2[u], v[j])) { + dis2[j] = max(dis2[u], v[j]); + q.push({dis2[j], j}); } } } @@ -86,12 +87,13 @@ int main() { } // 正向图跑一遍dijkstra dijkstra1(); + // 反向图跑一遍dijkstra dijkstra2(); int ans = 0; for (int i = 1; i <= n; i++) - ans = max(dist2[i] - dist1[i], ans); + ans = max(dis2[i] - dis1[i], ans); printf("%d\n", ans); return 0; diff --git a/TangDou/AcWing/MinimalPath/341.md b/TangDou/AcWing_TiGao/T3/MiniPathYingYong/341.md similarity index 72% rename from TangDou/AcWing/MinimalPath/341.md rename to TangDou/AcWing_TiGao/T3/MiniPathYingYong/341.md index 5c30e11..8279dad 100644 --- a/TangDou/AcWing/MinimalPath/341.md +++ b/TangDou/AcWing_TiGao/T3/MiniPathYingYong/341.md @@ -66,38 +66,39 @@ $1≤$各城市水晶球价格$≤100$ **阿龙决定从 $1$ 号城市出发,并最终在 $n$ 号城市结束自己的旅行。** -卖完后要回到点$n$,然而,题目并没有保证所有点都能去到点$n$,而且,**不是所有边都是无向边**。 +终点是$n$,但题目并没有保证所有点都能去到点$n$。 要知道哪些点不能去到点$n$,可以 **反向建图**,在这张图以$n$为起点看能到达哪些点。 -分析: 这道题需要建两个图,一个为 **正向图** ,一个为 **反向图** ,考虑分别跑最短路变形得到$dist1$数组和$dist2$数组: -* $dist1[i]$表示从点$1$到点$i$的所有路径上经过的 **最小点权** -* $dist2[i]$表示从点$n$经过反向边到点$i$的所有路径上经过的 **最大点权**。 +**分析**: 这道题需要建两个图,一个为 **正向图** ,一个为 **反向图** ,考虑分别跑$Dijkstra$算法得到$dis1$数组和$dis2$数组: + +* $dis1[i]$:从点$1$到点$i$的所有路径上经过的 **最小点权** +* $dis2[i]$:从点$n$经过反向边到点$i$的所有路径上经过的 **最大点权** 当求出这两个数组后就可以枚举路径上的 **中间点**$i$,最终答案就是 -$$\large max(dist2[i]-dis1t[i])$$ +$$\large max(dis2[i]-dis1[i])$$ -考虑如何通过最短路求出$dist$数组,常规思路就是 **最短路变形**,把松弛条件改为: -```cpp {.line-numbers} -if(dist1[j] > min(dist1[u], v[j])){ - dist1[j] = min(dist1[u], v[j]); - q.push({dist1[j], j}); -} -``` -**理论** 上这就没问题了,不过这道题目比较特殊,由于图中 **可能出现回路**,且$dist$值是记录 **点权的最值** ,在某些情况下是 **具有后效性**的,如下图: +**理论** 上这就没问题了,不过这道题目比较特殊,由于图中 **可能出现回路**,且$dis$值是记录 **点权的最值** ,在某些情况下是 **具有后效性**的,如下图:
-**点权** 用绿色数字标示在点号下方,可以发现在点$2$处会经过一个回路再次回到点$2$,但在这之前点$5$的$dist$已经被更新为$3$了,之后回到点$2$,由于$st[2] == true$直接$continue$,虽然此时$dist[2] == 1$但却无法把$1$传递给点$5$了。 +**点权** 用绿色数字标示在点号下方,可以发现在点$2$处会经过一个回路再次回到点$2$,但在这之前点$5$的$dis$已经被更新为$3$了 +>解释:因为$1 \rightarrow 2 \rightarrow 5$这条路线上,在点$2$时,水晶球的价格最便宜,价格是$3$ -**解决方法**:$dijkstra$算法中去掉$st$的限制,让整个算法不断迭代,直到无法更新导致队空退出循环。 +之后回到点$2$,由于$st[2] == true$直接$continue$,虽然此时$dis[2] == 1$但却无法把$1$传递给点$5$了。 + +采用办法 +在$dijkstra$算法中去掉$st$的限制,让整个算法不断迭代,直到无法更新导致队空退出循环。这就类似于$DP$的所有情况尝试,不断刷新最新最小价格! **总结** -本题用$Dijkstra$的话,其实已经不是传统意义上的$Dijkstra$了,因为它允许出边再进入队列!(去掉了$st$数组 ,因为有环嘛),指望 **更无可更,无需再更**。这么用$Dijkstra$其实就不如用$SPFA$来的直接了,$SPFA$本身就是更无可更,无需再更。 +本题用$Dijkstra$的话,其实已经不是传统意义上的$Dijkstra$了,因为它允许出边再进入队列!(去掉了$st$数组 ,因为有环嘛),指望 **更无可更,无需再更**。 + +**最大最小值**,其实也不是传统最短、最长路的路径累加和,而是类似于$DP$的思路,一路走来一路维护到达当前点的最大点权和最小点权。 -**最大最小值**,其实也不是传统最短、最长路的路径累加和,而是类似于$DP$的思路,一路走来一路维护到达当前点的最大点权和最小点权。严格意义上来讲,采用的$Dijkstra$或$SPFA$都不是本身的含义,只是一个协助$DP$的枚举过程。 +**配合$DP$** +严格意义上来讲,采用的$Dijkstra$不是本身的含义,只是一个协助$DP$的枚举过程。 #### $Code$ ```cpp {.line-numbers} @@ -108,21 +109,22 @@ const int INF = 0x3f3f3f3f; const int N = 100010, M = 2000010; int n, m; -int dist1[N], dist2[N]; +int dis1[N], dis2[N]; // 正反建图,传入头数组指针 int h1[N], h2[N], e[M], ne[M], w[M], idx; -void add(int *hh, int a, int b, int c = 0) { - e[idx] = b, ne[idx] = hh[a], w[idx] = c, hh[a] = idx++; +void add(int *h, int a, int b, int c = 0) { + e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++; } + // 每个节点的价值 int v[N]; void dijkstra1() { - memset(dist1, 0x3f, sizeof dist1); + memset(dis1, 0x3f, sizeof dis1); priority_queue, greater> q; - dist1[1] = v[1]; - q.push({dist1[1], 1}); + dis1[1] = v[1]; + q.push({dis1[1], 1}); while (q.size()) { int u = q.top().second; @@ -130,28 +132,29 @@ void dijkstra1() { for (int i = h1[u]; ~i; i = ne[i]) { int j = e[i]; - if (dist1[j] > min(dist1[u], v[j])) { - dist1[j] = min(dist1[u], v[j]); - q.push({dist1[j], j}); + if (dis1[j] > min(dis1[u], v[j])) { + dis1[j] = min(dis1[u], v[j]); + q.push({dis1[j], j}); } } } } void dijkstra2() { - memset(dist2, -0x3f, sizeof dist2); + memset(dis2, -0x3f, sizeof dis2); priority_queue q; - dist2[n] = v[n]; - q.push({dist2[n], n}); + dis2[n] = v[n]; + q.push({dis2[n], n}); while (q.size()) { int u = q.top().second; q.pop(); + for (int i = h2[u]; ~i; i = ne[i]) { int j = e[i]; - if (dist2[j] < max(dist2[u], v[j])) { - dist2[j] = max(dist2[u], v[j]); - q.push({dist2[j], j}); + if (dis2[j] < max(dis2[u], v[j])) { + dis2[j] = max(dis2[u], v[j]); + q.push({dis2[j], j}); } } } @@ -188,12 +191,13 @@ int main() { } // 正向图跑一遍dijkstra dijkstra1(); + // 反向图跑一遍dijkstra dijkstra2(); int ans = 0; for (int i = 1; i <= n; i++) - ans = max(dist2[i] - dist1[i], ans); + ans = max(dis2[i] - dis1[i], ans); printf("%d\n", ans); return 0; diff --git a/TangDou/AcWing/MinimalPath/342.cpp b/TangDou/AcWing_TiGao/T3/MiniPathYingYong/342.cpp similarity index 83% rename from TangDou/AcWing/MinimalPath/342.cpp rename to TangDou/AcWing_TiGao/T3/MiniPathYingYong/342.cpp index 917fec2..ad895fa 100644 --- a/TangDou/AcWing/MinimalPath/342.cpp +++ b/TangDou/AcWing_TiGao/T3/MiniPathYingYong/342.cpp @@ -20,7 +20,7 @@ int id[N]; // 节点在哪个连通块中 vector block[N]; // 连通块包含哪些节点 int bcnt; // 连通块序号计数器 -int dist[N]; // 最短距离(结果数组) +int dis[N]; // 最短距离(结果数组) int in[N]; // 每个DAG(节点即连通块)的入度 bool st[N]; // dijkstra用的是不是在队列中的数组 queue q; // 拓扑序用的队列 @@ -31,8 +31,8 @@ void dfs(int u, int bid) { block[bid].push_back(u); // ② 记录bid团包含u节点 // 枚举u节点的每一条出边,将对端的城镇也加入到bid这个团中 for (int i = h[u]; ~i; i = ne[i]) { - int v = e[i]; - if (!id[v]) dfs(v, bid); // Flood Fill + int j = e[i]; + if (!id[j]) dfs(j, bid); // Flood Fill } } @@ -41,12 +41,12 @@ void dijkstra(int bid) { priority_queue, greater> pq; /* 因为不确定连通块内的哪个点可以作为起点,所以就一股脑全加进来就行了, - 反正很多点的dist都是inf(这些都是不能成为起点的),那么可以作为起点的就自然出现在堆顶了 + 反正很多点的dis都是inf(这些都是不能成为起点的),那么可以作为起点的就自然出现在堆顶了 因为上面的写法把拓扑排序和dijkstra算法拼在一起了,如果不把所有点都加入堆, 会导致后面其他块的din[]没有减去前驱边,从而某些块没有被拓扑排序遍历到。 */ - for (auto u : block[bid]) pq.push({dist[u], u}); + for (auto u : block[bid]) pq.push({dis[u], u}); while (pq.size()) { int u = pq.top().second; @@ -57,10 +57,10 @@ void dijkstra(int bid) { int v = e[i]; if (st[v]) continue; - if (dist[v] > dist[u] + w[i]) { - dist[v] = dist[u] + w[i]; + if (dis[v] > dis[u] + w[i]) { + dis[v] = dis[u] + w[i]; // 如果是同团中的道路,需要再次进入Dijkstra的小顶堆,以便计算完整个团中的路径最小值 - if (id[u] == id[v]) pq.push({dist[v], v}); + if (id[u] == id[v]) pq.push({dis[v], v}); } /*如果u和v不在同一个团中,说明遍历到的是航线 此时,需要与拓扑序算法结合,尝试剪掉此边,是不是可以形成入度为的团 @@ -92,12 +92,12 @@ int main() { memset(h, -1, sizeof h); // 初始化 scanf("%d %d %d %d", &T, &R, &P, &S); // 城镇数量,道路数量,航线数量,出发点 - memset(dist, 0x3f, sizeof dist); // 初始化最短距离 - dist[S] = 0; // 出发点距离自己的长度是0,其它的最短距离目前是INF + memset(dis, 0x3f, sizeof dis); // 初始化最短距离 + dis[S] = 0; // 出发点距离自己的长度是0,其它的最短距离目前是INF int a, b, c; // 起点,终点,权值 - while (R--) { // 读入道路 + while (R--) { // 读入道路,团内无向图 scanf("%d %d %d", &a, &b, &c); add(a, b, c), add(b, a, c); // 连通块内是无向图 } @@ -124,18 +124,18 @@ int main() { while (P--) { scanf("%d %d %d", &a, &b, &c); add(a, b, c); // 单向边 - in[id[b]]++; // b节点所在团入度+1 + in[id[b]]++; // b节点所在团的番号,也就是某个团的入度+1 } - // 拓扑序 + // 拓扑 topsort(); // 从S到达城镇i的最小花费 for (int i = 1; i <= T; i++) { - if (dist[i] > INF / 2) + if (dis[i] > INF / 2) puts("NO PATH"); else - cout << dist[i] << endl; + cout << dis[i] << endl; } return 0; } \ No newline at end of file diff --git a/TangDou/AcWing/MinimalPath/342.md b/TangDou/AcWing_TiGao/T3/MiniPathYingYong/342.md similarity index 82% rename from TangDou/AcWing/MinimalPath/342.md rename to TangDou/AcWing_TiGao/T3/MiniPathYingYong/342.md index e665252..a8ba284 100644 --- a/TangDou/AcWing/MinimalPath/342.md +++ b/TangDou/AcWing_TiGao/T3/MiniPathYingYong/342.md @@ -34,18 +34,18 @@ **输出格式** 第 $1..T$ 行:第 $i$ 行输出从 $S$ 到达城镇 $i$ 的最小花费,如果不存在,则输出 `NO PATH`。 -### 二、$Dijkstra$不能处理负权边,但可以处理负权初值 +### 二、$Dijkstra$不能处理负权边 我们说了$Dijkstra$算法不能解决带有负权边的图,这是为什么呢?下面用一个例子讲解一下 ![](https://img-blog.csdnimg.cn/51115e15bd3040d7a8b3980b523ed943.png) 以这里图为例,一共有五个点,也就说要循环$5$次,确定每个点的最短距离 用$Dijkstra$算法解决的的详细步骤 -> 1. 初始$dist[1] = 0$,$1$号点距离起点$1$的距离为$0$ -> 2. 找到了未标识且离起点$1$最近的结点$1$,标记$1$号点,用$1$号点更新和它相连点的距离,$2$号点被更新成$dist[2] = 2$,$3$号点被更新成$dist[3] = 5$ -> 3. 找到了未标识且离起点$1$最近的结点$2$,标识$2$号点,用$2$号点更新和它相连点的距离,$4$号点被更新成$dist[4] = 4$ -> 4. 找到了未标识且离起点$1$最近的结点$4$,标识$4$号点,用$4$号点更新和它相连点的距离,$5$号点被更新成$dist[5] = 5$ -> 5. 找到了未标识且离起点$1$最近的结点$3$,标识$3$号点,用$3$号点更新和它相连点的距离,$4$号点被更新成$dist[4] = 3$ +> 1. 初始$dis[1] = 0$,$1$号点距离起点$1$的距离为$0$ +> 2. 找到了未标识且离起点$1$最近的结点$1$,标记$1$号点,用$1$号点更新和它相连点的距离,$2$号点被更新成$dis[2] = 2$,$3$号点被更新成$dis[3] = 5$ +> 3. 找到了未标识且离起点$1$最近的结点$2$,标识$2$号点,用$2$号点更新和它相连点的距离,$4$号点被更新成$dis[4] = 4$ +> 4. 找到了未标识且离起点$1$最近的结点$4$,标识$4$号点,用$4$号点更新和它相连点的距离,$5$号点被更新成$dis[5] = 5$ +> 5. 找到了未标识且离起点$1$最近的结点$3$,标识$3$号点,用$3$号点更新和它相连点的距离,$4$号点被更新成$dis[4] = 3$ > **结果** @@ -56,10 +56,8 @@ > 我们可以发现如果有负权边的话$4$号点经过标记后还可以继续更新 但此时$4$号点已经被标记过了,所以$4$号点不能被更新了,只能一条路走到黑 当用负权边更新$4$号点后$5$号点距离起点的距离我们可以发现可以进一步缩小成$4$。 -所以总结下来就是:$dijkstra$**不能解决负权边** 是因为 $dijkstra$要求每个点被确定后,$dist[j]$就是最短距离了,之后就不能再被更新了(**一锤子买卖**),而如果有负权边的话,那已经确定的点的$dist[j]$不一定是最短了,可能还可以通过负权边进行更新。 +所以总结下来就是:$dijkstra$ **不能解决负权边** 是因为 $dijkstra$要求每个点被确定后,$dis[j]$就是最短距离了,之后就不能再被更新了(**一锤子买卖**),而如果有负权边的话,那已经确定的点的$dis[j]$不一定是最短了,可能还可以通过负权边进行更新。 -**负权初始值** -那如果不是负权的边长,而是负权的初值呢?这个就没关系了,因为初值不影响算法逻辑,不信你看下有好多算法题都是判断$INF/2$,正无穷不也是在过程中松弛操作更改过吗,你是负的初始值也是没有问题,可以正确运行算法。 ### 三、拓扑序+$Dijkstra$ + 缩点 @@ -74,6 +72,8 @@ #### 算法步骤
+扩展:用拓扑排序解决dag带负权图的最短路问题 +![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/202312291324775.png) #### $Code$ ```cpp {.line-numbers} @@ -99,7 +99,7 @@ int id[N]; // 节点在哪个连通块中 vector block[N]; // 连通块包含哪些节点 int bcnt; // 连通块序号计数器 -int dist[N]; // 最短距离(结果数组) +int dis[N]; // 最短距离(结果数组) int in[N]; // 每个DAG(节点即连通块)的入度 bool st[N]; // dijkstra用的是不是在队列中的数组 queue q; // 拓扑序用的队列 @@ -110,8 +110,8 @@ void dfs(int u, int bid) { block[bid].push_back(u); // ② 记录bid团包含u节点 // 枚举u节点的每一条出边,将对端的城镇也加入到bid这个团中 for (int i = h[u]; ~i; i = ne[i]) { - int v = e[i]; - if (!id[v]) dfs(v, bid); // Flood Fill + int j = e[i]; + if (!id[j]) dfs(j, bid); // Flood Fill } } @@ -120,12 +120,12 @@ void dijkstra(int bid) { priority_queue, greater> pq; /* 因为不确定连通块内的哪个点可以作为起点,所以就一股脑全加进来就行了, - 反正很多点的dist都是inf(这些都是不能成为起点的),那么可以作为起点的就自然出现在堆顶了 + 反正很多点的dis都是inf(这些都是不能成为起点的),那么可以作为起点的就自然出现在堆顶了 因为上面的写法把拓扑排序和dijkstra算法拼在一起了,如果不把所有点都加入堆, 会导致后面其他块的din[]没有减去前驱边,从而某些块没有被拓扑排序遍历到。 */ - for (auto u : block[bid]) pq.push({dist[u], u}); + for (auto u : block[bid]) pq.push({dis[u], u}); while (pq.size()) { int u = pq.top().second; @@ -136,10 +136,10 @@ void dijkstra(int bid) { int v = e[i]; if (st[v]) continue; - if (dist[v] > dist[u] + w[i]) { - dist[v] = dist[u] + w[i]; + if (dis[v] > dis[u] + w[i]) { + dis[v] = dis[u] + w[i]; // 如果是同团中的道路,需要再次进入Dijkstra的小顶堆,以便计算完整个团中的路径最小值 - if (id[u] == id[v]) pq.push({dist[v], v}); + if (id[u] == id[v]) pq.push({dis[v], v}); } /*如果u和v不在同一个团中,说明遍历到的是航线 此时,需要与拓扑序算法结合,尝试剪掉此边,是不是可以形成入度为的团 @@ -171,12 +171,12 @@ int main() { memset(h, -1, sizeof h); // 初始化 scanf("%d %d %d %d", &T, &R, &P, &S); // 城镇数量,道路数量,航线数量,出发点 - memset(dist, 0x3f, sizeof dist); // 初始化最短距离 - dist[S] = 0; // 出发点距离自己的长度是0,其它的最短距离目前是INF + memset(dis, 0x3f, sizeof dis); // 初始化最短距离 + dis[S] = 0; // 出发点距离自己的长度是0,其它的最短距离目前是INF int a, b, c; // 起点,终点,权值 - while (R--) { // 读入道路 + while (R--) { // 读入道路,团内无向图 scanf("%d %d %d", &a, &b, &c); add(a, b, c), add(b, a, c); // 连通块内是无向图 } @@ -203,18 +203,18 @@ int main() { while (P--) { scanf("%d %d %d", &a, &b, &c); add(a, b, c); // 单向边 - in[id[b]]++; // b节点所在团入度+1 + in[id[b]]++; // b节点所在团的番号,也就是某个团的入度+1 } - // 拓扑序 + // 拓扑 topsort(); // 从S到达城镇i的最小花费 for (int i = 1; i <= T; i++) { - if (dist[i] > INF / 2) + if (dis[i] > INF / 2) puts("NO PATH"); else - cout << dist[i] << endl; + cout << dis[i] << endl; } return 0; } diff --git a/TangDou/AcWing_TiGao/T4/1250.cpp b/TangDou/AcWing_TiGao/T4/1250.cpp new file mode 100644 index 0000000..191abe0 --- /dev/null +++ b/TangDou/AcWing_TiGao/T4/1250.cpp @@ -0,0 +1,50 @@ +#include +using namespace std; + +const int N = 200 * 200 + 10; + +int n, m; +int p[N]; + +// 二维转一维的办法,坐标从(1,1)开始 +int get(int x, int y) { + return (x - 1) * n + y; +} + +// 最简并查集 +int find(int x) { + if (p[x] != x) p[x] = find(p[x]); // 路径压缩 + return p[x]; +} +int main() { + scanf("%d %d", &n, &m); + for (int i = 1; i <= n * n; i++) p[i] = i; // 并查集初始化 + + int res = 0; + for (int i = 1; i <= m; i++) { + int x, y; + char d; + cin >> x >> y >> d; + int a = get(x, y); // 计算a点点号 + int b; + if (d == 'D') // 向下走 + b = get(x + 1, y); + else // 向右走 + b = get(x, y + 1); + + // a,b需要两次相遇,才是出现了环~ + int pa = find(a), pb = find(b); + if (pa == pb) { + res = i; // 记录操作步数 + break; + } + // 合并并查集 + p[pa] = pb; + } + + if (!res) // 没有修改过这个值 + puts("draw"); // 平局 + else // 输出操作步数 + printf("%d\n", res); + return 0; +} \ No newline at end of file diff --git a/TangDou/AcWing/DisjointSet/1250.md b/TangDou/AcWing_TiGao/T4/1250.md similarity index 68% rename from TangDou/AcWing/DisjointSet/1250.md rename to TangDou/AcWing_TiGao/T4/1250.md index 5841525..d5eae4d 100644 --- a/TangDou/AcWing/DisjointSet/1250.md +++ b/TangDou/AcWing_TiGao/T4/1250.md @@ -45,9 +45,8 @@ $1≤n≤200, ``` ### 二、解题思路 -要想形成环,必须保证正在连的这条边,将原来已经半封闭的一个“半环”连通,即原来这个半环是一集合,$a,b$再次相边,那么之前$a,b$在同一集合中。 -**总结**: **判断是否成环就直接判断他们没连接前他们的祖宗结点是否一致,如果一致连接起来就必然成为环。** +**判断是否成环,可以判断他们没连接前,他们的祖宗结点是否一致,如果一致,连接起来就必然成环。** ### 三、实现代码 ```cpp {.line-numbers} @@ -59,45 +58,45 @@ const int N = 200 * 200 + 10; int n, m; int p[N]; -//二维转一维的办法,坐标从(1,1)开始 -inline int get(int x, int y) { +// 二维转一维的办法,坐标从(1,1)开始 +int get(int x, int y) { return (x - 1) * n + y; } -//最简并查集 +// 最简并查集 int find(int x) { - if (p[x] != x) p[x] = find(p[x]); //路径压缩 + if (p[x] != x) p[x] = find(p[x]); // 路径压缩 return p[x]; } int main() { - cin >> n >> m; - for (int i = 1; i <= n * n; i++) p[i] = i; + scanf("%d %d", &n, &m); + for (int i = 1; i <= n * n; i++) p[i] = i; // 并查集初始化 int res = 0; for (int i = 1; i <= m; i++) { int x, y; char d; cin >> x >> y >> d; - int a = get(x, y); //计算a点点号 + int a = get(x, y); // 计算a点点号 int b; - if (d == 'D') //向下走 + if (d == 'D') // 向下走 b = get(x + 1, y); - else //向右走 + else // 向右走 b = get(x, y + 1); // a,b需要两次相遇,才是出现了环~ int pa = find(a), pb = find(b); if (pa == pb) { - res = i; //记录操作步数 + res = i; // 记录操作步数 break; } - //合并并查集 + // 合并并查集 p[pa] = pb; } - if (!res) //没有修改过这个值 - puts("draw"); //平局 - else //输出操作步数 + if (!res) // 没有修改过这个值 + puts("draw"); // 平局 + else // 输出操作步数 printf("%d\n", res); return 0; } diff --git a/TangDou/AcWing/DisjointSet/1252.cpp b/TangDou/AcWing_TiGao/T4/1252.cpp similarity index 92% rename from TangDou/AcWing/DisjointSet/1252.cpp rename to TangDou/AcWing_TiGao/T4/1252.cpp index 7583878..a01d631 100644 --- a/TangDou/AcWing/DisjointSet/1252.cpp +++ b/TangDou/AcWing_TiGao/T4/1252.cpp @@ -14,13 +14,13 @@ int find(int x) { } int main() { - cin >> n >> m >> sum; + scanf("%d %d %d", &n, &m, &sum); // 初始化并查集 for (int i = 1; i <= n; i++) p[i] = i; // 读入每个云朵的价钱(体积)和价值 for (int i = 1; i <= n; i++) - cin >> v[i] >> w[i]; + scanf("%d %d", &v[i], &w[i]); while (m--) { int a, b; @@ -35,7 +35,7 @@ int main() { } // 01背包 // 注意:这里不能认为一维的能AC,二维的替代写法就一定能AC - // 这是因为这里的判断p[i]==i,导致i不一定是连通的, + // 这是因为这里的判断p[i]==i,导致i不一定是连续的, // 所以f[i][j]=f[i-1][j]这句话就不一定对 // 所以,看来终极版本的01背包一维解法还是有一定价值的。 for (int i = 1; i <= n; i++) diff --git a/TangDou/AcWing/DisjointSet/1252.md b/TangDou/AcWing_TiGao/T4/1252.md similarity index 83% rename from TangDou/AcWing/DisjointSet/1252.md rename to TangDou/AcWing_TiGao/T4/1252.md index 1a196f2..f745490 100644 --- a/TangDou/AcWing/DisjointSet/1252.md +++ b/TangDou/AcWing_TiGao/T4/1252.md @@ -49,32 +49,32 @@ $1≤n≤10000,0≤m≤5000,1≤w≤10000,1≤ci≤5000,1≤di≤100,1≤u_i,v_i using namespace std; const int N = 10010; -int n, m, sum; //有 n 朵云,m 个搭配,Joe有 sum 的钱。 -int v[N], w[N]; //表示 i 朵云的价钱和价值 -int p[N]; -int f[N]; +int n, m, sum; // 有 n 朵云,m 个搭配,Joe有 sum 的钱。 +int v[N], w[N]; // 表示 i 朵云的价钱和价值 +int p[N]; // 并查集数组 +int f[N]; // 01背包数组 -//最简并查集 +// 最简并查集 int find(int x) { - if (p[x] != x) p[x] = find(p[x]); //路径压缩 + if (p[x] != x) p[x] = find(p[x]); // 路径压缩 return p[x]; } int main() { - cin >> n >> m >> sum; - //初始化并查集 + scanf("%d %d %d", &n, &m, &sum); + // 初始化并查集 for (int i = 1; i <= n; i++) p[i] = i; - //读入每个云朵的价钱(体积)和价值 + // 读入每个云朵的价钱(体积)和价值 for (int i = 1; i <= n; i++) - cin >> v[i] >> w[i]; + scanf("%d %d", &v[i], &w[i]); while (m--) { int a, b; - cin >> a >> b; //两种云朵需要一起买 + cin >> a >> b; // 两种云朵需要一起买 int pa = find(a), pb = find(b); if (pa != pb) { - //集合有两个属性:总价钱、总价值,都记录到root节点上 + // 集合有两个属性:总价钱、总价值,都记录到root节点上 v[pb] += v[pa]; w[pb] += w[pa]; p[pa] = pb; @@ -82,14 +82,14 @@ int main() { } // 01背包 // 注意:这里不能认为一维的能AC,二维的替代写法就一定能AC - // 这是因为这里的判断p[i]==i,导致i不一定是连通的, + // 这是因为这里的判断p[i]==i,导致i不一定是连续的, // 所以f[i][j]=f[i-1][j]这句话就不一定对 // 所以,看来终极版本的01背包一维解法还是有一定价值的。 for (int i = 1; i <= n; i++) - if (p[i] == i) //只关心集合代表元素,选择一组 - for (int j = sum; j >= v[i]; j--) //体积由大到小,倒序,01背包 + if (p[i] == i) // 只关心集合代表元素,选择一组 + for (int j = sum; j >= v[i]; j--) // 体积由大到小,倒序,01背包 f[j] = max(f[j], f[j - v[i]] + w[i]); - //输出最大容量下获取到的价值 + // 输出最大容量下获取到的价值 printf("%d\n", f[sum]); return 0; } diff --git a/TangDou/AcWing/DisjointSet/1252_ErWei.cpp b/TangDou/AcWing_TiGao/T4/1252_ErWei.cpp similarity index 100% rename from TangDou/AcWing/DisjointSet/1252_ErWei.cpp rename to TangDou/AcWing_TiGao/T4/1252_ErWei.cpp diff --git a/TangDou/AcWing/DisjointSet/237.cpp b/TangDou/AcWing_TiGao/T4/237.cpp similarity index 100% rename from TangDou/AcWing/DisjointSet/237.cpp rename to TangDou/AcWing_TiGao/T4/237.cpp diff --git a/TangDou/AcWing/DisjointSet/237.md b/TangDou/AcWing_TiGao/T4/237.md similarity index 100% rename from TangDou/AcWing/DisjointSet/237.md rename to TangDou/AcWing_TiGao/T4/237.md diff --git a/TangDou/AcWing/DisjointSet/238.cpp b/TangDou/AcWing_TiGao/T4/238.cpp similarity index 100% rename from TangDou/AcWing/DisjointSet/238.cpp rename to TangDou/AcWing_TiGao/T4/238.cpp diff --git a/TangDou/AcWing/DisjointSet/238.md b/TangDou/AcWing_TiGao/T4/238.md similarity index 100% rename from TangDou/AcWing/DisjointSet/238.md rename to TangDou/AcWing_TiGao/T4/238.md diff --git a/TangDou/AcWing/DisjointSet/239.md b/TangDou/AcWing_TiGao/T4/239.md similarity index 100% rename from TangDou/AcWing/DisjointSet/239.md rename to TangDou/AcWing_TiGao/T4/239.md diff --git a/TangDou/AcWing/DisjointSet/239_DaiQuan_STL.cpp b/TangDou/AcWing_TiGao/T4/239_DaiQuan_STL.cpp similarity index 100% rename from TangDou/AcWing/DisjointSet/239_DaiQuan_STL.cpp rename to TangDou/AcWing_TiGao/T4/239_DaiQuan_STL.cpp diff --git a/TangDou/AcWing/DisjointSet/239_DaiQuan_Static_ErFen.cpp b/TangDou/AcWing_TiGao/T4/239_DaiQuan_Static_ErFen.cpp similarity index 100% rename from TangDou/AcWing/DisjointSet/239_DaiQuan_Static_ErFen.cpp rename to TangDou/AcWing_TiGao/T4/239_DaiQuan_Static_ErFen.cpp diff --git a/TangDou/AcWing/DisjointSet/239_ExtendDomain_STL.cpp b/TangDou/AcWing_TiGao/T4/239_ExtendDomain_STL.cpp similarity index 100% rename from TangDou/AcWing/DisjointSet/239_ExtendDomain_STL.cpp rename to TangDou/AcWing_TiGao/T4/239_ExtendDomain_STL.cpp diff --git a/TangDou/AcWing/DisjointSet/239_ExtendDomain_Static_ErFen.cpp b/TangDou/AcWing_TiGao/T4/239_ExtendDomain_Static_ErFen.cpp similarity index 100% rename from TangDou/AcWing/DisjointSet/239_ExtendDomain_Static_ErFen.cpp rename to TangDou/AcWing_TiGao/T4/239_ExtendDomain_Static_ErFen.cpp diff --git a/TangDou/AcWing/Math/GameTheory/1319.cpp b/TangDou/AcWing_TiGao/T5/GameTheory/1319.cpp similarity index 100% rename from TangDou/AcWing/Math/GameTheory/1319.cpp rename to TangDou/AcWing_TiGao/T5/GameTheory/1319.cpp diff --git a/TangDou/AcWing_TiGao/T5/GameTheory/1319.md b/TangDou/AcWing_TiGao/T5/GameTheory/1319.md new file mode 100644 index 0000000..c707378 --- /dev/null +++ b/TangDou/AcWing_TiGao/T5/GameTheory/1319.md @@ -0,0 +1,170 @@ +##[$AcWing$ $1319$. 移棋子游戏](https://www.acwing.com/problem/content/description/1321/) + +### 一、题目描述 +给定一个有 $N$ 个节点的 **有向无环图**,图中某些节点上有棋子,两名玩家交替移动棋子。 + +玩家每一步可将任意一颗棋子沿一条有向边移动到另一个点,无法移动者输掉游戏。 + +对于给定的图和棋子初始位置,双方都会采取最优的行动,询问先手必胜还是先手必败。 + +**输入格式** +第一行,三个整数 $N,M,K$,$N$ 表示图中节点总数,$M$ 表示图中边的条数,$K$ 表示棋子的个数。 + +接下来 $M$ 行,每行两个整数 $X,Y$ 表示有一条边从点 $X$ 出发指向点 $Y$。 + +接下来一行, $K$ 个空格间隔的整数,表示初始时,棋子所在的节点编号。 + +节点编号从 $1$ 到 $N$。 + +**输出格式** +若先手胜,输出 `win`,否则输出 `lose`。 + +**数据范围** +$1≤N≤2000,1≤M≤6000,1≤K≤N$ + +**输入样例:** +```cpp {.line-numbers} +6 8 4 +2 1 +2 4 +1 4 +1 5 +4 5 +1 3 +3 5 +3 6 +1 2 4 6 +``` + +**输出样例:** +```cpp {.line-numbers} +win +``` + +### 二、解题思路 + +#### 1、$SG$函数 +首先定义 $mex$ 函数,这是施加于一个集合的函数,返回 **最小的不属于这个集合的非负整数** + +例:$mex({1,2})=0,mex({0,1})=2,mex({0,1,2,4})=3$ + +在一张有向无环图中,对于每个点 $u$,设其 **所有能到的点** 的 $SG$ 函数值集合为集合 $A$,那么 $u$ 的 $SG$ 函数值为 $mex(A)$,记做 $SG(u)=mex(A)$ + +如图: +
+ +**例图解释**: + +$SG(5)=mex({\phi})=0$ +$SG(3)=mex({SG(5)})=mex({0})=1$ +$SG(4)=mex({SG(5),SG(3)})=mex({0,1})=2$ +$SG(2)=mex({SG(3)})=mex({1})=0$ +$SG(1)=mex({SG(2),SG(4)})=mex({0,2})=1$ + +#### 2、本题和 $SG$ 函数有什么关系? +下面先说本题做法,再证明该方法正确性。 + +**做法**:求出每个棋子所在的点的 $SG$ 函数值,将所有值异或起来。若异或值不为 $0$,则输出$win$,否则输出$lose$ + +**证明**: +首先,由于这是一张有向无环图,所以游戏最后一定会结束,也就是说每个棋子最后都会移动到一个点上,且该点没有任何能到达的点。 + +根据定义,结束状态的所有点的 $SG$ 函数值异或起来为 $0$,**做法对于结束状态可行**。 + +所以接下来,只要证明出 + +- ① 任何一种每个棋子所在点的 $SG$ 函数值异或起来非 $0$ 的情况,一定能通过一次移动棋子,到达一个 每个棋子所在点的 $SG$ 函数值异或起来为 $0$ 的情况 + +- ② 任何一种每个棋子所在点的 $SG$ 函数值异或起来为 $0$ 的情况,一定不能通过一次移动棋子,到达一个每个棋子所在点的 $SG$ 函数值异或起来为 $0$ 的情况 + +那么做法就是对的 + + +**证明** $1$: +设每个棋子所在点的 $SG$ 函数值分别为 $a_1,a_2,⋯,a_n$ +设 $x=a_1 XOR a_2 XOR ⋯ XOR a_n$,**此时$x$一定不等于$0$**。 +设 $x$ 的最高位为第 $k$ 位,那么在 $a_1,a_2,⋯,a_n$ 中,一定有一个值的第 $k$ 位为 $1$。 +设该值为 $a_i$,那么由于 $x$ 的第 $k$位和 $a_i$ 的第 $k$ 位都是 $1$,且,第 $k$ 位是 $x$ 的最高位,所以 $a_i XOR x$ 一定小于 $a_i$ + +又因为 $a_i$ 是其中一个棋子所在点的 $SG$ 函数值,那么根据 $SG$ 函数值的定义,该点能到达的所有点中,一定存在一个点的 $SG$ 函数值为 $a_i XOR x$ + +那么我们就可以将该点上的棋子,移到一个 $SG$ 函数值为 $a_i XOR x$ 的点上去 + +**移完之后**,原来每个棋子所在点的 $SG$ 函数异或值就变为了 $a_1 XOR a_2 XOR ⋯ XOR a_{i−1} XOR (a_i XOR x) XOR a_{i+1} ⋯ XOR a_n$ + +$=(a_1 XOR a_2 XOR ⋯ XOR a_n) XOR x=x XOR x=0$ + +① 证毕 + + +**证明** $2$: +反证法,设将点 $u$ 上的棋子移动到点 $v$ 上后,每个棋子所在点的 $SG$ 函数值仍然为 $0$ +那就说明 $SG(u)=SG(v)$,不符合 $SG$ 函数的定义,不成立 +② 证毕 + +所以做法是正确的。 + +#### 3、如何求出每个点的 $SG$ 函数值呢? +记忆化搜索就好啦~ +每层记忆化搜索中,如果该点的 $SG$ 函数值已经被计算出,那就直接返回该值。否则用一个 $set$ 记录每个点能到的所有点的 $SG$ 函数值集合,然后从 $0$ 开始遍历,找到第一个 $set$ 里面没有的数,将该值记录在该点上并返回。 + + +### 三、实现代码 +```cpp {.line-numbers} +#include + +using namespace std; +const int N = 2010, M = 6010; + +// SG函数模板题 +int n, m, k; +int f[N]; + +int h[N], e[M], ne[M], idx; +void add(int a, int b) { + e[idx] = b, ne[idx] = h[a], h[a] = idx++; +} + +int sg(int u) { + //记忆化搜索 + if (~f[u]) return f[u]; + + //找出当前结点u的所有出边,看看哪个sg值没有使用过 + set S; + for (int i = h[u]; ~i; i = ne[i]) + S.insert(sg(e[i])); + + //找到第一个没有出现的过的自然数, 0,1,2,3,4,... + for (int i = 0;; i++) + if (S.count(i) == 0) { + f[u] = i; + break; + } + return f[u]; +} + +int main() { + memset(h, -1, sizeof h); + cin >> n >> m >> k; + while (m--) { + int a, b; + cin >> a >> b; + add(a, b); + } + + memset(f, -1, sizeof f); //初始化sg函数的结果表 + int res = 0; + while (k--) { + int u; + cin >> u; + res ^= sg(u); //计算每个出发点的sg(u),然后异或在一起 + } + + if (res) //所有出发点的异或和不等于0,先手必胜 + puts("win"); + else //所有出发点的异或和等于0,先手必败 + puts("lose"); + + return 0; +} +``` \ No newline at end of file diff --git a/TangDou/AcWing_TiGao/T5/GameTheory/1321.cpp b/TangDou/AcWing_TiGao/T5/GameTheory/1321.cpp new file mode 100644 index 0000000..7f6502d --- /dev/null +++ b/TangDou/AcWing_TiGao/T5/GameTheory/1321.cpp @@ -0,0 +1,46 @@ +#include +using namespace std; +const int N = 55, M = 50060; +int n, f[N][M]; + +int dfs(int a, int b) { + if (~f[a][b]) return f[a][b]; // 记忆化搜索 + int &v = f[a][b]; // 引用,赋值更快捷 + if (!a) return b & 1; // a==0时,看b是不是奇数,奇数必胜,否则必败 + + if (b == 1) return dfs(a + 1, b - 1); + if (a && !dfs(a - 1, b)) return v = 1; // 从左边取一石子:左侧不空,并且取完后整体是一个必败态,那我必胜 + if (b && !dfs(a, b - 1)) return v = 1; // 合并b + if (a && b > 1 && !dfs(a - 1, b + 1)) return v = 1; // 合并a,b各一个 + if (a > 1 && !dfs(a - 2, b == 0 ? b + 2 : b + 3)) return v = 1; // 合并a + return v = 0; +} +int main() { + int T; + cin >> T; + memset(f, -1, sizeof f); // 初始化DP数组,-1 + + // 边界初始化 + f[1][0] = f[2][0] = 1; + f[3][0] = 0; + + while (T--) { + cin >> n; + int a = 0, b = -1; + for (int i = 1; i <= n; i++) { + int x; + cin >> x; + if (x == 1) + a++; // 左侧石子个数为1的石子堆数量 + else + b += x + 1; // 1:新增加1堆,x:这一堆x个 + } + if (b < 0) b = 0; // 一堆都没有,b=0 + + if (dfs(a, b)) + puts("YES"); + else + puts("NO"); + } + return 0; +} \ No newline at end of file diff --git a/TangDou/AcWing_TiGao/T5/GameTheory/1321.eddx b/TangDou/AcWing_TiGao/T5/GameTheory/1321.eddx new file mode 100644 index 0000000..e5f6838 Binary files /dev/null and b/TangDou/AcWing_TiGao/T5/GameTheory/1321.eddx differ diff --git a/TangDou/AcWing_TiGao/T5/GameTheory/1321.md b/TangDou/AcWing_TiGao/T5/GameTheory/1321.md new file mode 100644 index 0000000..add2c6b --- /dev/null +++ b/TangDou/AcWing_TiGao/T5/GameTheory/1321.md @@ -0,0 +1,160 @@ +##[$AcWing$ $1321$. 取石子](https://www.acwing.com/problem/content/description/1323/) + +### 一、题目描述 +$Alice$ 和 $Bob$ 两个好朋友又开始玩取石子了。 + +游戏开始时,有 $N$ 堆石子排成一排,然后他们轮流操作($Alice$ 先手),每次操作时从下面的规则中任选一个: + +- 从某堆石子中取走一个; +- 合并任意两堆石子。 + +不能操作的人输。 + +$Alice$ 想知道,她是否能有必胜策略。 + +**输入格式** +第一行输入 $T$,表示数据组数。 + +对于每组测试数据,第一行读入 $N$; + +接下来 $N$ 个正整数 $a_1,a_2,⋯,a_N$ ,表示每堆石子的数量。 + +**输出格式** +对于每组测试数据,输出一行。 + +输出 $YES$ 表示 $Alice$ 有必胜策略,输出 $NO$ 表示 $Alice$ 没有必胜策略。 + +**数据范围** +$1≤T≤100,1≤N≤50,1≤a_i≤1000$ + +**输入样例:** +```cpp {.line-numbers} +3 +3 +1 1 2 +2 +3 4 +3 +2 3 5 +``` + +**输出样例:** +```cpp {.line-numbers} +YES +NO +NO +``` + +### 二、博弈论总结 + + 必胜态 $\Rightarrow$ 选择合适方案 $\Rightarrow$ 对手必败态 + 必败态 $\Rightarrow$ 选择任何路线 $\Rightarrow$ 对手必胜态 + + + 对手聪明绝顶,不会犯错误,一旦他有机会获胜,他一定能找到合适的方案!所以,一定不能让他有机会,也就是总要让他总是处于必败状态,你才能获胜! + + +### 三、思考过程 + +**$Q1$:本题中博弈的胜负与什么因素相关呢?** +**答**:因为只有两种操作:**拿走一个石子、合并两堆**,很显然, 两个关键因素: **石子个数、堆数** + +**$Q2$:一般情况是什么,特殊情况是什么呢?** +**答**:如果某一堆石子只有$1$个,随着我们执行拿走$1$个的操作,它的堆就没了,这样石子个数变了,堆数也变了,一下变两个,问题变复杂了,上来就想难题,怕是搞不定。 +既然这样,我们就思考一下 **一般情况** :只考虑所有个数大于等于$2$的那些堆,其它可能存在石子数等于$1$的,等我们想明白这个一般情况问题再研究特殊情况的事,由易到难。 + +**$Q3$:猜一下关联关系?** +两个操作同一时间只能执行一个,可以理解为拿走一个石子对结果影响一下,合并两堆石子对结果也是影响一下,初步考虑应该堆个数与石子总数的加法关系相关。 + +**一般情况:当每堆的石子个数都是大于等于$2$时,猜关联关系** + +
设 剩余操作数 = $b$ = 堆数 + 石子总数 - $1$
+
结论:$b$是奇数⟺先手必胜,$b$是偶数⟺先手必败
+ +我们可以发现,当$n$是$1$的时候,也就是只有$1$堆时,比如$a_0=3$,那么$b=3+1-1=3$,是奇数: +- ① 先手拿走一个,剩操作数=$2$ +- ② 后手只能拿走$1$个,剩操作数$1$ +- ③ 先手再拿走1个,剩余操作数$=0$ +- ④ 后手没有可以拿的了,后手负,先手必胜! + +结论显然成立。 + +当不只有$1$堆时,分类讨论: + +**情况$1$:没有数量为$1$的堆** + +先证明奇数必胜,对于先手来说, + - $n>1$, 那么 **只要选两堆合并**, 那么 **总操作数变成偶数** + - $n=1$, 明显只能选择减少$1$操作,后手还是偶数。 + +对于后手来说,无论他是减少$1$,还是合并操作,留下的总操作数一定还是奇数。 + +对于某些读者来说,可能会问,如果后手把某个$2$变成$1$,先手该怎么办,其实这个很容易操作,如果堆数超过$1$,先手一定选择合并这个数量为$1$的堆,如果只有一堆了而且还是$1$,明显先手必胜了。 所以,先手总是有办法让后手必败(操作数为偶数的局面),后手无论怎么走,都会让先手必胜(变成操作为奇数的局面),所以我们证明成立。 + +在上面,我们也证明了当操作数是偶数的时候,先手是必败的。 + + +**情况$2$:有数量为$1$的堆** + +这个情况比较复杂,因为如果某个人把$1$减少$1$,那么这个堆同时也消失了,相当于操作数减少了$2$。 + +* 假设有$a$堆石子,其中每堆石子个数为$1$ +* 剩余堆的石子个数都严格大于$1$ + +根据这些数量大于$1$的堆的石子可以求出上述定义出的$b$,我们使用$f(a, b)$表示此时先手必胜还是必败,因为博弈论在本质上是可以递推的,我们可以想出起点,再想出递推关系,就可以递推得到更大数据情况下的递推值,也就是博弈论本质上是$dp$。 + +
+ + +![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/202312261053532.png) + + +### 六、实现代码 +```cpp {.line-numbers} +#include +using namespace std; +const int N = 55, M = 50060; +int n, f[N][M]; + +int dfs(int a, int b) { + if (~f[a][b]) return f[a][b]; // 记忆化搜索 + int &v = f[a][b]; // 引用,赋值更快捷 + if (!a) return b & 1; // a==0时,看b是不是奇数,奇数必胜,否则必败 + + if (b == 1) return dfs(a + 1, b - 1); + if (a && !dfs(a - 1, b)) return v = 1; // 从左边取一石子:左侧不空,并且取完后整体是一个必败态,那我必胜 + if (b && !dfs(a, b - 1)) return v = 1; // 合并b + if (a && b > 1 && !dfs(a - 1, b + 1)) return v = 1; // 合并a,b各一个 + if (a > 1 && !dfs(a - 2, b == 0 ? b + 2 : b + 3)) return v = 1; // 合并a + return v = 0; +} +int main() { + int T; + cin >> T; + memset(f, -1, sizeof f); // 初始化DP数组,-1 + + // 边界初始化 + f[1][0] = f[2][0] = 1; + f[3][0] = 0; + + while (T--) { + cin >> n; + int a = 0, b = -1; + for (int i = 1; i <= n; i++) { + int x; + cin >> x; + if (x == 1) + a++; // 左侧石子个数为1的石子堆数量 + else + b += x + 1; // 1:新增加1堆,x:这一堆x个 + } + if (b < 0) b = 0; // 一堆都没有,b=0 + + if (dfs(a, b)) + puts("YES"); + else + puts("NO"); + } + return 0; +} +``` diff --git a/TangDou/AcWing_TiGao/T5/GameTheory/1322.cpp b/TangDou/AcWing_TiGao/T5/GameTheory/1322.cpp new file mode 100644 index 0000000..3956405 --- /dev/null +++ b/TangDou/AcWing_TiGao/T5/GameTheory/1322.cpp @@ -0,0 +1,51 @@ +#include + +using namespace std; +const int N = 1010; +int n; +int a[N]; +int left[N][N], right[N][N]; // left,right 在 iostream库中用过了,不能用! + +int main() { + int T; + scanf("%d", &T); + while (T--) { + scanf("%d", &n); + for (int i = 1; i <= n; i++) scanf("%d", &a[i]); + + for (int len = 1; len <= n; len++) // 枚举长度 + for (int i = 1; i + len - 1 <= n; i++) { // left[i][j],从i到j + int j = i + len - 1; + if (len == 1) + left[i][j] = right[i][j] = a[i]; // DP初始值 + else { + int L = left[i][j - 1], R = right[i][j - 1], X = a[j]; + if (R == X) + left[i][j] = 0; + else if (X < L && X < R || X > L && X > R) + left[i][j] = X; + else if (L > R) + left[i][j] = X - 1; + else + left[i][j] = X + 1; + + L = left[i + 1][j], R = right[i + 1][j], X = a[i]; + if (L == X) + right[i][j] = 0; + else if (X < L && X < R || X > L && X > R) + right[i][j] = X; + else if (R > L) + right[i][j] = X - 1; + else + right[i][j] = X + 1; + } + } + + if (n == 1) + puts("1"); + else + printf("%d\n", left[2][n] != a[1]); + } + + return 0; +} diff --git a/TangDou/AcWing_TiGao/T5/GameTheory/1322.md b/TangDou/AcWing_TiGao/T5/GameTheory/1322.md new file mode 100644 index 0000000..7a240e4 --- /dev/null +++ b/TangDou/AcWing_TiGao/T5/GameTheory/1322.md @@ -0,0 +1,274 @@ +##[$AcWing 1322$. 取石子游戏](https://www.acwing.com/problem/content/1324/) + +### 一、题目描述 +在研究过 $Nim$ 游戏及各种变种之后,$Orez$ 又发现了一种全新的取石子游戏,这个游戏是这样的: + +有 $n$ 堆石子,将这 $n$ 堆石子摆成一排。 + +游戏由两个人进行,两人轮流操作,每次操作者都可以从 **最左** 或 **最右** 的一堆中取出若干颗石子,可以将那一堆全部取掉,但不能不取,**不能操作的人就输了**。 + +$Orez$ 问:对于任意给出的一个初始局面,是否存在先手必胜策略。 + +**输入格式** +第一行为一个整数 $T$,表示有 $T$ 组测试数据。 + +对于每组测试数据,第一行为一个整数 $n$,表示有 $n$ 堆石子,第二行为 $n$ 个整数 $a_i$ ,依次表示每堆石子的数目。 + +**输出格式** +对于每组测试数据仅输出一个整数 $0$ 或 $1$,占一行。 + +其中 $1$ 表示有先手必胜策略,$0$ 表示没有。 + +**数据范围** +$1≤T≤10,1≤n≤1000,1≤a_i≤10^9$ + +**输入样例**: +```cpp {.line-numbers} +1 +4 +3 1 9 4 +``` +输出样例: +```cpp {.line-numbers} +0 +``` + +### 二、思考过程 + +#### 1、状态定义 + +① 设 $left[i][j]$ 表示在 $[i,j]$ 已经固定的区间 **左侧** 放上一堆数量为 $left[i][j]$ 的石子后,**先手必败** +② 设 $right[i][j]$ 表示在 $[i,j]$ 已经固定的区间 **右侧** 放上一堆数量为 $right[i][j]$ 的石子后,**先手必败** + +即:$(left[i][j],\underbrace{a_i,a_{i+1},\cdots,a_j}_{a[i]\sim a[j]})$,$(\underbrace{a_i,a_{i+1},\cdots,a_j}_{a[i]\sim a[j]},right[i][j])$ 为 **先手必败** 局面 + +有如下两个性质: + +#### 2、$left[i][j]$,$right[i][j]$一定存在 +**反证法**: +假设不存在满足定义的 $left[i][j]$,则对于 **任意非负整数** $x$,有形如: + +$$\large \underbrace{x,a_i,a_{i+1},\cdots,a_j}_{A(x)}$$ 都为 **必胜局面** ,记为 $A(x)$ 局面。 + +由于 $A(x)$ 为必胜局面,故从 $A(x)$ 局面 必然存在若干种办法一步可达必败局面。 + +若从最左边一堆中拿,因为假设原因,不可能变成必败局面,因为这样得到的局面仍形如 $A(x)$。 + + +左边拿没用,只能考虑从右边拿(即从$a_j$里拿): + +于是设 $A(x)$ 一步可达的某个 **必败局面**为 $(x,a_i,a_{i+1},\cdots,a_{j-1},y)$,显然有 $0 \le y < a_j$。 + +**由于 $x$ 有无限个,但 $y$ 只有 $a_j$种——根据抽屉原理,必然存在 $x_1,x_2(x_1 > x_2),y$ 满足 $(x_1,a_i,a_{i+1},\cdots,a_{j-1},y)$ 和 $(x_2,a_i,a_{i+1},\cdots,a_{j-1},y)$ 都是必败局面**。但这两个必败局面之间 **实际一步可达**(比如拿走$x_1-x_2$个),矛盾,假设不成立,原命题成立。 + +#### 3、$left[i][j]$,$right[i][j]$必然唯一 + +**反证法**: +假设 $left(i,j)$ 不唯一,则存在非负整数 $x_1,x_2(x_1 \neq x_2)$,使得$(x_1,a_i,a_{i+1},⋯,a_{j−1},a_j)$ 和 $(x_2,a_i,a_{i+1},\cdots,a_{j-1},a_j)$ 均为必败局面,而 **第一个必败局面** 可以通过拿走左侧$x_1-x_2$个石子到达另一个 **必败局面** ,矛盾,假设不成立,原命题成立。 + +#### 4、推论 +有了上面推的$left[i][j]$唯一性,得出一个有用的推论: +**对于任意非负整数 $x \neq left(i,j)$,$\large (x,a_i,a_{i+1},\cdots,a_j)$为必胜局面** + +#### 5、疑问 + +博弈论的题目都是可以通过动态规划来递推的。 + +**$Q$:为什么定义先手必败,而不是定义先手必胜呢?** +答:因为上面证明过定义 **先手必败** 的动态规划结果数组,是肯定存在并且是唯一的.存在且唯一的,可以递推出来,如果定义的是 **先手必胜**,根据博弈论的知识,我们知道,必胜的策略不唯一,不方便递推。而如果我们采用的是 **先手必败** 这样的定义,那么由于它的存在性和唯一性,所以,只要不是它就是必胜局面! + +**$Q$:怎么递推?** +递推嘛,就是类似于 **数学归纳法**,先求出初始状态是多少,然后假设$i \sim j-1$这段已经计算出$left[i][j-1],right[i][j-1]$了,现在想依赖于这两个数值推导出$left[i][j],right[i][j]$,怕写的太长麻烦,就定义了$L=left[i][j-1],R=right[i][j-1]$ + +![](http://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/2023/02/cccc1a0f80d1475adcf8096aa9e35ff0.png) + + +考虑三个问题: +- ① 初始值 +- ② 答案在哪 +- ③ 递推式 +> **注:答案在哪,并不是和递推式相关,而是和状态表示相关,一定要注意** + + + +**① 初始值** +$\large left[i][i]=right[i][i]=a_i$ + +当只有一堆石子时($only$ $i$),我在这堆前面添加一堆,个数和这堆一样多,对于**两堆相同的石子**,**后手进行和先手对称的操作**,你咋干我就咋干,我拿完,你瞪眼~, **先手必败** + + +**② 答案在哪** +**先手必败** $\Leftrightarrow \ left[2][n]==a[1]$ , **先手必胜** $\Leftrightarrow \ left[2][n]!=a[1]$ +> **解释**:$a[2] \sim a[n]$,前面放上了一个$a[1]$, +> 根据定义$left[2][n]$代表在$a[2]\sim a[n]$之前放上一个数,可以使得放后的局面必败。 +> 现在放上去的是$a[1]$,可以它偏偏等于$left[2][n]$这个令人讨厌的数字,面对这样的局面,天生是死局,先手必败。 + + + +**③ 递推式** +* 变化方法:从左侧拿走一些石子或者从右侧拿走一些石子,我们需要考虑在一个局面确定后,在此局面上左侧、右侧添加一个什么数字(石子个数),才能使得变化后的局面必败。 + +* $left[i][j-1]$和$right[i][j-1]$表示$left[i][j]$和$right[i][j]$,形成$DP$递推关系 + + +递推式需要分类讨论 + +先把 **特殊情况** 说清楚: + +**$\large L=R=0$** + +若 $R=0$ 则 $L=0$ +> 注:因$R=0$,表示在[$i$,$j-1$]确定后,右侧为$0$就能满足[$i$,$j-1$]这一段为先手必败,此时,左侧增加那堆个数为$0$就可以继续保持原来的先手必败,即$L=0$,而且已经证明了$L=R=0$是唯一的。 + +此时 $X>\max\{L,R\}$,也就是说 $L=0$ 和 $R=0$ 都属于 $Case$ $5$,故其它 $Case$ 满足 $L,R>0$。 + +令 $\displaystyle \large X=a[j](X>0)$ + +下面,我们按$X$与$R$的大小关系,划分为三种情况,分别进行讨论: +$$ +\large \left\{\begin{matrix} +X=R & \\ +XR & \left\{\begin{matrix} X \leq L \\X>L \end{matrix}\right. +\end{matrix}\right. +$$ + +* $X=R$($Case$ $1$) + 根据 $R=right[i][j-1]$ 的定义,$X=R$则区间 $[i,j]$ 是必败局面,因此左边啥也不能添,添了反而错 + $$\large left[i][j]=0$$ + +* $X + + * ② $X \geq L$,即 $L \leq X < R$($Case$ $3$) + * **必胜策略**: + 当右侧石子个数为$X$时,$\large left[i][j]=X+1$.即在右侧石子个数确定为$X$后,如果在左侧添加一堆石子,个数为$X+1$,就可以保证当前局面先手必败。 + * **证明**: + 即 **求证** $(X+1,a_i,a_{i+1},\cdots,a_{j-1},X)$为 **必败局面** ,其中 $L \leq X L$,则后手将最右堆拿成 $z-1$ 个石子($z-1 \ge L>0$),**保证左侧比右侧多$1$个石子**,就能回到 $Case$ $3$ 本身,递归证明即可 + * 若 $z=L$,则后手将最右堆拿完,根据 $L=left[i][j-1]$ 定义知此时局面必败 + * 若 $0 + * 若先手拿最右边一堆,设拿了以后 **还剩 $z$ 个石子** + * 若 $z \ge L$,则后手将最左堆拿成 $z+1$个石子,就能回到 $Case$ $3$ 本身,递归证明即可 + * 若 $0R$ + * ① $X≤L$,即 $R < X \leq L$($Case$ $4$) + * **必胜策略**:$$\large left[i][j]=X-1$$ + * **证明**: + * 若先手拿最左边一堆,设拿了以后还剩 $z$ 个石子。 + * 若 $z \geq R$,则后手将最右堆拿成 $z+1$ 个石子,保证左侧比右侧少$1$个石子,就能回到 $Case$ $4$ 本身,递归证明即可。 + * 若 $0R$),由 $right[i][j-1])$ 的定义知此时是必败局面。 + +
+ + * 若先手拿最右边一堆,设拿了以后还剩 $z$ 个石子。 + * 若 $z>R$,则后手将最左边一堆拿成 $z-1$ 个石子(注意 $z-1 \ge R >0$),递归证明即可。保证右侧比左侧多$1$个石子。 + * 若 $z=R$,则后手把最左堆拿完,根据 $right[i][j-1]$的定义可知得到了必败局面。 + * 若 $0 + + * ② $X>L$,即 $X>\max\{L,R\}$($Case$ $5$) + * **必胜策略**:$$\large left[i][j]=x$$ + * **证明**: + 设先手将其中一堆拿成了 $z$ 个石子。 + * 若 $z>\max\{L,R\}$,后手将另一堆也拿成$z$个,回到 $Case$ $5$,递归证明。 + * 若 $0 温馨提示:**请看清楚 $L$ 取不取等,乱取等是错的!** + +同理可求 $right(i,j)$。 + +### 六、实现代码 +```cpp {.line-numbers} +#include + +using namespace std; +const int N = 1010; +int n; +int a[N]; +int left[N][N], right[N][N]; // left,right 在 iostream库中用过了,不能用! + +int main() { + int T; + scanf("%d", &T); + while (T--) { + scanf("%d", &n); + for (int i = 1; i <= n; i++) scanf("%d", &a[i]); + + for (int len = 1; len <= n; len++) // 枚举长度 + for (int i = 1; i + len - 1 <= n; i++) { // left[i][j],从i到j + int j = i + len - 1; + if (len == 1) + left[i][j] = right[i][j] = a[i]; // DP初始值 + else { + int L = left[i][j - 1], R = right[i][j - 1], X = a[j]; + if (R == X) + left[i][j] = 0; + else if (X < L && X < R || X > L && X > R) + left[i][j] = X; + else if (L > R) + left[i][j] = X - 1; + else + left[i][j] = X + 1; + + L = left[i + 1][j], R = right[i + 1][j], X = a[i]; + if (L == X) + right[i][j] = 0; + else if (X < L && X < R || X > L && X > R) + right[i][j] = X; + else if (R > L) + right[i][j] = X - 1; + else + right[i][j] = X + 1; + } + } + + if (n == 1) + puts("1"); + else + printf("%d\n", left[2][n] != a[1]); + } + + return 0; +} + +``` \ No newline at end of file diff --git a/TangDou/AcWing/Math/GameTheory/1322.vsdx b/TangDou/AcWing_TiGao/T5/GameTheory/1322.vsdx similarity index 100% rename from TangDou/AcWing/Math/GameTheory/1322.vsdx rename to TangDou/AcWing_TiGao/T5/GameTheory/1322.vsdx diff --git a/TangDou/AcWing/Math/GameTheory/HDU1730.cpp b/TangDou/AcWing_TiGao/T5/GameTheory/HDU1730.cpp similarity index 100% rename from TangDou/AcWing/Math/GameTheory/HDU1730.cpp rename to TangDou/AcWing_TiGao/T5/GameTheory/HDU1730.cpp diff --git a/TangDou/AcWing/Math/GameTheory/HDU1730.md b/TangDou/AcWing_TiGao/T5/GameTheory/HDU1730.md similarity index 100% rename from TangDou/AcWing/Math/GameTheory/HDU1730.md rename to TangDou/AcWing_TiGao/T5/GameTheory/HDU1730.md diff --git a/TangDou/AcWing/Math/GameTheory/HDU1846.cpp b/TangDou/AcWing_TiGao/T5/GameTheory/HDU1846.cpp similarity index 100% rename from TangDou/AcWing/Math/GameTheory/HDU1846.cpp rename to TangDou/AcWing_TiGao/T5/GameTheory/HDU1846.cpp diff --git a/TangDou/AcWing/Math/GameTheory/HDU1847.cpp b/TangDou/AcWing_TiGao/T5/GameTheory/HDU1847.cpp similarity index 100% rename from TangDou/AcWing/Math/GameTheory/HDU1847.cpp rename to TangDou/AcWing_TiGao/T5/GameTheory/HDU1847.cpp diff --git a/TangDou/AcWing/Math/GameTheory/HDU1850.cpp b/TangDou/AcWing_TiGao/T5/GameTheory/HDU1850.cpp similarity index 100% rename from TangDou/AcWing/Math/GameTheory/HDU1850.cpp rename to TangDou/AcWing_TiGao/T5/GameTheory/HDU1850.cpp diff --git a/TangDou/AcWing/Math/GameTheory/HDU2176.cpp b/TangDou/AcWing_TiGao/T5/GameTheory/HDU2176.cpp similarity index 100% rename from TangDou/AcWing/Math/GameTheory/HDU2176.cpp rename to TangDou/AcWing_TiGao/T5/GameTheory/HDU2176.cpp diff --git a/TangDou/AcWing/Math/GameTheory/HDU2188.cpp b/TangDou/AcWing_TiGao/T5/GameTheory/HDU2188.cpp similarity index 100% rename from TangDou/AcWing/Math/GameTheory/HDU2188.cpp rename to TangDou/AcWing_TiGao/T5/GameTheory/HDU2188.cpp diff --git a/TangDou/AcWing/Math/GameTheory/HDU4315.cpp b/TangDou/AcWing_TiGao/T5/GameTheory/HDU4315.cpp similarity index 100% rename from TangDou/AcWing/Math/GameTheory/HDU4315.cpp rename to TangDou/AcWing_TiGao/T5/GameTheory/HDU4315.cpp diff --git a/TangDou/AcWing/Math/GameTheory/HDU4315.md b/TangDou/AcWing_TiGao/T5/GameTheory/HDU4315.md similarity index 100% rename from TangDou/AcWing/Math/GameTheory/HDU4315.md rename to TangDou/AcWing_TiGao/T5/GameTheory/HDU4315.md diff --git a/TangDou/AcWing/Math/GameTheory/POJ1704.drawio b/TangDou/AcWing_TiGao/T5/GameTheory/POJ1704.drawio similarity index 100% rename from TangDou/AcWing/Math/GameTheory/POJ1704.drawio rename to TangDou/AcWing_TiGao/T5/GameTheory/POJ1704.drawio diff --git a/TangDou/AcWing/Math/GameTheory/POJ1704.md b/TangDou/AcWing_TiGao/T5/GameTheory/POJ1704.md similarity index 100% rename from TangDou/AcWing/Math/GameTheory/POJ1704.md rename to TangDou/AcWing_TiGao/T5/GameTheory/POJ1704.md diff --git a/TangDou/AcWing/Math/GameTheory/POJ1704_Nim.cpp b/TangDou/AcWing_TiGao/T5/GameTheory/POJ1704_Nim.cpp similarity index 100% rename from TangDou/AcWing/Math/GameTheory/POJ1704_Nim.cpp rename to TangDou/AcWing_TiGao/T5/GameTheory/POJ1704_Nim.cpp diff --git a/TangDou/AcWing/Math/GameTheory/POJ1704_StaircaseNim_7.cpp b/TangDou/AcWing_TiGao/T5/GameTheory/POJ1704_StaircaseNim_7.cpp similarity index 100% rename from TangDou/AcWing/Math/GameTheory/POJ1704_StaircaseNim_7.cpp rename to TangDou/AcWing_TiGao/T5/GameTheory/POJ1704_StaircaseNim_7.cpp diff --git a/TangDou/AcWing/Math/GameTheory/POJ1704_StaircaseNim_8.cpp b/TangDou/AcWing_TiGao/T5/GameTheory/POJ1704_StaircaseNim_8.cpp similarity index 100% rename from TangDou/AcWing/Math/GameTheory/POJ1704_StaircaseNim_8.cpp rename to TangDou/AcWing_TiGao/T5/GameTheory/POJ1704_StaircaseNim_8.cpp diff --git a/TangDou/AcWing/Math/GameTheory/不一样的博弈论笔记.md b/TangDou/AcWing_TiGao/T5/GameTheory/不一样的博弈论笔记.md similarity index 100% rename from TangDou/AcWing/Math/GameTheory/不一样的博弈论笔记.md rename to TangDou/AcWing_TiGao/T5/GameTheory/不一样的博弈论笔记.md diff --git a/TangDou/AcWing/Math/GameTheory/博弈论.md b/TangDou/AcWing_TiGao/T5/GameTheory/博弈论.md similarity index 56% rename from TangDou/AcWing/Math/GameTheory/博弈论.md rename to TangDou/AcWing_TiGao/T5/GameTheory/博弈论.md index fb6a442..440969d 100644 --- a/TangDou/AcWing/Math/GameTheory/博弈论.md +++ b/TangDou/AcWing_TiGao/T5/GameTheory/博弈论.md @@ -73,122 +73,7 @@ $$ [$HDU2176$](https://www.cnblogs.com/littlehb/p/16403990.html) -<<<<<<< HEAD [$HDU1730$](https://www.cnblogs.com/littlehb/p/16403933.html) -======= -int main() { - int n; - scanf("%d", &n); - - int res = 0; //起始值是0,因为任何数与0进行异或都它本身 - while (n--) { - int x; - scanf("%d", &x); - res ^= x; - } - if (res) - puts("Yes"); //异或值非零,必胜 - else puts("No"); //异或值是零,必败 - return 0; -} -``` - -经典$Nim$游戏 [$HDU1850$](https://acm.hdu.edu.cn/showproblem.php?pid=1850) - -$HDU1850$ 本题为一道**尼姆博弈**的改进题目,题目问题是给你几堆扑克;让你判断如果你能赢,你第一次操作都能使对手败的操作次数有多少种。根据题目大致可以根据其特点,得到如下思路;因为你每次能从任意选择一堆并取走其中的任意张牌。那么,只要看每一堆中有多少种操作;只要每一次中改变的不使之得到如下结果$a_i ∧ k -using namespace std; -const int N = 100 + 10; -int n, a[N], res; -int main() { - while (scanf("%d", &n) && n) { - res = 0; - for (int i = 1; i <= n; i++) { - scanf("%d", &a[i]); - res ^= a[i]; - } - //先手必改,没的选择,输出0 - if (res == 0) - puts("0"); - else { - //先手的人如果想赢,第一步有几种选择呢? - int ans = 0; - for (int i = 1; i <= n; i++) - if ((res ^ a[i]) < a[i]) ans++; - printf("%d\n", ans); - } - } - return 0; -} -``` - -[$HDU$2176](https://acm.hdu.edu.cn/showproblem.php?pid=2176) -> 先取者负输出$No$.先取者胜输出$Yes$,然后输出先取者第$1$次取子的所有方法.如果从有$a$个石子的堆中取若干个后剩下$b$个后会胜就输出$a$ $b$ - -```c++ -#include -using namespace std; -const int N = 200010; -int a[N]; - -int main() { - int n; - while (~scanf("%d", &n) && n) { - int x = 0; - for (int i = 0; i < n; i++) { - scanf("%d", &a[i]); - x = x ^ a[i]; - } - if (!x) - puts("No"); - else { - puts("Yes"); - for (int i = 0; i < n; i++) { - int res = a[i] ^ x; - //找到了证明中提到的: a_i ,此堆的数量最高位第k位,数值为1 - //拿走:a[i]- (a[i]^x) - //剩下:a[i]^x = res个 - if (res < a[i]) - printf("%d %d\n", a[i], res); - } - } - } - return 0; -} -``` - -经典$Nim$ -[$HDU$1730](https://acm.hdu.edu.cn/showproblem.php?pid=1730) - -> 思路:如果$2$个棋子相邻的话,那么就是一个棋子逃跑,一个棋子尾随,此时相邻的两个棋子是必败态,每次都是走一步,这个游戏有$n$行,所以我们可以把这个游戏看成是$n$堆的取石子游戏,每一堆的石子数即为两个棋子的间距,所以可以利用$Nim$博弈 - -```c++ -#include -using namespace std; -int main() { - int n, m; - while (~scanf("%d %d", &n, &m)) { - int res = 0; - for (int i = 1; i <= n; ++i) { - int a, b; - scanf("%d %d", &a, &b); - // 两个棋子的距离的绝对值减1.因为我们看当两个棋子相邻的时候, - // 已经是一个必败态了。不信我贴着你,你走吧。你走一步我跟一步,最后你将无路路可走 - res = res ^ (abs(a - b) - 1); - } - if (res == 0) - puts("BAD LUCK!"); - else - puts("I WIN!"); - } - return 0; -} -``` ->>>>>>> parent of ab3f165ae ('commit') --- diff --git a/TangDou/AcWing_TiGao/T5/QiWang/218.cpp b/TangDou/AcWing_TiGao/T5/QiWang/218.cpp new file mode 100644 index 0000000..65bca6b --- /dev/null +++ b/TangDou/AcWing_TiGao/T5/QiWang/218.cpp @@ -0,0 +1,64 @@ +#include +using namespace std; +const int N = 15; +const int INF = 0x3f3f3f3f; +double f[N][N][N][N][5][5]; +int A, B, C, D; + +// 如果大小王翻出来放1里,则a++,放2里b++,... +void add(int &a, int &b, int &c, int &d, int x) { + if (x == 1) a++; + if (x == 2) b++; + if (x == 3) c++; + if (x == 4) d++; +} + +/* +功能:计算当前状态f(a,b,c,d,x,y)下的期望值 +*/ +double dfs(int a, int b, int c, int d, int x, int y) { + // 记忆化搜索 + if (f[a][b][c][d][x][y] > 0) return f[a][b][c][d][x][y]; + + // 递归出口:当前状态是否到达目标状态,目标状态的期望值是0 + int ta = a, tb = b, tc = c, td = d; // 抄出来 + add(ta, tb, tc, td, x), add(ta, tb, tc, td, y); // 大王小王会改变四个花色的数量 + if (ta >= A && tb >= B && tc >= C && td >= D) return 0; // 如果条件全满足就是终止状态 + + // 当前状态下的剩余牌数量 + int rst = 54 - ta - tb - tc - td; + if (rst <= 0) return INF; // 还没有完成目标,没有剩余的牌了,无解 + + // 当前状态可以向哪些状态转移 + double v = 1; + if (a < 13) // 黑桃有剩余,可能选出的是黑桃 + v += dfs(a + 1, b, c, d, x, y) * (13 - a) / rst; + if (b < 13) // 红桃有剩余,可能选出的是红桃 + v += dfs(a, b + 1, c, d, x, y) * (13 - b) / rst; + if (c < 13) // 梅花有剩余,可能选出的是梅花 + v += dfs(a, b, c + 1, d, x, y) * (13 - c) / rst; + if (d < 13) // 方块有剩余,可能选出的是方块 + v += dfs(a, b, c, d + 1, x, y) * (13 - d) / rst; + + // 如果小王没有被选出 + if (x == 0) + v += min(min(dfs(a, b, c, d, 1, y), dfs(a, b, c, d, 2, y)), min(dfs(a, b, c, d, 3, y), dfs(a, b, c, d, 4, y))) / rst; + + // 如果大王没有被选出 + if (y == 0) + v += min(min(dfs(a, b, c, d, x, 1), dfs(a, b, c, d, x, 2)), min(dfs(a, b, c, d, x, 3), dfs(a, b, c, d, x, 4))) / rst; + + return f[a][b][c][d][x][y] = v; +} + +int main() { + cin >> A >> B >> C >> D; + + double res = dfs(0, 0, 0, 0, 0, 0); // 四种花色、大小王都还没有被抽取 + + if (res > INF / 2) // 因为是浮点数,不能用等号判断是不是相等,简单的办法就是INF/2 + puts("-1.000"); + else + printf("%.3f\n", res); + return 0; +} \ No newline at end of file diff --git a/TangDou/AcWing_TiGao/T5/QiWang/218.md b/TangDou/AcWing_TiGao/T5/QiWang/218.md index a4c716a..22c5065 100644 --- a/TangDou/AcWing_TiGao/T5/QiWang/218.md +++ b/TangDou/AcWing_TiGao/T5/QiWang/218.md @@ -13,6 +13,18 @@ $Rainbow$ 想问问 $Admin$,得到 $A$ 张黑桃、$B$ 张红桃、$C$ 张梅 特殊地,如果翻开的牌是大王或者小王,$Admin$ 将会把它作为某种花色的牌放入对应堆中,使得放入之后 $E$的值尽可能小。 +> 注:从牌堆里面翻出来的牌的期望张数最少是多少? +> **期望**:用初等数学的语言去描述,就是平均值是多少。 +> $Q:$为什么会有最少的概念出现的呢?这个数量不是固定的吗? +> 答:这是因为大王和小王可以被认为是其中四个花色中的某一种花色,这就导致产生了不同的事件。 +> 放到红桃里是一种事件,放到黑桃里是一种事件,...,这四个事件的期望不一定相同! +> 选择就在大小王上,我们要选择一个期望张数最少的放法,问我们这个期望张数最小是多少。 + +###$yxc$经典语录 +> **图中,点是状态表示,边是状态转移**。 +> **理解**:前一个题中,需要建图,建图完成后才能用图进行状态表示和状态转移,本题,状态表示和状态转移都是很明确的,不需要建图。 +> **总结** :动态规划是精髓,建图与否是表象。 + 由于 $Admin$ 和 $Rainbow$ 还在玩扑克,所以这个程序就交给你来写了。 **输入格式** @@ -38,10 +50,6 @@ $0≤A,B,C,D≤15$ ### 二、题意分析 -$Q$:为什么从终止状态向起始状态递推? - -**答**:满足条件的终止状态较多,而起始状态唯一。考虑以终止状态为初值,起始状态为目标,进行动态规划。 - #### 状态表示 $f[a][b][c][d][x][y]$ : 当前已翻开状态下,还需翻开牌的数量 **期望数**。 @@ -70,10 +78,9 @@ using namespace std; const int N = 15; const int INF = 0x3f3f3f3f; double f[N][N][N][N][5][5]; -int st[N][N][N][N][5][5]; int A, B, C, D; -//如果大小王翻出来放1里,则a++,放2里b++,... +// 如果大小王翻出来放1里,则a++,放2里b++,... void add(int &a, int &b, int &c, int &d, int x) { if (x == 1) a++; if (x == 2) b++; @@ -85,38 +92,34 @@ void add(int &a, int &b, int &c, int &d, int x) { 功能:计算当前状态f(a,b,c,d,x,y)下的期望值 */ double dfs(int a, int b, int c, int d, int x, int y) { - //记忆化,同时因为f为double类型,不能使用传统的memset(0x3f)之类 - //进行初始化并判断是否修改过,只能再开一个st数组 - if (st[a][b][c][d][x][y]) return f[a][b][c][d][x][y]; - st[a][b][c][d][x][y] = 1; + // 记忆化搜索 + if (f[a][b][c][d][x][y] > 0) return f[a][b][c][d][x][y]; - //递归出口:当前状态是否到达目标状态,目标状态的期望值是0 - int ta = a, tb = b, tc = c, td = d; //抄出来 - add(ta, tb, tc, td, x), add(ta, tb, tc, td, y); //大王小王会改变四个花色的数量 - if (ta >= A && tb >= B && tc >= C && td >= D) return 0; + // 递归出口:当前状态是否到达目标状态,目标状态的期望值是0 + int ta = a, tb = b, tc = c, td = d; // 抄出来 + add(ta, tb, tc, td, x), add(ta, tb, tc, td, y); // 大王小王会改变四个花色的数量 + if (ta >= A && tb >= B && tc >= C && td >= D) return 0; // 如果条件全满足就是终止状态 - //当前状态下的剩余牌数量 + // 当前状态下的剩余牌数量 int rst = 54 - ta - tb - tc - td; - if (rst == 0) return INF; //还没有完成目标,没有剩余的牌了,无解 + if (rst <= 0) return INF; // 还没有完成目标,没有剩余的牌了,无解 - //当前状态可以向哪些状态转移 - // Q:v为什么要初始化为1? - // A:看题解内容 + // 当前状态可以向哪些状态转移 double v = 1; - if (a < 13) //黑桃有剩余,可能选出的是黑桃 + if (a < 13) // 黑桃有剩余,可能选出的是黑桃 v += dfs(a + 1, b, c, d, x, y) * (13 - a) / rst; - if (b < 13) //红桃有剩余,可能选出的是红桃 + if (b < 13) // 红桃有剩余,可能选出的是红桃 v += dfs(a, b + 1, c, d, x, y) * (13 - b) / rst; - if (c < 13) //梅花有剩余,可能选出的是梅花 + if (c < 13) // 梅花有剩余,可能选出的是梅花 v += dfs(a, b, c + 1, d, x, y) * (13 - c) / rst; - if (d < 13) //方块有剩余,可能选出的是方块 + if (d < 13) // 方块有剩余,可能选出的是方块 v += dfs(a, b, c, d + 1, x, y) * (13 - d) / rst; - //如果小王没有被选出 + // 如果小王没有被选出 if (x == 0) v += min(min(dfs(a, b, c, d, 1, y), dfs(a, b, c, d, 2, y)), min(dfs(a, b, c, d, 3, y), dfs(a, b, c, d, 4, y))) / rst; - //如果大王没有被选出 + // 如果大王没有被选出 if (y == 0) v += min(min(dfs(a, b, c, d, x, 1), dfs(a, b, c, d, x, 2)), min(dfs(a, b, c, d, x, 3), dfs(a, b, c, d, x, 4))) / rst; @@ -125,11 +128,10 @@ double dfs(int a, int b, int c, int d, int x, int y) { int main() { cin >> A >> B >> C >> D; - //① 终点状态不唯一,起点是唯的的,所以以起点为终点,以终点为起点,反着推 - //② AcWing 217. 绿豆蛙的归宿 需要建图,本题不用建图 - double res = dfs(0, 0, 0, 0, 0, 0); //四种花色、大小王都还没有被抽取 - if (res > INF / 2) //因为是浮点数,不能用等号判断是不是相等,简单的办法就是INF/2 + double res = dfs(0, 0, 0, 0, 0, 0); // 四种花色、大小王都还没有被抽取 + + if (res > INF / 2) // 因为是浮点数,不能用等号判断是不是相等,简单的办法就是INF/2 puts("-1.000"); else printf("%.3f\n", res); @@ -137,16 +139,18 @@ int main() { } ``` -### 四、期望值为什么初始化为$1$? +#### $Q$:期望值为什么初始化为$1$? + +
-$f[i]$: 从$i$卡牌状态到终点状态所需要的**期望卡牌数** +$f[v_i]$: 从$i$卡牌状态到终点状态所需要的**期望卡牌数** 每次抽一张牌变到下个状态,所以每条路径的权值为$1$ -$$\large f[v]=p_1×(f[1]+1)+p_2×(f[2]+1)+p_3×(f[3]+1)+…+p_k×(f[k]+1) = \\ -\sum_{i=1}^{k}p_i+\sum_{i=1}^{k}p_i \times f[i] +$$\large f[v]=p_1×(f[v_1]+1)+p_2×(f[v_2]+1)+p_3×(f[v_3]+1)+…+p_k×(f[v_k]+1) = \\ +\sum_{i=1}^{k}p_i+\sum_{i=1}^{k}p_i \times f[v_i] $$ - 因为$v$一定能到达下个局面,所以下个状态的概率和为$1$,这里的$\large \displaystyle \sum_{i=1}^{k}p_i=1$ 那么就有:$\displaystyle \large f[v]=1+\sum_{i=1}^{k}p_i \times f[i]$  -综上这里的$f[v]$可以初始化为$1$! -
\ No newline at end of file + 因为$v$一定能到达下个局面,所以下个状态的概率和为$1$,这里的$\large \displaystyle \sum_{i=1}^{k}p_i=1$ 那么就有:$\displaystyle \large f[v]=1+\sum_{i=1}^{k}p_i \times f[v_i]$  +综上这里的$f[v]$可以初始化为$1$。 + diff --git a/TangDou/AcWing_TiGao/T5/QiWang/218_dfs.cpp b/TangDou/AcWing_TiGao/T5/QiWang/218_dfs.cpp deleted file mode 100644 index cb9302e..0000000 --- a/TangDou/AcWing_TiGao/T5/QiWang/218_dfs.cpp +++ /dev/null @@ -1,70 +0,0 @@ -#include -using namespace std; -const int N = 15; -const int INF = 0x3f3f3f3f; -double f[N][N][N][N][5][5]; -int st[N][N][N][N][5][5]; -int A, B, C, D; - -//如果大小王翻出来放1里,则a++,放2里b++,... -void add(int &a, int &b, int &c, int &d, int x) { - if (x == 1) a++; - if (x == 2) b++; - if (x == 3) c++; - if (x == 4) d++; -} - -/* -功能:计算当前状态f(a,b,c,d,x,y)下的期望值 -*/ -double dfs(int a, int b, int c, int d, int x, int y) { - //记忆化,同时因为f为double类型,不能使用传统的memset(0x3f)之类 - //进行初始化并判断是否修改过,只能再开一个st数组 - if (st[a][b][c][d][x][y]) return f[a][b][c][d][x][y]; - st[a][b][c][d][x][y] = 1; - - //递归出口:当前状态是否到达目标状态,目标状态的期望值是0 - int ta = a, tb = b, tc = c, td = d; //抄出来 - add(ta, tb, tc, td, x), add(ta, tb, tc, td, y); //大王小王会改变四个花色的数量 - if (ta >= A && tb >= B && tc >= C && td >= D) return 0; - - //当前状态下的剩余牌数量 - int rst = 54 - ta - tb - tc - td; - if (rst == 0) return INF; //还没有完成目标,没有剩余的牌了,无解 - - //当前状态可以向哪些状态转移 - // Q:v为什么要初始化为1? - // A:看题解内容 - double v = 1; - if (a < 13) //黑桃有剩余,可能选出的是黑桃 - v += dfs(a + 1, b, c, d, x, y) * (13 - a) / rst; - if (b < 13) //红桃有剩余,可能选出的是红桃 - v += dfs(a, b + 1, c, d, x, y) * (13 - b) / rst; - if (c < 13) //梅花有剩余,可能选出的是梅花 - v += dfs(a, b, c + 1, d, x, y) * (13 - c) / rst; - if (d < 13) //方块有剩余,可能选出的是方块 - v += dfs(a, b, c, d + 1, x, y) * (13 - d) / rst; - - //如果小王没有被选出 - if (x == 0) - v += min(min(dfs(a, b, c, d, 1, y), dfs(a, b, c, d, 2, y)), min(dfs(a, b, c, d, 3, y), dfs(a, b, c, d, 4, y))) / rst; - - //如果大王没有被选出 - if (y == 0) - v += min(min(dfs(a, b, c, d, x, 1), dfs(a, b, c, d, x, 2)), min(dfs(a, b, c, d, x, 3), dfs(a, b, c, d, x, 4))) / rst; - - return f[a][b][c][d][x][y] = v; -} - -int main() { - cin >> A >> B >> C >> D; - //① 终点状态不唯一,起点是唯的的,所以以起点为终点,以终点为起点,反着推 - //② AcWing 217. 绿豆蛙的归宿 需要建图,本题不用建图 - double res = dfs(0, 0, 0, 0, 0, 0); //四种花色、大小王都还没有被抽取 - - if (res > INF / 2) //因为是浮点数,不能用等号判断是不是相等,简单的办法就是INF/2 - puts("-1.000"); - else - printf("%.3f\n", res); - return 0; -} \ No newline at end of file diff --git a/TangDou/AcWing_TiGao/T5/QiWang/218_dp.cpp b/TangDou/AcWing_TiGao/T5/QiWang/218_dp.cpp deleted file mode 100644 index ca614d3..0000000 --- a/TangDou/AcWing_TiGao/T5/QiWang/218_dp.cpp +++ /dev/null @@ -1,48 +0,0 @@ -#include -using namespace std; -const int INF = 0x3f3f3f3f; -const int N = 15; -double f[N][N][N][N][5][5]; -int a, b, c, d; -int main() { - memset(f, -1, sizeof f); - cin >> a >> b >> c >> d; - - for (int i = 13; i >= 0; i--) - for (int j = 13; j >= 0; j--) - for (int k = 13; k >= 0; k--) - for (int w = 13; w >= 0; w--) - for (int x = 4; x >= 0; x--) - for (int y = 4; y >= 0; y--) { - double &v = f[i][j][k][w][x][y]; - if (i + (x == 1) + (y == 1) >= a && j + (x == 2) + (y == 2) >= b - && k + (x == 3) + (y == 3) >= c && w + (x == 4) + (y == 4) >= d) { - v = 0; - continue; - } - - v = 1; - int sum = i + j + k + w + (x != 0) + (y != 0); - if (i < 13) v += f[i + 1][j][k][w][x][y] * (13 - i) / (54 - sum); - if (j < 13) v += f[i][j + 1][k][w][x][y] * (13 - j) / (54 - sum); - if (k < 13) v += f[i][j][k + 1][w][x][y] * (13 - k) / (54 - sum); - if (w < 13) v += f[i][j][k][w + 1][x][y] * (13 - w) / (54 - sum); - if (x == 0) { - double t = INF; - for (int u = 1; u <= 4; u++) t = min(t, f[i][j][k][w][u][y] / (54 - sum)); - v += t; - } - if (y == 0) { - double t = INF; - for (int u = 1; u <= 4; u++) t = min(t, f[i][j][k][w][x][u] / (54 - sum)); - v += t; - } - } - - if (f[0][0][0][0][0][0] > 54) - printf("-1.000"); - else - printf("%.3lf", f[0][0][0][0][0][0]); - - return 0; -} \ No newline at end of file diff --git a/TangDou/Topic/HDU1385.cpp b/TangDou/Topic/HDU1385.cpp new file mode 100644 index 0000000..9f519e4 --- /dev/null +++ b/TangDou/Topic/HDU1385.cpp @@ -0,0 +1,70 @@ +#include +using namespace std; + +const int N = 110; +const int INF = 0x3f3f3f3f; +// Floyd+记录起点后继 +int n; +int g[N][N], w[N]; +int path[N][N]; // 记录i到j最短路径中i的后继 + +void floyd() { + for (int k = 1; k <= n; k++) + for (int i = 1; i <= n; i++) + for (int j = 1; j <= n; j++) { + if (g[i][j] > g[i][k] + g[k][j] + w[k]) { + g[i][j] = g[i][k] + g[k][j] + w[k]; + path[i][j] = path[i][k]; // i->j这条最短路径上,i后面第一个节点,是i->k路径上第一个节点 + } + // 相同路径下选择后继更小的(为了字典序) + if (g[i][j] == g[i][k] + g[k][j] + w[k]) + if (path[i][j] > path[i][k]) + path[i][j] = path[i][k]; + } +} + +// 递归输出路径 +void print(int s, int e) { + printf("-->%d", path[s][e]); // 输出s的后继 + if (path[s][e] != e) // 如果不是直连 + print(path[s][e], e); // 递归输出后继 +} +/* +From 1 to 3 : +Path: 1-->5-->4-->3 +Total cost : 21 + +From 3 to 5 : +Path: 3-->4-->5 +Total cost : 16 + +From 2 to 4 : +Path: 2-->1-->5-->4 +Total cost : 17 +*/ +int main() { +#ifndef ONLINE_JUDGE + freopen("HDU1385.in", "r", stdin); +#endif + while (cin >> n, n) { + for (int i = 1; i <= n; i++) + for (int j = 1; j <= n; j++) { + cin >> g[i][j]; + if (g[i][j] == -1) g[i][j] = INF; + path[i][j] = j; + } + + for (int i = 1; i <= n; i++) cin >> w[i]; + floyd(); + + int s, e; + + while (cin >> s >> e, ~s && ~e) { + printf("From %d to %d :\n", s, e); + printf("Path: %d", s); + if (s != e) print(s, e); // 起点与终点不同开始递归 + printf("\nTotal cost : %d\n\n", g[s][e]); + } + } + return 0; +} \ No newline at end of file diff --git a/TangDou/Topic/HDU1385.in b/TangDou/Topic/HDU1385.in new file mode 100644 index 0000000..722ece9 --- /dev/null +++ b/TangDou/Topic/HDU1385.in @@ -0,0 +1,12 @@ +5 +0 3 22 -1 4 +3 0 5 -1 -1 +22 5 0 9 20 +-1 -1 9 0 4 +4 -1 20 4 0 +5 17 8 3 1 +1 3 +3 5 +2 4 +-1 -1 +0 \ No newline at end of file diff --git a/TangDou/Topic/HDU1599.cpp b/TangDou/Topic/HDU1599.cpp new file mode 100644 index 0000000..30345d9 --- /dev/null +++ b/TangDou/Topic/HDU1599.cpp @@ -0,0 +1,48 @@ +#include +using namespace std; +#define int long long +#define endl "\n" +const int INF = 0x3f3f3f3f; +const int N = 110; + +int dis[N][N], g[N][N]; +int n, m, ans; + +void floyd() { + memcpy(dis, g, sizeof g); + for (int k = 1; k <= n; k++) { + // 最小环的DP操作 + for (int i = 1; i < k; i++) // 枚举i,j + for (int j = i + 1; j < k; j++) // 注意i,j,k不能相同 + if (ans > dis[i][j] + g[i][k] + g[k][j]) + ans = dis[i][j] + g[i][k] + g[k][j]; + + for (int i = 1; i <= n; i++) // 原floyd + for (int j = 1; j <= n; j++) + if (dis[i][j] > dis[i][k] + dis[k][j]) + dis[i][j] = dis[i][k] + dis[k][j]; + } +} +signed main() { + while (cin >> n >> m && (~n && ~m)) { + // 邻接矩阵初始化 + for (int i = 1; i <= n; i++) + for (int j = 1; j <= n; j++) + if (i == j) + g[i][j] = 0; + else + g[i][j] = INF; + + while (m--) { + int a, b, c; + cin >> a >> b >> c; + g[a][b] = g[b][a] = min(c, g[a][b]); // 防重边 + } + ans = INF; + floyd(); + if (ans == INF) + puts("It's impossible."); + else + cout << ans << endl; + } +} \ No newline at end of file diff --git a/TangDou/Topic/HDU1704.cpp b/TangDou/Topic/HDU1704.cpp new file mode 100644 index 0000000..e8872fb --- /dev/null +++ b/TangDou/Topic/HDU1704.cpp @@ -0,0 +1,36 @@ +#include +using namespace std; +#define inf 0x3f3f3f3f +const int N = 510; +int n, m, x, y, ans; +int g[N][N]; + +void floyd() { + for (int k = 1; k <= n; k++) + for (int i = 1; i <= n; i++) { + if (!g[i][k]) continue; // floyd优化 + for (int j = 1; j <= n; j++) + g[i][j] |= g[i][k] & g[k][j]; // 通过k传递,或运算 + } +} +int main() { + int T; + cin >> T; + while (T--) { + cin >> n >> m; + memset(g, 0, sizeof g); + while (m--) { + cin >> x >> y; + g[x][y] = 1; // x +using namespace std; +#define inf 0x3f3f3f3f +const int N = 310; +int t, n, m, q; +int g[N][N]; +bool flag[N]; // 记录是否标记 +int a, b, c; + +void floyd(int k) { // 以k为中转节点进行转移 + for (int i = 0; i < n; i++) + for (int j = 0; j < n; j++) + if (g[i][j] > g[i][k] + g[k][j]) + g[i][j] = g[i][k] + g[k][j]; +} + +int main() { + // 加快读入 + ios::sync_with_stdio(false), cin.tie(0); + while (cin >> n >> m >> q && n + m + q) { + if (t) printf("\n"); // 谜之格式 + printf("Case %d:\n", ++t); + + // 整体正无穷,对角线清零 + memset(g, inf, sizeof g); + for (int i = 0; i <= n; i++) g[i][i] = 0; + + memset(flag, false, sizeof flag); + + while (m--) { + cin >> a >> b >> c; + g[a][b] = min(c, g[a][b]); // floyd也可以跑有向图 + } + while (q--) { + cin >> c; + if (c == 0) { + cin >> a; + if (flag[a]) // 如果a已经被标记过了 + printf("ERROR! At point %d\n", a); + else { + flag[a] = true; // 标记上 + floyd(a); // 通过a进行其它节点转移 + } + } else { + cin >> a >> b; + if (!(flag[a] && flag[b])) + printf("ERROR! At path %d to %d\n", a, b); + else if (g[a][b] == inf) + printf("No such path\n"); + else + printf("%d\n", g[a][b]); + } + } + } + return 0; +} \ No newline at end of file diff --git a/TangDou/Topic/P1364.cpp b/TangDou/Topic/P1364.cpp new file mode 100644 index 0000000..4cd6375 --- /dev/null +++ b/TangDou/Topic/P1364.cpp @@ -0,0 +1,38 @@ +#include +using namespace std; +const int N = 1000010; +const int INF = 0x3f3f3f3f; + +int g[150][150]; +int w[N]; // 居民人口数,点权 + +int main() { + int n; + cin >> n; + + // 地图初始化 + memset(g, 0x3f, sizeof g); + for (int i = 1; i <= n; i++) g[i][i] = 0; + + for (int i = 1; i <= n; i++) { + int a, b; + cin >> w[i] >> a >> b; // w[i]:点权 + g[i][a] = g[a][i] = 1; // i<->a无向边 + g[i][b] = g[b][i] = 1; // i<->b无向边 + } + + // floyd + for (int k = 1; k <= n; k++) + for (int i = 1; i <= n; i++) + for (int j = 1; j <= n; j++) + if (g[i][j] > g[i][k] + g[k][j]) g[i][j] = g[i][k] + g[k][j]; + + int ans = INF; + for (int i = 1; i <= n; i++) { // 如果将医院设置在i处,那么计算一下它到各地的加权和 + int s = 0; + for (int j = 1; j <= n; j++) s += w[j] * g[i][j]; + ans = min(ans, s); + } + printf("%d", ans); + return 0; +} \ No newline at end of file diff --git a/TangDou/Topic/P1828.cpp b/TangDou/Topic/P1828.cpp new file mode 100644 index 0000000..80fb5f9 --- /dev/null +++ b/TangDou/Topic/P1828.cpp @@ -0,0 +1,42 @@ +#include +using namespace std; +const int N = 814; +const int INF = 0x3f3f3f3f; +int id[N]; +int a, b, c, g[N][N], res = INF; +int main() { +#ifndef ONLINE_JUDGE + freopen("P1828_11.in", "r", stdin); + // 参考答案:8 +#endif + // 加快读入 + ios::sync_with_stdio(false), cin.tie(0); + int p, n, m; // p只奶牛,n个牧场,m条边 + cin >> p >> n >> m; + + memset(g, 0x3f, sizeof g); + for (int i = 1; i <= n; i++) g[i][i] = 0; // 初始化 + + for (int i = 1; i <= p; i++) cin >> id[i]; // i号奶牛,在id[i]这个牧场 + + while (m--) { + cin >> a >> b >> c; + g[a][b] = g[b][a] = min(c, g[a][b]); + } + // 标准的Floyd板子 + for (int k = 1; k <= n; k++) + for (int i = 1; i <= n; i++) { + if (g[i][k] == INF) continue; // floyd小优化 + for (int j = 1; j <= n; j++) + if (g[i][j] > g[i][k] + g[k][j]) + g[j][i] = g[i][j] = g[i][k] + g[k][j]; + } + + for (int i = 1; i <= n; i++) { // 每个牧场出发 + int ans = 0; + for (int j = 1; j <= p; j++) ans += g[i][id[j]]; + if (ans >= 0) res = min(res, ans); + } + printf("%d", res); + return 0; +} diff --git a/TangDou/Topic/P1828_11.in b/TangDou/Topic/P1828_11.in new file mode 100644 index 0000000..aea7f2f --- /dev/null +++ b/TangDou/Topic/P1828_11.in @@ -0,0 +1,9 @@ +3 5 5 +2 +3 +4 +1 2 1 +1 3 5 +2 3 7 +2 4 3 +3 4 5 diff --git a/TangDou/Topic/POJ3259.cpp b/TangDou/Topic/POJ3259.cpp new file mode 100644 index 0000000..da42f28 --- /dev/null +++ b/TangDou/Topic/POJ3259.cpp @@ -0,0 +1,51 @@ +#include +#include +#include +#include +using namespace std; +const int INF = 0x3f3f3f3f; + +const int N = 502; +int n, m, w; +int g[N][N]; + +// floyd判断是否存在负圈 +bool floyd() { + for (int k = 1; k <= n; k++) + for (int i = 1; i <= n; i++) + if (g[i][k] != INF) { // 优化 + for (int j = 1; j <= n; j++) + if (g[i][j] > g[i][k] + g[k][j]) + g[i][j] = g[i][k] + g[k][j]; + if (g[i][i] < 0) return true; // 发现负圈 + } + return false; +} +int main() { + int T; + cin >> T; + while (T--) { + cin >> n >> m >> w; + memset(g, INF, sizeof g); // 初始化邻接矩阵 + + // 双向正值边 + while (m--) { + int a, b, c; + cin >> a >> b >> c; + // 注意坑:重边 + g[a][b] = g[b][a] = min(c, g[a][b]); + } + // 单向负值边 + while (w--) { + int a, b, c; + cin >> a >> b >> c; + g[a][b] = -c; // 负值边 + } + + if (floyd()) + puts("YES"); + else + puts("NO"); + } + return 0; +} \ No newline at end of file diff --git a/TangDou/Topic/SSL1613.cpp b/TangDou/Topic/SSL1613.cpp new file mode 100644 index 0000000..5d667dc --- /dev/null +++ b/TangDou/Topic/SSL1613.cpp @@ -0,0 +1,43 @@ +#include +using namespace std; +int o(int t) { + return t * t; +} +const int N = 110; +int n, m, x[N], y[N]; +double g[N][N]; + +int main() { + cin >> n; + for (int i = 1; i <= n; i++) cin >> x[i] >> y[i]; + + // double类型的数组,初始化不能用memset!!!! + // memset(g, 0x3f, sizeof g); + // for (int i = 0; i <= n; i++) g[i][i] = 0; + + // 需要用二重循环进行初始化 + for (int i = 1; i <= n; i++) + for (int j = 1; j <= n; j++) { + if (i == j) + g[i][j] = 0; + else + g[i][j] = 0x3f3f3f3f; + } + + cin >> m; + int l, r; + while (m--) { + cin >> l >> r; + g[r][l] = g[l][r] = sqrt((double)o(x[l] - x[r]) + (double)o(y[l] - y[r])); // 勾股定理 + } + + // floyd + for (int k = 1; k <= n; k++) + for (int i = 1; i <= n; i++) + for (int j = 1; j <= n; j++) + if (g[i][j] > g[i][k] + g[k][j]) g[i][j] = g[i][k] + g[k][j]; + + cin >> l >> r; + printf("%.2lf", g[l][r]); + return 0; +} diff --git a/TangDou/Topic/SSL_1760.cpp b/TangDou/Topic/SSL_1760.cpp new file mode 100644 index 0000000..65d006d --- /dev/null +++ b/TangDou/Topic/SSL_1760.cpp @@ -0,0 +1,29 @@ +#include +using namespace std; +const int N = 210; +int n, g[N][N]; +const int INF = 0x3f3f3f3f; +int ans = INF; +int main() { + cin >> n; + for (int i = 1; i <= n; i++) + for (int j = 1; j <= n; j++) { + cin >> g[i][j]; + if (g[i][j] == 0 && i != j) g[i][j] = INF; // 建图(注意i==j要为0) + } + // floyd + for (int k = 1; k <= n; k++) + for (int i = 1; i <= n; i++) + for (int j = 1; j <= n; j++) + if (g[i][j] > g[i][k] + g[k][j]) + g[i][j] = g[i][k] + g[k][j]; + + int s; + for (int i = 1; i <= n; i++) { + s = 0; + for (int j = 1; j <= n; j++) s += g[i][j]; + if (s < ans) ans = s; + } + printf("%d", ans); + return 0; +} diff --git a/TangDou/Topic/【Floyd专题】.md b/TangDou/Topic/【Floyd专题】.md new file mode 100644 index 0000000..7d7becb --- /dev/null +++ b/TangDou/Topic/【Floyd专题】.md @@ -0,0 +1,639 @@ +## 图论-多源最短路径($Floyd$算法) + +### 一、$Floyd$ +$Floyd$算法是一次性求所有结点之间的最短距离,能处理负权边的图,程序比暴力的$DFS$更简单,但是复杂度是$O(n^3)$,只适合 $n < 200$的情况。 +$Floyd$运用了 **动态规划** 的思想,求 $i 、 j$两点的最短距离,可分两种情况考虑,即经过图中某个点 $k$的路径和不经过点 $k$ 的路径,**取两者中的最短路径**。 + + +### 二、模板 +```cpp {.line-numbers} +void floyd() { + for (int k = 1; k <= n; k++) + for (int i = 1; i <= n; i++) + if (g[i][k] != inf) //优化 + for (int j = 1; j <= n; j++) + if (g[i][j] > g[i][k] + g[k][j]) + g[i][j] = g[i][k] + g[k][j]; +} +``` + +### 三、最短路+思维 + +#### [$AcWing$ $1125$ 牛的旅行](https://www.cnblogs.com/littlehb/p/16025700.html) + +总结: +- 一遍$floyd$算出原始各连通块内的多源最短路径 +- 遍历枚举找出每个点在自己连通块中可以到达的最远距离,$PK$后获取到原始的最大直径长度 +- 遍历所有可能连接上的两个不在同一连通块中的点,尝试连接上这两个点后,得到可以获得到的最小直径。 +- 原始直径与遍历尝试的所有可能直径$PK$,谁大谁是答案。 + +### 四、判负环 + +眼尖的人儿可能发现邻接矩阵 $g$ 中, $g[i][i]$并没有赋初值$0$,而是 $inf$。并且计算后 $g[i][i]$的值也不是 $0$,而是 $g[i][i]=g[i][u]+……+g[v][i]$,即从外面绕一圈回来的最短路径,而这正 **用于判断负圈**,即 $g[i][i]<0$。 + +#### [$POJ-3259$ $Wormholes$](https://link.juejin.cn/?target=https%3A%2F%2Fvjudge.net%2Fproblem%2FPOJ-3259) + +**类型** +判负环 + +**题意** +- 正常路是$m$条双向正权边 +- 虫洞是$w$条单向负权边 +- 题目让判断是否有负权回路 + +**办法** +利用$Floyd$找两点间花费的最短时间,判断从起始位置到起始位置的最短时间是否为负值(判断负权环),若为负值,说明他通过虫洞回到起始位置时比自己最初离开起始位置的时间早。 + +**代码实现**: +在第二重循环,求完第$i$个结点后判断。$i$到$i$之间的最短距离是一个负值,说明存在一个经过它的负环。 + +```cpp {.line-numbers} +#include +#include +#include +#include +using namespace std; +const int INF = 0x3f3f3f3f; + +const int N = 502; +int n, m, w; +int g[N][N]; + +// floyd判断是否存在负圈 +bool floyd() { + for (int k = 1; k <= n; k++) + for (int i = 1; i <= n; i++) + if (g[i][k] != INF) { // 优化 + for (int j = 1; j <= n; j++) + if (g[i][j] > g[i][k] + g[k][j]) + g[i][j] = g[i][k] + g[k][j]; + if (g[i][i] < 0) return true; // 发现负圈 + } + return false; +} +int main() { + int T; + cin >> T; + while (T--) { + cin >> n >> m >> w; + memset(g, INF, sizeof g); // 初始化邻接矩阵 + + // 双向正值边 + while (m--) { + int a, b, c; + cin >> a >> b >> c; + // 注意坑:重边 + g[a][b] = g[b][a] = min(c, g[a][b]); + } + // 单向负值边 + while (w--) { + int a, b, c; + cin >> a >> b >> c; + g[a][b] = -c; // 负值边 + } + + if (floyd()) + puts("YES"); + else + puts("NO"); + } + return 0; +} +``` + +### 五、打印路径 + +#### [$HDU-1385$ $Minimum$ $Transport$ $Cost$](http://acm.hdu.edu.cn/showproblem.php?pid=1385) + +**类型** +打印路径 + +**题意** +给你所有城市到其他城市的道路成本和经过每个城市的城市税,给你很多组城市,要求你找出每组城市间的最低运输成本并且输出路径,**如果有多条路径则输出字典序最小的那条路径**。 **注意**,起点城市和终点城市不需要收城市税(中间点才收税,也就是插值的$k$收税)。 + +**分析** +输出路径,多个答案则输出字典序最小的,无法到达输出$-1$。 +读入邻接表, $w[]$记录每个城市额外费用, $path[][]$记录路径,$floyd()$里维护即可。然后处理下输出(比较恶心)。 + +> **解释**:`int path[N][N]; ` +$i \rightarrow j$ 可能存在多条路线,我要找最短的。如果有多条最短的,我要字典序最小的。现在路线唯一了吧!比如这条路线最终是 +$i \rightarrow a \rightarrow b \rightarrow c \rightarrow d \rightarrow j$,则$path[i][j]=a$,也就是第一个后继节点。 + +```cpp {.line-numbers} +#include +using namespace std; + +const int N = 110; +const int INF = 0x3f3f3f3f; +// Floyd+记录起点后继 +int n; +int g[N][N], w[N]; +int path[N][N]; // 记录i到j最短路径中i的后继 + +void floyd() { + for (int k = 1; k <= n; k++) + for (int i = 1; i <= n; i++) + for (int j = 1; j <= n; j++) { + if (g[i][j] > g[i][k] + g[k][j] + w[k]) { + g[i][j] = g[i][k] + g[k][j] + w[k]; + path[i][j] = path[i][k]; // i->j这条最短路径上,i后面第一个节点,是i->k路径上第一个节点 + } + // 相同路径下选择后继更小的(为了字典序) + if (g[i][j] == g[i][k] + g[k][j] + w[k]) + if (path[i][j] > path[i][k]) + path[i][j] = path[i][k]; + } +} + +// 递归输出路径 +void print(int s, int e) { + printf("-->%d", path[s][e]); // 输出s的后继 + if (path[s][e] != e) // 如果不是直连 + print(path[s][e], e); // 递归输出后继 +} +/* +From 1 to 3 : +Path: 1-->5-->4-->3 +Total cost : 21 + +From 3 to 5 : +Path: 3-->4-->5 +Total cost : 16 + +From 2 to 4 : +Path: 2-->1-->5-->4 +Total cost : 17 +*/ +int main() { +#ifndef ONLINE_JUDGE + freopen("HDU1385.in", "r", stdin); +#endif + while (cin >> n, n) { + for (int i = 1; i <= n; i++) + for (int j = 1; j <= n; j++) { + cin >> g[i][j]; + if (g[i][j] == -1) g[i][j] = INF; + path[i][j] = j; + } + + for (int i = 1; i <= n; i++) cin >> w[i]; + floyd(); + + int s, e; + + while (cin >> s >> e, ~s && ~e) { + printf("From %d to %d :\n", s, e); + printf("Path: %d", s); + if (s != e) print(s, e); // 起点与终点不同开始递归 + printf("\nTotal cost : %d\n\n", g[s][e]); + } + } + return 0; +} +``` + +### 六、最小环 +#### [$HDU$-$1599$ $find$ $the$ $mincost$ $route$](https://acm.hdu.edu.cn/showproblem.php?pid=1599) + +**类型: 最小环** + +**题意**: +>杭州有$N$个景区,景区之间有一些双向的路来连接,现在$8600$想找一条旅游路线,这个路线从$A$点出发并且最后回到$A$点,假设经过的路线为$V_1,V_2,…V_K$,$V_1$,那么必须满足$K>2$,就是说至除了出发点以外至少要经过$2$个其他不同的景区,而且不能重复经过同一个景区。现在$8600$需要你帮他找一条这样的路线,并且花费越少越好。 +>**$Input$** +第一行是$2$个整数$N$和$M$($N <= 100, M <= 1000$),代表景区的个数和道路的条数。 +接下来的$M$行里,每行包括$3$个整数$a,b,c$.代表$a$和$b$之间有一条通路,并且需要花费$c$元($c <= 100$)。 +**$Output$** +对于每个测试实例,如果能找到这样一条路线的话,输出花费的最小值。如果找不到的话,输出"It’s impossible.". +**$Sample$ $Input$** +cpp +3 3 +1 2 1 +2 3 1 +1 3 1 +3 3 +1 2 1 +1 2 3 +2 3 1 +**$Sample$ $Output$** +3 +It’s impossible + +**分析**: +求最小环,用$g[]$记录原距离,当枚举中间结点 $k$时,首先知道任意两点 $i、j$不经过 $k$的最短路径 $dis[i][j]$(原$floyd$的二三重循环后更新 $dis[i][j]$得到经过$k$的最短路),此时枚举 $i$和 $j$得到一个经过 $k$的环( $i$到 $j$, $j$到 $k$, $k$到 $i$)并记录最小答案即可,即 $dis[i][j] + g[i][k] + g[k][j]$。 +注意题目 $i, j, k$不能相同,还有坑点:`long long` + +```cpp {.line-numbers} +#include +using namespace std; +#define int long long +#define endl "\n" +const int INF = 0x3f3f3f3f; +const int N = 110; + +int dis[N][N], g[N][N]; +int n, m, ans; + +void floyd() { + memcpy(dis, g, sizeof g); + for (int k = 1; k <= n; k++) { + // 最小环的DP操作 + for (int i = 1; i < k; i++) // 枚举i,j + for (int j = i + 1; j < k; j++) // 注意i,j,k不能相同 + if (ans > dis[i][j] + g[i][k] + g[k][j]) + ans = dis[i][j] + g[i][k] + g[k][j]; + + for (int i = 1; i <= n; i++) // 原floyd + for (int j = 1; j <= n; j++) + if (dis[i][j] > dis[i][k] + dis[k][j]) + dis[i][j] = dis[i][k] + dis[k][j]; + } +} +signed main() { + while (cin >> n >> m && (~n && ~m)) { + // 邻接矩阵初始化 + for (int i = 1; i <= n; i++) + for (int j = 1; j <= n; j++) + if (i == j) + g[i][j] = 0; + else + g[i][j] = INF; + + while (m--) { + int a, b, c; + cin >> a >> b >> c; + g[a][b] = g[b][a] = min(c, g[a][b]); // 防重边 + } + ans = INF; + floyd(); + if (ans == INF) + puts("It's impossible."); + else + cout << ans << endl; + } +} +``` + **练习题** + #### [$AcWing$ $344$. 观光之旅](https://www.cnblogs.com/littlehb/p/16033489.html) + + +### 七、传递闭包 +#### [$HDU$-$1704$ $Rank$](https://acm.hdu.edu.cn/showproblem.php?pid=1704) + + +**题意** +给出$M$对胜负关系,胜负关系有传递性(若$A$胜$B$,$B$胜$C$则$A$胜$C$), **求有多少对不能确定的胜负关系** + +**解法**:思路很简单,$floyd$ 一遍做传递闭包,然后暴力枚举就行辣,但是竟然会$TLE$,然后上网学了一种新的优化姿势(其实这种优化用处不大,但由于本题是非常稀疏的图,所以$O(N^3)$几乎变成了$O(N^2)$) + +```cpp {.line-numbers} +#include +using namespace std; +#define inf 0x3f3f3f3f +const int N = 510; +int n, m, x, y, ans; +int g[N][N]; + +void floyd() { + for (int k = 1; k <= n; k++) + for (int i = 1; i <= n; i++) { + if (!g[i][k]) continue; // floyd优化 + for (int j = 1; j <= n; j++) + g[i][j] |= g[i][k] & g[k][j]; // 通过k传递,或运算 + } +} +int main() { + int T; + cin >> T; + while (T--) { + cin >> n >> m; + memset(g, 0, sizeof g); + while (m--) { + cin >> x >> y; + g[x][y] = 1; // x +using namespace std; +#define inf 0x3f3f3f3f +const int N = 310; +int t, n, m, q; +int g[N][N]; +bool flag[N]; // 记录是否标记 +int a, b, c; + +void floyd(int k) { // 以k为中转节点进行转移 + for (int i = 0; i < n; i++) + for (int j = 0; j < n; j++) + if (g[i][j] > g[i][k] + g[k][j]) + g[i][j] = g[i][k] + g[k][j]; +} + +int main() { + // 加快读入 + ios::sync_with_stdio(false), cin.tie(0); + while (cin >> n >> m >> q && n + m + q) { + if (t) printf("\n"); // 谜之格式 + printf("Case %d:\n", ++t); + + // 整体正无穷,对角线清零 + memset(g, inf, sizeof g); + for (int i = 0; i <= n; i++) g[i][i] = 0; + + memset(flag, false, sizeof flag); + + while (m--) { + cin >> a >> b >> c; + g[a][b] = min(c, g[a][b]); // floyd也可以跑有向图 + } + while (q--) { + cin >> c; + if (c == 0) { + cin >> a; + if (flag[a]) // 如果a已经被标记过了 + printf("ERROR! At point %d\n", a); + else { + flag[a] = true; // 标记上 + floyd(a); // 通过a进行其它节点转移 + } + } else { + cin >> a >> b; + if (!(flag[a] && flag[b])) + printf("ERROR! At path %d to %d\n", a, b); + else if (g[a][b] == inf) + printf("No such path\n"); + else + printf("%d\n", g[a][b]); + } + } + } + return 0; +} +``` +### 九、限定边数量的情况下求多源最短路径 + +#### [$AcWing$ $345$ 牛站](https://www.cnblogs.com/littlehb/p/16043039.html) + +### 十、其它习题 + +#### $SSL-1760$(商店选址) +**题目** +给出一个城市的地图(用邻接矩阵表示),商店设在一点,使各个地方到商店距离之和最短。 + +$Input$ +第一行为$n$(共有几个城市); $N$小于$201$ +第二行至第$n+1$行为城市地图(用邻接矩阵表示) + +$Output$ +最短路径之和 + +$Sample$ $Input$ +```cpp {.line-numbers} +3 +0 3 1 +3 0 2 +1 2 0 +1 +2 +3 +4 +``` + +$Sample$ $Output$ +```cpp {.line-numbers} +3 +1 +``` +```cpp {.line-numbers} +#include +using namespace std; +const int N = 210; +int n, g[N][N]; +const int INF = 0x3f3f3f3f; +int ans = INF; +int main() { + cin >> n; + for (int i = 1; i <= n; i++) + for (int j = 1; j <= n; j++) { + cin >> g[i][j]; + if (g[i][j] == 0 && i != j) g[i][j] = INF; // 建图(注意i==j要为0) + } + // floyd + for (int k = 1; k <= n; k++) + for (int i = 1; i <= n; i++) + for (int j = 1; j <= n; j++) + if (g[i][j] > g[i][k] + g[k][j]) + g[i][j] = g[i][k] + g[k][j]; + + int s; + for (int i = 1; i <= n; i++) { + s = 0; + for (int j = 1; j <= n; j++) s += g[i][j]; + if (s < ans) ans = s; + } + printf("%d", ans); + return 0; +} + +``` +#### [$P1828$ [$USACO3.2$] 香甜的黄油 $Sweet$ $Butter$](https://www.luogu.com.cn/problem/P1828) +```cpp {.line-numbers} +#include +using namespace std; +const int N = 814; +const int INF = 0x3f3f3f3f; +int id[N]; +int a, b, c, g[N][N], res = INF; +int main() { +#ifndef ONLINE_JUDGE + freopen("P1828_11.in", "r", stdin); + // 参考答案:8 +#endif + // 加快读入 + ios::sync_with_stdio(false), cin.tie(0); + int p, n, m; // p只奶牛,n个牧场,m条边 + cin >> p >> n >> m; + + memset(g, 0x3f, sizeof g); + for (int i = 1; i <= n; i++) g[i][i] = 0; // 初始化 + + for (int i = 1; i <= p; i++) cin >> id[i]; // i号奶牛,在id[i]这个牧场 + + while (m--) { + cin >> a >> b >> c; + g[a][b] = g[b][a] = min(c, g[a][b]); + } + // 标准的Floyd板子 + for (int k = 1; k <= n; k++) + for (int i = 1; i <= n; i++) { + if (g[i][k] == INF) continue; // floyd小优化 + for (int j = 1; j <= n; j++) + if (g[i][j] > g[i][k] + g[k][j]) + g[j][i] = g[i][j] = g[i][k] + g[k][j]; + } + + for (int i = 1; i <= n; i++) { // 每个牧场出发 + int ans = 0; + for (int j = 1; j <= p; j++) ans += g[i][id[j]]; + if (ans >= 0) res = min(res, ans); + } + printf("%d", res); + return 0; +} + +``` + +#### [$P1364$ 医院设置](https://www.luogu.com.cn/problem/P1364) + +```cpp {.line-numbers} +#include +using namespace std; +const int N = 1000010; +const int INF = 0x3f3f3f3f; + +int g[150][150]; +int w[N]; // 居民人口数,点权 + +int main() { + int n; + cin >> n; + + // 地图初始化 + memset(g, 0x3f, sizeof g); + for (int i = 1; i <= n; i++) g[i][i] = 0; + + for (int i = 1; i <= n; i++) { + int a, b; + cin >> w[i] >> a >> b; // w[i]:点权 + g[i][a] = g[a][i] = 1; // i<->a无向边 + g[i][b] = g[b][i] = 1; // i<->b无向边 + } + + // floyd + for (int k = 1; k <= n; k++) + for (int i = 1; i <= n; i++) + for (int j = 1; j <= n; j++) + if (g[i][j] > g[i][k] + g[k][j]) g[i][j] = g[i][k] + g[k][j]; + + int ans = INF; + for (int i = 1; i <= n; i++) { // 如果将医院设置在i处,那么计算一下它到各地的加权和 + int s = 0; + for (int j = 1; j <= n; j++) s += w[j] * g[i][j]; + ans = min(ans, s); + } + printf("%d", ans); + return 0; +} +``` + +#### [$SSL-1613$]() +> Description +平面上有$n$个点($N<=100$),每个点的坐标均在$-10000\sim 10000$之间。其中的一些点之间有连线。若有连线,则表示可从一个点到达另一个点,即两点间有通路,通路的距离为两点直线的 **距离** 。现在的任务是找出从一点到另一点之间的最短路径。 + +$Input$ +输入文件$short.in$,共有$n+m+3$行,其中: +第一行为一个整数$n$。 +第$2$行到第$n+1$行(共$n$行),每行的两个整数$x$和$y$,描述一个点的坐标(以一个空格隔开)。 + +第$n+2$行为一个整数$m$,表示图中的连线个数。 +此后的$m$行,每行描述一条连线,由两个整数$i,j$组成,表示第$i$个点和第$j$个点之间有连线。 +最后一行:两个整数$s$和$t$,分别表示源点和目标点。 + +$Output$ +输出文件$short.out$仅一行,一个实数(保留两位小数),表示从$S$到$T$的最短路径的长度。 + +$Sample$ $Input$ +```cpp {.line-numbers} +5 +0 0 +2 0 +2 2 +0 2 +3 1 +5 +1 2 +1 3 +1 4 +2 5 +3 5 +1 5 +``` + +Sample Output +```cpp {.line-numbers} +3.41 +``` + +#### $Code$ +```cpp {.line-numbers} +#include +using namespace std; +int o(int t) { + return t * t; +} +const int N = 110; +int n, m, x[N], y[N]; +double g[N][N]; + +int main() { + cin >> n; + for (int i = 1; i <= n; i++) cin >> x[i] >> y[i]; + + // double类型的数组,初始化不能用memset!!!! + // memset(g, 0x3f, sizeof g); + // for (int i = 0; i <= n; i++) g[i][i] = 0; + + // 需要用二重循环进行初始化 + for (int i = 1; i <= n; i++) + for (int j = 1; j <= n; j++) { + if (i == j) + g[i][j] = 0; + else + g[i][j] = 0x3f3f3f3f; + } + + cin >> m; + int l, r; + while (m--) { + cin >> l >> r; + g[r][l] = g[l][r] = sqrt((double)o(x[l] - x[r]) + (double)o(y[l] - y[r])); // 勾股定理 + } + + // floyd + for (int k = 1; k <= n; k++) + for (int i = 1; i <= n; i++) + for (int j = 1; j <= n; j++) + if (g[i][j] > g[i][k] + g[k][j]) g[i][j] = g[i][k] + g[k][j]; + + cin >> l >> r; + printf("%.2lf", g[l][r]); + return 0; +} +``` \ No newline at end of file diff --git a/TangDou/Topic/【最短路径】Dijkstra算法专题.md b/TangDou/Topic/【最短路径】Dijkstra算法专题.md index 8c4efaa..c3df912 100644 --- a/TangDou/Topic/【最短路径】Dijkstra算法专题.md +++ b/TangDou/Topic/【最短路径】Dijkstra算法专题.md @@ -596,4 +596,10 @@ int main() { cout << res << endl; return 0; } +``` + +$TODO$ +#### [$P2176$ $RoadBlock$ $S$](https://www.luogu.com.cn/problem/P2176) +```cpp {.line-numbers} + ``` \ No newline at end of file