黄海 2 years ago
commit b7c3f8675f

@ -1,50 +0,0 @@
#include <bits/stdc++.h>
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;
}

@ -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<B
A<C
B<C
C<D
B<D
A<B
3 2
A<B
B<A
26 1
A<Z
0 0
```
**输出样例$1$**
```cpp {.line-numbers}
Sorted sequence determined after 4 relations: ABCD.
Inconsistency found after 2 relations.
Sorted sequence cannot be determined.
```
**输入样例$2$**
```cpp {.line-numbers}
6 6
A<F
B<D
C<E
F<D
D<E
E<F
0 0
```
**输出样例$2$**
```cpp {.line-numbers}
Inconsistency found after 6 relations.
```
**输入样例$3$**
```cpp {.line-numbers}
5 5
A<B
B<C
C<D
D<E
E<A
0 0
```
**输出样例$3$**
```cpp {.line-numbers}
Sorted sequence determined after 4 relations: ABCDE.
```
### 二、$floyd$ 求传递闭包
**概念**
给定若干对元素和若干对二元关系,并且关系具有传递性,通过传递性推导出尽量多的元素之间关系的问题被称为 **传递闭包**。
>**解释**:比如$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
for (int i = 0; i < n; i++)
if (f[i][i]) return 2;
// 存在还没有识别出关系的两个点i,j,还要继续读入
for (int i = 0; i < n; i++)
for (int j = 0; j < i; j++)
if (!f[i][j] && !f[j][i]) return 3;
return 1;
}
```
* ① $f[i][i] = 1$ 发生矛盾
* ② $f[i][j] = f[j][i] = 0$ 表示$i$与$j$之间的大小关系还没有确定下来,需要继续读取下一对二元关系
* ③ 所有的关系都确定,而且没有发生矛盾
```cpp {.line-numbers}
string getorder(){
char s[26];
for(int i = 0;i < n;i++){
int cnt = 0;
for(int j = 0;j < n;j++) cnt += f[i][j];//i
s[n - cnt - 1] = i + 'A'; //反着才能记录下名次
}
return string(s,s + n); //用char数组构造出string返回
}
```
> **解释**:确定所有元素次序后如何判断元素`i`在第几个位置呢?`f[i][j] = 1`表示`i < j`,因此计算下`i`小于元素的个数`cnt`,就可以判定`i`是第`cnt + 1`大的元素了
#### $Code$
```cpp {.line-numbers}
#include <bits/stdc++.h>
// 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<i
for (int i = 0; i < n; i++)
if (f[i][i]) return 2;
// 存在还没有识别出关系的两个点i,j,还要继续读入
for (int i = 0; i < n; i++)
for (int j = 0; j < i; j++)
if (!f[i][j] && !f[j][i]) return 3;
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 += f[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字符数组为字符串
return string(s, s + n);
}
int main() {
// n个变量,m个不等式
// 当输入一行 0 0 时,表示输入终止
while (scanf("%d %d", &n, &m), n && m) {
string S;
int k = 3; // 3:不能确定两两之间的关系
memset(f, 0, sizeof f); // 初始化邻接矩阵
// m条边
for (int i = 1; i <= m; i++) {
cin >> S;
// 已确定或者出现了矛盾,就没有必要再处理了,但是,还需要耐心的读取完毕,因为可能还有下一轮,不读入完耽误下一轮
if (k < 3) continue;
// 变量只可能为大写字母A~Z,映射到0~25
int a = S[0] - 'A', b = S[2] - 'A';
f[a][b] = 1; // 记录a<b
// 每输入一个关系,就计算一遍传递闭包
floyd();
// 检查一下现在的情况,是不是已经可以判定了
k = check();
if (k == 2) // 出现矛盾
printf("Inconsistency found after %d relations.\n", i);
else if (k == 1) { // 可以确定
string ans = getorder(); // 输出升序排列的所有变量
printf("Sorted sequence determined after %d relations: %s.\n", i, ans.c_str());
}
}
// 所有表达式都输入了,仍然定不下来关系
if (k == 3) printf("Sorted sequence cannot be determined.\n");
}
return 0;
}
```
### 三、拓扑序解法
开始想到拓扑排序,但是感觉从前往后枚举一遍会超时,然后看了蓝书$floyd$的做法实现了一遍,感觉还不如直接拓扑。
大致是:
$m$次循环,每次加入一条边到图中,再跑一遍拓扑排序
- 排序后,排序数组不为$n$个,则表示有环,矛盾,跳出循环
- 排序后,排序数组为$n$个,但是在过程中,有$2$个或以上的点在队列中,表示拓扑序并不唯一,那么此时并不能确定所有点的顺序,因此进行下一次循环
- 排序后,排序数组为$n$个,且在过程中,队列中一直只有一个,拓扑序唯一,输出结果,跳出循环
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int N = 30, M = N * N;
int n, m;
int a[1050], b[1050]; // a[i]<b[i]
char s[N]; // 输入的偏序关系
int in[N], ind[N];
int d[N], dl;
// 链式前向星
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++;
}
/*
拓扑序
(1) 出队列节点数量小于n,表示有环,矛盾
(2) 出队列节点数量等于n,在过程中有2个或以上的点在队列中表示拓扑序并不唯一那么此时并不能确定所有点的顺序
(3) 出队列节点数量等于n,在过程中,队列中一直只有一个,拓扑序唯一
*/
int topsort() {
queue<int> 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<C
a[i] = s[0] - 'A', b[i] = s[2] - 'A'; // 用两个数组a[],b[]记录关系,表示a[i]<b[i]
}
bool flag = 1; // 是不是已经找出了全部的大于关系序列
// 逐个讨论每个大小关系
for (int i = 1; i <= m; i++) {
dl = 0; // 拓扑序输出数组清零
add(a[i], b[i]); // 建图
ind[b[i]]++; // 记录b[i]入度
// 因为topsort会在过程中执行--ind[j]而此图和入度的值后面还要继续用不能让topsort改坏了
// 复制出来一个临时的入度数组in[]
memcpy(in, ind, sizeof ind);
// 每输入一个关系表达式就topsort一次
int res = topsort();
// 拓扑序唯一
if (res == 3) {
printf("Sorted sequence determined after %d relations: ", i);
for (int j = 1; j <= dl; j++) printf("%c", d[j] + 'A');
puts(".");
flag = 0;
break;
} else if (res == 1) { // 有环
printf("Inconsistency found after %d relations.\n", i);
flag = 0;
break;
}
}
// 最终还是没有发现矛盾,也没有输出唯一序,说明条件还是不够啊,顺序无法确定
if (flag) puts("Sorted sequence cannot be determined.");
}
return 0;
}
```
> **注**:此题没有给出$M$的数据范围,导致我调试了半个多小时,差评$yxc$

@ -1,74 +0,0 @@
#include <bits/stdc++.h>
// 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<i那么就是出现了矛盾
for (int i = 0; i < n; i++)
if (f[i][i]) return 2;
// 存在还没有识别出关系的两个点i,j,还要继续读入
for (int i = 0; i < n; i++)
for (int j = 0; j < i; j++)
if (!f[i][j] && !f[j][i]) return 3;
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 += f[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字符数组为字符串
return string(s, s + n);
}
int main() {
// n个变量,m个不等式
// 当输入一行 0 0 时,表示输入终止
while (scanf("%d %d", &n, &m), n && m) {
string S;
int k = 3; // 3:不能确定两两之间的关系
memset(f, 0, sizeof f); // 初始化邻接矩阵
// m条边
for (int i = 1; i <= m; i++) {
cin >> S;
// 已确定或者出现了矛盾,就没有必要再处理了,但是,还需要耐心的读取完毕,因为可能还有下一轮,不读入完耽误下一轮
if (k < 3) continue;
// 变量只可能为大写字母A~Z,映射到0~25
int a = S[0] - 'A', b = S[2] - 'A';
f[a][b] = 1; // 记录a<b
// 每输入一个关系,就计算一遍传递闭包
floyd();
// 检查一下现在的情况,是不是已经可以判定了
k = check();
if (k == 2) // 出现矛盾
printf("Inconsistency found after %d relations.\n", i);
else if (k == 1) { // 可以确定
string ans = getorder(); // 输出升序排列的所有变量
printf("Sorted sequence determined after %d relations: %s.\n", i, ans.c_str());
}
}
// 所有表达式都输入了,仍然定不下来关系
if (k == 3) printf("Sorted sequence cannot be determined.\n");
}
return 0;
}

@ -1,109 +0,0 @@
#include <bits/stdc++.h>
using namespace std;
const int N = 30, M = N * N;
int n, m;
int a[1050], b[1050]; // a[i]<b[i]
char s[N]; // 输入的偏序关系
int in[N], ind[N];
int d[N], dl;
// 链式前向星
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++;
}
/*
(1) n,
(2) n,2
(3) n,
*/
int topsort() {
queue<int> 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<C
a[i] = s[0] - 'A', b[i] = s[2] - 'A'; // 用两个数组a[],b[]记录关系,表示a[i]<b[i]
}
bool flag = 1; // 是不是已经找出了全部的大于关系序列
// 逐个讨论每个大小关系
for (int i = 1; i <= m; i++) {
dl = 0; // 拓扑序输出数组清零
add(a[i], b[i]); // 建图
ind[b[i]]++; // 记录b[i]入度
// 因为topsort会在过程中执行--ind[j]而此图和入度的值后面还要继续用不能让topsort改坏了
// 复制出来一个临时的入度数组in[]
memcpy(in, ind, sizeof ind);
// 每输入一个关系表达式就topsort一次
int res = topsort();
// 拓扑序唯一
if (res == 3) {
printf("Sorted sequence determined after %d relations: ", i);
for (int j = 1; j <= dl; j++) printf("%c", d[j] + 'A');
puts(".");
flag = 0;
break;
} else if (res == 1) { // 有环
printf("Inconsistency found after %d relations.\n", i);
flag = 0;
break;
}
}
// 最终还是没有发现矛盾,也没有输出唯一序,说明条件还是不够啊,顺序无法确定
if (flag) puts("Sorted sequence cannot be determined.");
}
return 0;
}

@ -1,79 +0,0 @@
#include <bits/stdc++.h>
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:ijk?
A:i == k
ni, j, kcontinue
Q2:DPFloydFloydDP
A:kdist[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的最短路径怎么走,当然,也是在<k的范围内的
// 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 (dist[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;
}

@ -1,131 +0,0 @@
## [$AcWing$ $344$. 观光之旅](https://www.acwing.com/problem/content/346/)
### 一、题目描述
给定一张无向图,求图中一个 **至少包含 $3$ 个点** 的环,环上的节点不重复,并且环上的边的长度之和最小。
该问题称为 **无向图的最小环问题**。
**你需要输出最小环的方案**,若最小环不唯一,输出任意一个均可。
**输入格式**
第一行包含两个整数 $N$ 和 $M$,表示无向图有 $N$ 个点,$M$ 条边。
接下来 $M$ 行,每行包含三个整数 $uvl$,表示点 $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 <bits/stdc++.h>
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的最短路径怎么走,当然,也是在<k
// 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 (dist[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;
}
```

@ -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)$
例图:
<center><img src='https://cdn.acwing.com/media/article/image/2020/06/25/30334_2388c086b6-%E6%97%A0%E6%A0%87%E9%A2%98.png'></center>
例图解释:
$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 <bits/stdc++.h>
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<int> 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;
}
```

@ -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
```
### 二、博弈论总结
<font size=4 color='red'><b>
必胜态 $\Rightarrow$ 选择合适方案 $\Rightarrow$ 必败态
必败态 $\Rightarrow$ 选择任何路线 $\Rightarrow$ 必胜态
</b></font>
### 三、简单情况
为什么会想到讨论简单情况呢?我们来思考一下:如果某一堆石子只有$1$个,随着我们执行拿走$1$个的操作,它的堆就没了,这样石子个数变了,堆数也变了,两个变量,问题变复杂了,我们上来就想难题,怕是搞不定。
既然这样,我们就思考一下 **子空间** :只考虑所有个数大于等于$2$的那些堆,其它可能存在石子数等于$1$的,等我们想明白这个简单问题再研究扩展的事,由易到难。
同时,我们需要思考博弈的胜负与什么因素相关呢?因为只有两种操作:**拿走一个石子、合并两堆**,很显然,**两个关键因素:石子个数、堆数**
同时,两个操作同一时间只能执行一个,所以可以理解为拿走一个石子对结果影响一下,合并两堆石子对结果也是影响一下,初步考虑应该堆个数与石子总数的加法关系相关。
**子空间:当每堆的石子个数都是大于等于$2$时**
<font size=5 color='red'><center><b>设$b$ = 堆数 + 石子总数 - $1$</b></center></font>
<font size=5 color='red'><center><b>结论:$b$是奇数⟺先手必胜,$b$是偶数⟺先手必败</b></center></font>
**证明:**
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$。
<center><img src='https://cdn.acwing.com/media/article/image/2021/05/15/61813_30ead721b5-image-20210515211400052.png'></center>
<font color='red' size=4><b>相关疑问</b></font>
$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$,为什么没有看到这个状态变化呢?**
答:<font color='red' size=4><b>这是因为聪明人不会从右侧某个石子数量大于$2$的堆中取走石子!</b></font>
看一下 **讨论简单情况** 中第$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 <bits/stdc++.h>
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;
}
```

@ -1,63 +0,0 @@
#include <bits/stdc++.h>
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;
}

@ -1,45 +0,0 @@
#include <bits/stdc++.h>
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;
}

@ -1,40 +0,0 @@
#include <bits/stdc++.h>
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;
}

@ -1,295 +0,0 @@
<!-- 让表格居中显示的风格 -->
<style>
.center
{
width: auto;
display: table;
margin-left: auto;
margin-right: auto;
}
</style>
##[$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
```
### 二、状态定义
**① 原来每堆数量是长成这个样的,可能是必胜状态,也可能是必败状态,都可以:**
<div class="center">
| $a_i$ | $a_{i+1}$ | ... | $a_{j-1}$ | $a_{j}$ |
| ---- | ---- |---- | ---- | ---- | ---- | ---- |
</div>
- 设 $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$)使得状态改为 **先手必败**
<div class="center">
| $left[i][j]$ | $a_i$ | $a_{i+1}$ | ... | $a_{j-1}$ | $a_{j}$ | $right[i][j]$ |
| ---- | ---- |---- | ---- | ---- | ---- | ---- |
</div>
即:$(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]$ 同理,下同):
<font color='red' size=4><b>反证法:</b></font>
假设不存在满足定义的 $left[i][j]$,则对于 **任意非负整数** $x$,有形如:
$$\large \underbrace{x,a_i,a_{i+1},\cdots,a_j}_{A(x)}$$ 都为**必胜局面**,记为 $A(x)$ 局面。
由于 $A(x)$ 为必胜局面,故从 $A(x)$ 局面 <font color='red' size=4><b>必然存在$M$种一步可达必败局面</b></font>
若从最左边一堆中拿,<font color='red' size=4><b>因为假设原因,不可能变成必败局面</b></font>,因为这样得到的局面仍形如 $A(x)$。
<font color='red' size=4><b>注意包括此行在内的接下来几行默认 $x \neq 0$</b></font>
左边拿没用,只能考虑从右边拿:
于是设 $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]$ 的唯一性证明
<font color='red' size=4><b>反证法:</b></font>
假设 $left(i,j)$ 不唯一,则存在非负整数 $x_1,x_2(x_1 \neq x_2)$,使得$(x_1,a_i,a_{i+1},⋯,a_{j1},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$。
<font color='red'><b>注:因$R=0$,表示在[$i$,$j-1$]确定后,右侧为$0$就能满足[$i$,$j-1$]这一段为先手必败,此时,左侧增加那堆个数为$0$就可以继续保持原来的先手必败,即$L=0$。</b></font>
#### 5、分类讨论
* $x=R$$Case$ $1$
最简单的情况——根据 $R=right[i][j-1]$ 的定义,区间 $[i,j]$ 本来就是必败局面,因此左边啥也不能添,添了反而错,故
$$\large left[i][j]=0$$
* $x<R$
* $x<L$,即 $x< \min\{L,R\}$$Case$ $2$
* **结论**
$$\large left[i][j]=x$$
* **证明**
**求证** $\large (x,a_i,a_{i+1},\cdots,a_{j-1},x)$为必败局面,其中$x< \min\{L,R\}$
由于最左边和最右边的两堆石子数量相同,后手可进行和先手 **对称** 操作,后手必将获得一个形如$(y,a_i,a_{i+1},⋯,a_{j1})$ 或 $(a_i,a_{i+1},\cdots,a_{j-1},y)$ 的局面,其中: $0<y < x<\min\{L,R\}$
**只有左侧为$L=left(i,j-1)$这个唯一值时,才是必败态,现在不是$L$,而是$y<min(L,R)$,所以后手必胜,即先手必败**, **证毕**。
<br>
* $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 <R$
* 若先手拿最左边一堆,设拿了以后 **还剩 $z$ 个石子**
* 若 $z>L$,则后手将最右堆拿成 $z-1$ 个石子($z-1 \ge L>0$**保证左侧比右侧多$1$个石子**,就能回到 $Case$ $3$ 本身,递归证明即可
* 若 $z=L$,则后手将最右堆拿完,根据 $L[i][j-1]$ 定义知此时局面必败
* 若 $0<z<L$,则后手将最右堆拿成 $z$ 个石子,**由 $Case$ $2$ 知此时是必败局面**
* 若 $z=0$,此时最右堆石子数 $x$ 满足 $L \le x<R$,结合 $right[i][j-1]$ 定义知 **此局面必胜**
<br>
* 若先手拿最右边一堆,设拿了以后 **还剩 $z$ 个石子**
* 若 $z \ge L$,则后手将最左堆拿成 $z+1$个石子,就能回到 $Case$ $3$ 本身,递归证明即可
* 若 $0<z<L$,则后手将最左堆拿成 $z$ 个石子,**由 $Case$ $2$ 知此时是必败局面**
* 若 $z=0$,则后手将最左堆拿成 $L$ 个石子,由 $left[i][j-1]$定义知此时局面必败
* $x>R$
* $x≤L$,即 $R < x \leq L$$Case$ $4$
* 结论:$$\large left[i][j]=x-1$$
* **证明**
* 若先手拿最左边一堆,设拿了以后还剩 $z$ 个石子。
* 若 $z \geq R$,则后手将最右堆拿成 $z+1$ 个石子,<font color='red' size=4><b>保证左侧比右侧多$1$个石子</b></font>,就能回到 $Case$ $4$ 本身,递归证明即可。
* 若 $0<z<R$ $z$ $Case$ $2$
* 若 $z=0$,则后手将最右堆拿成 $R$ 个石子(注意 $Case$ $4$ 保证了此时最右堆石子个数 $>R$),由 $right[i][j-1])$ 的定义知此时是必败局面。
<br>
* 若先手拿最右边一堆,设拿了以后还剩 $z$ 个石子。
* 若 $z>R$,则后手将最左边一堆拿成 $z-1$ 个石子(注意 $z-1 \ge R >0$),递归证明即可。<font color='red' size=4><b>保证右侧比左侧多$1$个石子。</b></font>
* 若 $z=R$,则后手把最左堆拿完,根据 $right[i][j-1]$的定义可知得到了必败局面。
* 若 $0<z<R$ $z$ $Case$ $2$
* 若 $z=0$,此时最左堆石子数量 $k$ 满足 $0<k<L$ $left[i][j-1]$
<br>
* $x>L$,即 $x>\max\{L,R\}$$Case$ $5$
* **结论**$$\large left[i][j]=x$$
* **证明**
设先手将其中一堆拿成了 $z$ 个石子。
* 若 $z>\max\{L,R\}$,后手将另一堆也拿成$z$个,回到 $Case$ $5$,递归证明。
* 若 $0<z<\min\{L,R\}$,后手把另一堆也拿成 $z$ 个石子即可转 $Case$ $2$。
* 若 $z=0$,将另一堆拿成 $L$ 或 $R$ 个石子即可得到必败局面。
* 剩余的情况是 $L \le z \le R$或 $R \le z \le L$。
$Case$ $3$ 可以解决最左堆 $L +1 \le z \le R$,最右堆 $L \le z \le R-1$ 的情况
$Case$ $4$ 可以解决最左堆 $R \le z \le L-1$,最右堆 $R+1 \le z \le L$的情
况。
​所以只需解决最左堆 $z=L$ 和最右堆 $z=R$ 的情况。而这两种情况直接把另一堆拿完就可以得到必败局面。
**综上所述:**
$$ \large
L[i][j]=
\large \left\{\begin{matrix}
0 & x=R \\
x+1&L \leq x < R \\
x-1 & R<x \leq L \\
x & otherwise
\end{matrix}\right.
$$
<font color='blue' size=4><b>温馨提示:</b></font>**请看清楚 $L$ 取不取等,乱取等是错的!**
同理可求 $R(i,j)$。
回到原题,**先手必败当且仅当** $L[2][n]=a_1$ ,于是我们就做完啦!
时间复杂度 $O(n^2)$。
### 六、实现代码
```cpp {.line-numbers}
#include <cstdio>
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;
}
```

@ -1,27 +0,0 @@
#include <bits/stdc++.h>
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;
}

@ -1 +0,0 @@
<mxfile host="Electron" modified="2022-06-20T07:53:04.288Z" agent="5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/16.0.2 Chrome/96.0.4664.55 Electron/16.0.5 Safari/537.36" etag="jNveJB-z9TWn2FKd464S" version="16.0.2" type="device"><diagram id="S7yhPZ9SSO8EGXcPaCVl" name="第 1 页">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=</diagram></mxfile>

@ -1,206 +0,0 @@
##[$AcWing$ $217$. 绿豆蛙的归宿](https://www.acwing.com/problem/content/description/219/)
### 一、题目描述
给出一个有向无环的连通图,起点为 $1$ ,终点为 $N$,每条边都有一个长度。
数据保证从起点出发能够到达图中所有的点,图中所有的点也都能够到达终点。
绿豆蛙从起点出发,走向终点。
到达每一个顶点时,如果有 $K$ 条离开该点的道路,绿豆蛙可以选择任意一条道路离开该点,并且走向每条路的概率为 $1/K$。
现在绿豆蛙想知道,从起点走到终点所经过的路径总长度的 **期望** 是多少?
**输入格式**
第一行: 两个整数 $NM$,代表图中有 $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:$什么意思呢?
如图:
<center><img src='https://img-blog.csdnimg.cn/c8715153345a4f48b17bca9db4a18c45.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzUxOTY4MTU1,size_16,color_FFFFFF,t_70'></center>
我们计算从$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$
<font color='red'><h3>总结:概率是叠乘的</h3></font>
本题有 **正推** 和 **倒推** 两种写法:
### 二、正推法
<center><img src='https://img-blog.csdnimg.cn/ba5182288859446386d641317c691cda.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzUxOTY4MTU1,size_16,color_FFFFFF,t_70'></center>
设:
- $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 <bits/stdc++.h>
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<int> 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;
}
```
### 三、倒推法
现在学会了正推,来看看 **逆推**,即 **从终点找到起点**
<center><img src='https://cdn.acwing.com/media/article/image/2022/06/20/64630_41a7f81ff0-217.drawio.png'></center>
设 $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 <bits/stdc++.h>
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<int> 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;
}
```

@ -1,44 +0,0 @@
#include <bits/stdc++.h>
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<int> 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;
}

@ -1,59 +0,0 @@
#include <bits/stdc++.h>
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<int> 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;
}

@ -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
```
### 二、题意分析
<font color='red' size=4><b>$Q$:为什么从终止状态向起始状态递推?</b></font>
**答**:满足条件的终止状态较多,而起始状态唯一。考虑以终止状态为初值,起始状态为目标,进行动态规划。
#### 状态表示
$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{13a}{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 <bits/stdc++.h>
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$
<center><img src='https://img-blog.csdnimg.cn/4479c699431c421da4ab5d4f3f3db1bc.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzUxOTY4MTU1,size_16,color_FFFFFF,t_70'></center>

@ -1,70 +0,0 @@
#include <bits/stdc++.h>
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,48 +0,0 @@
#include <bits/stdc++.h>
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;
}

@ -1,89 +0,0 @@
#include <bits/stdc++.h>
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<Node, vector<Node>, 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;
}

@ -1,102 +0,0 @@
#include <queue>
#include <cstring>
#include <iostream>
#include <algorithm>
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<Node> q; //通过定义结构体小于号,实现小顶堆
memset(dist, 0x3f, sizeof(dist)); //清空最小距离与次小距离数组
memset(cnt, 0, sizeof(cnt)); //清空最小距离路线个数与次小距离路线个数数组
memset(st, 0, sizeof(st)); //清空是否出队过数组
cnt[s][0] = 1; //起点s0:最短路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;
}

@ -1,15 +1,18 @@
#include <bits/stdc++.h>
using namespace std;
typedef pair<int, int> PII;
#define x first
#define y second
typedef pair<int, int> 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一下

@ -138,16 +138,19 @@ maxd[i] + maxd[j] + get(q[i], q[j])
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
typedef pair<int, int> PII;
#define x first
#define y second
typedef pair<int, int> 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++) // ii()
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一下

@ -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<B
A<C
B<C
C<D
B<D
A<B
3 2
A<B
B<A
26 1
A<Z
0 0
```
**输出样例$1$**
```cpp {.line-numbers}
Sorted sequence determined after 4 relations: ABCD.
Inconsistency found after 2 relations.
Sorted sequence cannot be determined.
```
**输入样例$2$**
```cpp {.line-numbers}
6 6
A<F
B<D
C<E
F<D
D<E
E<F
0 0
```
**输出样例$2$**
```cpp {.line-numbers}
Inconsistency found after 6 relations.
```
**输入样例$3$**
```cpp {.line-numbers}
5 5
A<B
B<C
C<D
D<E
E<A
0 0
```
**输出样例$3$**
```cpp {.line-numbers}
Sorted sequence determined after 4 relations: ABCDE.
```
### 二、$floyd$ 求传递闭包
**概念**
给定若干对元素和若干对二元关系,并且关系具有传递性,通过传递性推导出尽量多的元素之间关系的问题被称为 **传递闭包**。
>**解释**:比如$a < b,b < c$,$a < c$$a$$b$ <font color='red' size=4><b>【小的向大的连一条边】</b></font>$b$到$c$有一条有向边,可以推出$a$可以到达$c$,找出图中各点能够到达点的集合,**类似** 于$floyd$算法求图中任意两点间的最短距离 <font color='red' size=4><b>【魔改版的$floyd$】</b></font>
**模板**
```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
for (int i = 0; i < n; i++)
if (f[i][i]) return 2;
// 存在还没有识别出关系的两个点i,j,还要继续读入
for (int i = 0; i < n; i++)
for (int j = 0; j < i; j++)
if (!f[i][j] && !f[j][i]) return 3;
return 1;
}
```
* ① 所有的关系都确定,而且没有发生矛盾
* ② $f[i][i] = 1$ 发生矛盾
* ③ $f[i][j] = f[j][i] = 0$ 表示$i$与$j$之间的大小关系还没有确定下来,需要继续读取下一对二元关系
```cpp {.line-numbers}
string getorder(){
char s[26];
for(int i = 0;i < n;i++){
int cnt = 0;
for(int j = 0;j < n;j++) cnt += f[i][j];//i
s[n - cnt - 1] = i + 'A'; //反着才能记录下名次
}
return string(s,s + n); //用char数组构造出string返回
}
```
> **解释**:确定所有元素次序后如何判断元素`i`在第几个位置呢?`f[i][j] = 1`表示`i < j`,因此计算下`i`小于元素的个数`cnt`,就可以判定`i`是第`cnt + 1`大的元素了
#### $Code$ $O(N^3)$
```cpp {.line-numbers}
#include <bits/stdc++.h>
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<a$,,$b<y$,$x<y$
![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/202401031410473.png)
```cpp {.line-numbers}
#include <bits/stdc++.h>
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<b,a,b
if (!type) {
g[a][b] = 1;
for (int x = 0; x < n; x++) {
if (g[x][a]) g[x][b] = 1; // 所有比a小的x,一定比b小
if (g[b][x]) g[a][x] = 1; // 所有比b大的x,一定比a大
for (int y = 0; y < n; y++)
if (g[x][a] && g[b][y])
g[x][y] = 1;
}
type = check();
if (type) 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;
}
```

@ -0,0 +1,77 @@
#include <bits/stdc++.h>
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;
}

@ -0,0 +1,75 @@
#include <bits/stdc++.h>
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<b,那么不需要完全的重新计算完整的传递闭包只需要把与a,b相关的变更进行记录大小关系即可
if (!type) {
g[a][b] = 1;
for (int x = 0; x < n; x++) {
if (g[x][a]) g[x][b] = 1; // 所有比a小的x,一定比b小
if (g[b][x]) g[a][x] = 1; // 所有比b大的x,一定比a大
for (int y = 0; y < n; y++)
if (g[x][a] && g[b][y])
g[x][y] = 1;
}
type = check();
if (type) 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;
}

@ -0,0 +1,67 @@
#include <bits/stdc++.h>
using namespace std;
const int N = 110, INF = 0x3f3f3f3f;
int n, m;
int g[N][N], dis[N][N];
vector<int> 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;
}

@ -0,0 +1,152 @@
## [$AcWing$ $344$. 观光之旅](https://www.acwing.com/problem/content/346/)
### 一、题目描述
给定一张无向图,求图中一个至少包含 $3$ 个点的环,环上的节点不重复,并且环上的边的长度之和最小。
该问题称为 **无向图的最小环问题**。
**你需要输出最小环的方案**,若最小环不唯一,输出任意一个均可。
**输入格式**
第一行包含两个整数 $N$ 和 $M$,表示无向图有 $N$ 个点,$M$ 条边。
接下来 $M$ 行,每行包含三个整数 $uvl$,表示点 $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 k1$ 的最短距离。因此第 $k$ 类最小值可在此时得到。
#### 状态表示
![](https://cdn.acwing.com/media/article/image/2021/08/08/52559_1791f992f8-5.png)
#### 求方案
$DP$ 求方案一般要 **记录转移前驱的所有维**。但 $floyd$ 转移方程中的 $k$ 表示路径的中间点,由于路径可以被两端和中间点覆盖,只要记下中间点,就能递归出路径。
### 三、$floyd+dp+$递归输出路径
```cpp {.line-numbers}
#include <cstring>
#include <iostream>
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$

@ -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];
```

@ -6,7 +6,7 @@ typedef pair<int, int> 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<PII> q; // bfs用的队列
memset(dis, 0x3f, sizeof dis); // 初始化距离
queue<PII> 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
}
}
}

@ -77,12 +77,12 @@ $1≤k≤150$
### 二、解题思路
试想下如果本题 **没有钥匙和门** 的条件,只要求从 **左上角** 走到 **右下角** 的最小步数,就是简单的迷宫问题了,可以使用$BFS$解决。
试想下如果本题 **没有钥匙和门** 的条件,只要求从 **左上角** 走到 **右下角** 的最小步数,就是简单的迷宫问题了,可以使用$bfs$解决。
#### 状态表示
加上钥匙和门的的条件,便是**类似于八数码问题**了。实际上$BFS$解决的最短路问题都可以看作**求从初始状态到结束状态需要的最小转移次数**:
加上钥匙和门的的条件,便是**类似于八数码问题**了。实际上$bfs$解决的最短路问题都可以看作 **求从初始状态到结束状态需要的最小转移次数**:
普通迷宫问题的 **状态** 就是 **当前所在的坐标**,八数码问题的 **状态** 就是**当前棋盘的局面**。
普通迷宫问题的 **状态** 就是 **当前所在的坐标**,八数码问题的 **状态** 就是 **当前棋盘的局面**。
本题在迷宫问题上加上了 **钥匙和门** 的条件,显然,处在同一个坐标下,**持有钥匙和不持有钥匙就不是同一个状态了**,为了能够清楚的表示每个状态,除了当前坐标外还需要加上当前获得的钥匙信息,即$f[x][y][st]$表示当前处在$(xy)$位置下持有钥匙状态为$st$,将二维坐标压缩成一维就得到$f[z][st]$这样的状态表示了,或者说,$z$是格子的编号,从上到下,从左而右的编号依次为$1$到$n*m$$st$为$0110$时,表示持有第$1,2$类钥匙,这里注意我在 <font color='red'><b>表示状态时抛弃了最右边的一位</b></font>,因为钥匙编号从$1$开始,我想确定是否持有第$i$类钥匙时,只需要判断`st >> i & 1`是不是等于$1$即可。
@ -108,7 +108,7 @@ typedef pair<int, int> 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<PII> q; // bfs用的队列
memset(dis, 0x3f, sizeof dis); // 初始化距离
queue<PII> 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;
}
```

@ -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<int> 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<PII, vector<PII>, greater<PII>> pq;
pq.push({0, 1});
priority_queue<PII, vector<PII>, greater<PII>> 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<Node> q; //通过定义结构体小于号,实现小顶堆
memset(dist, 0x3f, sizeof(dist)); //清空最小距离与次小距离数组
memset(cnt, 0, sizeof(cnt)); //清空最小距离路线个数与次小距离路线个数数组
memset(st, 0, sizeof(st)); //清空是否出队过数组
priority_queue<Node> q; // 默认是大顶堆,通过定义结构体小于号,实现小顶堆。比如认证的d值更大谁就更小
memset(dis, 0x3f, sizeof dis); // 清空最小距离与次小距离数组
memset(cnt, 0, sizeof cnt); // 清空最小距离路线个数与次小距离路线个数数组
memset(st, 0, sizeof st); // 清空是否出队过数组
cnt[s][0] = 1; //起点s0:最短路1:有一条
cnt[s][1] = 0; //次短路路线数为0
cnt[s][0] = 1; // 起点s0:最短路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;
}
```

@ -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<int> 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;
}
}

@ -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<PII, vector<PII>, greater<PII>> pq;
pq.push({0, 1});
priority_queue<PII, vector<PII>, greater<PII>> 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;
}
}

@ -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<PII, vector<PII>, greater<PII>> 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<PII, vector<PII>, greater<PII>> 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<PII, vector<PII>, greater<PII>> 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);
}

@ -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<PII, vector<PII>, greater<PII>> pq; // 小顶堆
pq.push({0, 0}); // 出发点入队列
d[0] = 0; // 出发点距离0
memset(dis, 0x3f, sizeof dis); // 初始化大
memset(st, 0, sizeof st); // 初始化为未出队列过
priority_queue<PII, vector<PII>, greater<PII>> 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();
}

@ -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<PII, vector<PII>, greater<PII>> 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<PII, vector<PII>, greater<PII>> 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);
}

@ -0,0 +1,97 @@
#include <bits/stdc++.h>
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<Node> q; // 默认是大顶堆通过定义结构体小于号实现小顶堆。比如认证的d值更大谁就更小
memset(dis, 0x3f, sizeof dis); // 清空最小距离与次小距离数组
memset(cnt, 0, sizeof cnt); // 清空最小距离路线个数与次小距离路线个数数组
memset(st, 0, sizeof st); // 清空是否出队过数组
cnt[s][0] = 1; // 起点s0:最短路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;
}

@ -119,13 +119,15 @@ $dist[S][0]$=$0$$cnt[S][0]$=$1$
```cpp {.line-numbers}
#include <bits/stdc++.h>
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<Node, vector<Node>, 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<Node> q; // 默认是大顶堆通过定义结构体小于号实现小顶堆。比如认证的d值更大谁就更小
memset(dis, 0x3f, sizeof dis); // 清空最小距离与次小距离数组
memset(cnt, 0, sizeof cnt); // 清空最小距离路线个数与次小距离路线个数数组
memset(st, 0, sizeof st); // 清空是否出队过数组
cnt[s][0] = 1; // 起点s0:最短路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;
}
```

@ -0,0 +1,108 @@
#include <queue>
#include <cstring>
#include <iostream>
#include <algorithm>
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<Node> q; // 默认是大顶堆通过定义结构体小于号实现小顶堆。比如认证的d值更大谁就更小
memset(dis, 0x3f, sizeof dis); // 清空最小距离与次小距离数组
memset(cnt, 0, sizeof cnt); // 清空最小距离路线个数与次小距离路线个数数组
memset(st, 0, sizeof st); // 清空是否出队过数组
cnt[s][0] = 1; // 起点s0:最短路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;
}

@ -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

@ -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++
Sdist[S][]
Sdis[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<PII, vector<PII>, greater<PII>> 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);

@ -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<PII, vector<PII>, greater<PII>> 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);

@ -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<PII, vector<PII>, greater<PII>> 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;

@ -75,7 +75,7 @@ $0≤K<N≤1000,1≤P≤10000,1≤L_i≤1000000$
噢,原来需要 **二分答案**
### 三、$Code$
#### $Code$
```cpp {.line-numbers}
#include <bits/stdc++.h>
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<PII, vector<PII>, greater<PII>> 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;

@ -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<PII, vector<PII>, greater<PII>> 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<PII> 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;

@ -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$值是记录 **点权的最值** ,在某些情况下是 **具有后效性**的,如下图:
<center><img src='https://img-blog.csdnimg.cn/f553c1000eb04000a4fe36692462d41e.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAI-i_meaYr-S4gOadoeazqOmHiuOAgg==,size_20,color_FFFFFF,t_70,g_se,x_16'></center>
**点权** 用绿色数字标示在点号下方,可以发现在点$2$处会经过一个回路再次回到点$2$,但在这之前点$5$的$dist$已经被更新为$3$了,之后回到点$2$,由于$st[2] == true$直接$continue$,虽然此时$dist[2] == 1$但却无法把$1$传递给点$5$了。
**点权** 用绿色数字标示在点号下方,可以发现在点$2$处会经过一个回路再次回到点$2$,但在这之前点$5$的$dis$已经被更新为$3$了
><font color='red' size=3><b>解释:因为$1 \rightarrow 2 \rightarrow 5$这条路线上,在点$2$时,水晶球的价格最便宜,价格是$3$</b></font>
**解决方法**:$dijkstra$算法中去掉$st$的限制,让整个算法不断迭代,直到无法更新导致队空退出循环。
之后回到点$2$,由于$st[2] == true$直接$continue$,虽然此时$dis[2] == 1$但却无法把$1$传递给点$5$了。
<font color='blue' size=4><b>采用办法</b></font>
在$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<PII, vector<PII>, greater<PII>> 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<PII> 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;

@ -20,7 +20,7 @@ int id[N]; // 节点在哪个连通块中
vector<int> block[N]; // 连通块包含哪些节点
int bcnt; // 连通块序号计数器
int dist[N]; // 最短距离(结果数组)
int dis[N]; // 最短距离(结果数组)
int in[N]; // 每个DAG(节点即连通块)的入度
bool st[N]; // dijkstra用的是不是在队列中的数组
queue<int> 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<PII, vector<PII>, greater<PII>> pq;
/*
distinf,
disinf,
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;
}

@ -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 @@
#### 算法步骤
<center><img src='https://cdn.acwing.com/media/article/image/2021/07/12/61813_53a888d8e2-image-20210323144423779.png'></center>
扩展用拓扑排序解决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<int> block[N]; // 连通块包含哪些节点
int bcnt; // 连通块序号计数器
int dist[N]; // 最短距离(结果数组)
int dis[N]; // 最短距离(结果数组)
int in[N]; // 每个DAG(节点即连通块)的入度
bool st[N]; // dijkstra用的是不是在队列中的数组
queue<int> 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<PII, vector<PII>, greater<PII>> 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;
}

@ -0,0 +1,50 @@
#include <bits/stdc++.h>
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;
}

@ -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;
}

@ -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++)

@ -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;
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save