'commit'
This commit is contained in:
83
TangDou/CSP-J/AcWingTraining/T8/1164.cpp
Normal file
83
TangDou/CSP-J/AcWingTraining/T8/1164.cpp
Normal file
@@ -0,0 +1,83 @@
|
||||
#include <bits/stdc++.h>
|
||||
|
||||
using namespace std;
|
||||
|
||||
typedef pair<int, int> PII;
|
||||
#define x first // 节点号
|
||||
#define y second // 奇偶性
|
||||
|
||||
const int N = 100010, M = 200010;
|
||||
|
||||
int n, m, Q;
|
||||
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 dist[N][2]; // 奇偶最短路 dist[5][0]:4,dist[6][1]:3
|
||||
queue<PII> q;
|
||||
|
||||
// ① 因边边权都是1,不需要Dijkstra或者Floyd,只需要bfs即可
|
||||
// ② 由于本题是强调 奇偶性 ,所以,需要使用 拆点 的思想
|
||||
// ③ 所谓拆点,就是像状态机或者DP中扩展维度的思想,将奇偶点分开,把混沌的事情清晰化。
|
||||
// dist[i][0]:表示是偶数步的点,值是最小步数
|
||||
// dist[i][1]:表示是奇数步的点
|
||||
void bfs() {
|
||||
memset(dist, 0x3f, sizeof dist); // 预求最短,先设最大
|
||||
dist[1][0] = 0; // 1是起点,0:理解为从1号出发,到1号结束,走了0步的结果,所以是偶数步,步数为0
|
||||
q.push({1, 0}); // 起点入队列
|
||||
|
||||
while (q.size()) {
|
||||
auto u = q.front();
|
||||
q.pop();
|
||||
|
||||
for (int i = h[u.x]; ~i; i = ne[i]) { // u点
|
||||
PII v = {e[i], u.y ^ 1}; // 0 ^ 1 = 1 1 ^ 1 =0
|
||||
if (dist[v.x][v.y] > dist[u.x][u.y] + 1) { // 如果利用u可以更新v这个点
|
||||
dist[v.x][v.y] = dist[u.x][u.y] + 1; // 更新
|
||||
q.push(v); // v的最短距离更新了,入队列,去更新更多的点
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
// 加快读入
|
||||
ios::sync_with_stdio(false), cin.tie(0);
|
||||
memset(h, -1, sizeof h);
|
||||
cin >> n >> m >> Q;
|
||||
while (m--) {
|
||||
int a, b;
|
||||
cin >> a >> b;
|
||||
add(a, b), add(b, a);
|
||||
}
|
||||
|
||||
bfs();
|
||||
|
||||
while (Q--) {
|
||||
int a, l;
|
||||
cin >> a >> l;
|
||||
/*
|
||||
如果不写下面的特判,会被欺负死:
|
||||
3 1 1
|
||||
2 3
|
||||
1 2
|
||||
|
||||
第一行三个正整数 n,m 和 q,分别表示工人的数目、传送带的数目和工单的数目。
|
||||
也就是3个工人,1个传送带,1个工单
|
||||
传送带是2~3,也就是1是与世隔绝的
|
||||
|
||||
接下来 q 行,每行两个正整数 a 和 L,表示编号为 a 的工人想生产一个第 L 阶段的零件。
|
||||
1号工人,想生产2阶段的零件
|
||||
|
||||
由于1号工人是与世隔绝的,没有人给它提供1阶段的零件,肯定返回No,此时需要特判,返回答案
|
||||
*/
|
||||
if (h[1] == -1)
|
||||
puts("No");
|
||||
else if (l >= dist[a][l & 1]) // l是奇数,并且大于奇数最短路,则可以通过+2,+4,+6...等方法来回磨,凑够l就完事
|
||||
// l是偶数,并且大于偶数最短路,则可以通过+2,+4,+6...等方法来回磨,凑够l就完事
|
||||
puts("Yes");
|
||||
else
|
||||
puts("No");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
282
TangDou/CSP-J/AcWingTraining/T8/1164.md
Normal file
282
TangDou/CSP-J/AcWingTraining/T8/1164.md
Normal file
@@ -0,0 +1,282 @@
|
||||
## [$AcWing$ $1164$. 加工零件](https://www.acwing.com/problem/content/description/1166/)
|
||||
|
||||
### 一、题目描述
|
||||
凯凯的工厂正在有条不紊地生产一种神奇的零件,神奇的零件的生产过程自然也很神奇。
|
||||
|
||||
工厂里有 $n$ 位工人,工人们从 $1∼n$ 编号。
|
||||
|
||||
某些工人之间存在 **双向** 的零件传送带。
|
||||
|
||||
保证每两名工人之间最多只存在一条传送带。
|
||||
|
||||
如果 $x$ 号工人想生产一个被加工到第 $L(L>1)$ 阶段的零件,则所有与 $x$ 号工人有传送带直接相连的工人,都需要生产一个被加工到第 $L−1$ 阶段的零件(但 $x$ 号工人自己无需生产第 $L−1$ 阶段的零件)。
|
||||
|
||||
如果 $x$ 号工人想生产一个被加工到第 $1$ 阶段的零件,则所有与 $x$ 号工人有传送带直接相连的工人,都需要为 $x$ 号工人提供一个原材料。
|
||||
|
||||
轩轩是 $1$ 号工人。
|
||||
|
||||
现在给出 $q$ 张工单,第 $i$ 张工单表示编号为 $a_i$ 的工人想生产一个第 $L_i$ 阶段的零件。
|
||||
|
||||
轩轩想知道 **对于每张工单,他是否需要给别人提供原材料**。
|
||||
|
||||
他知道聪明的你一定可以帮他计算出来!
|
||||
|
||||
**输入格式**
|
||||
第一行三个正整数 $n$,$m$ 和 $q$,分别表示工人的数目、传送带的数目和工单的数目。
|
||||
|
||||
接下来 $m$ 行,每行两个正整数 $u$ 和 $v$,表示编号为 $u$ 和 $v$ 的工人之间存在一条零件传输带。保证 $u≠v$。
|
||||
|
||||
接下来 $q$ 行,每行两个正整数 $a$ 和 $L$,表示编号为 $a$ 的工人想生产一个第 $L$ 阶段的零件。
|
||||
|
||||
**输出格式**
|
||||
共 $q$ 行,每行一个字符串 “$Yes$” 或者 “$No$”。如果按照第 $i$ 张工单生产,需要编号为 $1$ 的轩轩提供原材料,则在第 $i$ 行输出 “$Yes$”;否则在第 $i$ 行输出 “$No$”。注意输出不含引号。
|
||||
|
||||
**数据范围**
|
||||
$1≤u,v,a≤n$。
|
||||
|
||||
测试点 $1∼4$,$1≤n,m≤1000$,$q=3$,$L=1$。
|
||||
测试点 $5∼8$,$1≤n,m≤1000$,$q=3$,$1≤L≤10$。
|
||||
测试点 $9∼12,1≤n,m,L≤1000,1≤q≤100$。
|
||||
测试点 $13∼16,1≤n,m,L≤1000,1≤q≤105$。
|
||||
测试点 $17∼20,1≤n,m,q≤105,1≤L≤109$。
|
||||
|
||||
**输入样例1**:
|
||||
```cpp {.line-numbers}
|
||||
3 2 6
|
||||
1 2
|
||||
2 3
|
||||
1 1
|
||||
2 1
|
||||
3 1
|
||||
1 2
|
||||
2 2
|
||||
3 2
|
||||
```
|
||||
|
||||
**输出样例1**:
|
||||
```cpp {.line-numbers}
|
||||
No
|
||||
Yes
|
||||
No
|
||||
Yes
|
||||
No
|
||||
Yes
|
||||
```
|
||||
|
||||
**输入样例2**:
|
||||
```cpp {.line-numbers}
|
||||
5 5 5
|
||||
1 2
|
||||
2 3
|
||||
3 4
|
||||
4 5
|
||||
1 5
|
||||
1 1
|
||||
1 2
|
||||
1 3
|
||||
1 4
|
||||
1 5
|
||||
```
|
||||
|
||||
**输出样例2**:
|
||||
```cpp {.line-numbers}
|
||||
No
|
||||
Yes
|
||||
No
|
||||
Yes
|
||||
Yes
|
||||
```
|
||||
|
||||
|
||||
### 二、抽象题意
|
||||
给定一张包含 $n$ 个点和 $m$ 条边的 **无向图**,再给定 $q$ 个询问:$a_i,L_i$,判断是否存在一条从$1$号点走到$a_i$号点的 **恰好** 经过$L$条边的路径。
|
||||
|
||||
<center><img src='https://cdn.luogu.com.cn/upload/image_hosting/tfom5z2s.png'></center>
|
||||
|
||||
设求第$3$点为第$3$阶段时,点$1$是否需要提供原材料。
|
||||
|
||||
$【3,3】 => 【2,2】,【4,2】$
|
||||
|
||||
$【2,2】 => 【1,1】,【3,1】$
|
||||
$【4,2】 => 【3,1】,【5,1】$
|
||||
|
||||
$【1,1】 => 【2,0】,【5,0】$
|
||||
$【3,1】 => 【2,0】,【4,0】$
|
||||
$【5,1】 => 【1,0】,【4,0】$ # 此处$1$需要提供原材料
|
||||
|
||||
比较容易想到对于每个询问进行暴搜,若点$1$为$0$,则$Yes$, 时间复杂度很高,必然超时。
|
||||
|
||||
```cpp {.line-numbers}
|
||||
#include <bits/stdc++.h>
|
||||
|
||||
using namespace std;
|
||||
|
||||
typedef pair<int, int> PII;
|
||||
#define x first
|
||||
#define y second
|
||||
|
||||
const int N = 1e5 + 10, M = N << 1;
|
||||
|
||||
// 20个测试点,过了7个,得了35分,其它TLE
|
||||
// 邻接表
|
||||
int e[M], h[N], idx, ne[M];
|
||||
void add(int a, int b) {
|
||||
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
|
||||
}
|
||||
|
||||
bool bfs(int start, int dist) {
|
||||
queue<PII> q;
|
||||
q.push({start, dist});
|
||||
|
||||
while (q.size()) {
|
||||
auto u = q.front();
|
||||
q.pop();
|
||||
|
||||
if (u.x == 1 && u.y == 0) return true; // 在用完所有步数后,到达1号点,成功!
|
||||
|
||||
for (int i = h[u.x]; ~i; i = ne[i]) {
|
||||
int v = e[i];
|
||||
if (u.y) q.push({v, u.y - 1});
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int main() {
|
||||
memset(h, -1, sizeof h);
|
||||
|
||||
int n, m, Q;
|
||||
cin >> n >> m >> Q;
|
||||
|
||||
while (m--) {
|
||||
int a, b;
|
||||
cin >> a >> b;
|
||||
add(a, b), add(b, a); // 无向图,双向建边
|
||||
}
|
||||
|
||||
while (Q--) {
|
||||
int u, dist;
|
||||
cin >> u >> dist;
|
||||
cout << (bfs(u, dist) ? "Yes" : "No") << endl;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
### 四、优化算法
|
||||
(最短路,$BFS$) $O(n+m+q)$
|
||||
|
||||
**本题关键点**:
|
||||
> 如果存在一条长度是 $L$ 的路径,其中 $L>0$,那么我们可以在其中任意一条边上来回走,就可以构造出来长度是 $L+2,L+4,L+6,…$的路径。
|
||||
|
||||
|
||||
因此当我们想判断是否存在长度是 $L$ 的路径时,只需判断是否 **存在长度小于等于 $L$**,且 **奇偶性** 和 $L$ 相同的路径即可。
|
||||
|
||||
因此我们可以预处理出从$1$号点出发,到每个点长度为 **奇数的最短路径** 和 **长度为偶数的最短路径**。
|
||||
|
||||
这里可以使用拆点技巧来构造新图,类似于$DP$中的[状态机模型](https://www.acwing.com/problem/content/1051/):
|
||||
|
||||
* 将原图中的每个点 $v$ 拆成两个新点:偶点$v_0$和奇点$v_1$;
|
||||
* 将原图中的每条边 $(u,v)$ 拆成两条新边:$(u_0,v_1)$ 和 $(u_1,v_0)$;
|
||||
|
||||
那么在新图中从 $1$ 号点走到 $v_0$ 的所有路径,对应在原图中从 $1$ 号点走到 $v$ 的所有 **长度是偶数** 的路径;在新图中从 $1$ 号点走到 $v_1$ 的所有路径,对应在原图中从 $1$ 号点走到 $v$ 的所有 **长度是奇数** 的路径。
|
||||
|
||||
因此在新图上求出$1$号点到其他所有点的最短路径,即可求出在原图中从$1$号点到其他所有点的长度是奇数和偶数的最短路径。
|
||||
|
||||
由于所有边的长度为$1$,因此可以用$BFS$求最短路。
|
||||
|
||||
以上我们处理了 $L_i>0$ 的情况,还需特判一下 $L_i==0$ 的情况,$L_i==0$ 表示一条边都不存在,即$1$号点与其他点均不连通,此时由于输入时 $L_i≥1$,因此直接输出 $No$即可。
|
||||
|
||||
#### 时间复杂度
|
||||
|
||||
使用$BFS$求最短路的时间复杂度是 $(n+m)$;
|
||||
判断每个查询操作的时间复杂度是 $O(1)$;
|
||||
|
||||
因此总时间复杂度是 $O(n+m+q)$。
|
||||
|
||||
```cpp {.line-numbers}
|
||||
#include <bits/stdc++.h>
|
||||
|
||||
using namespace std;
|
||||
|
||||
typedef pair<int, int> PII;
|
||||
#define x first // 节点号
|
||||
#define y second // 奇偶性
|
||||
|
||||
const int N = 100010, M = 200010;
|
||||
|
||||
int n, m, Q;
|
||||
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 dist[N][2]; // 奇偶最短路
|
||||
queue<PII> q;
|
||||
|
||||
// 因边边权都是1,不需要Dijkstra或者Floyd,只需要bfs即可
|
||||
// 由于本题是强调 奇偶性 ,所以,需要使用 拆点 的思想
|
||||
// 所谓拆点,就是像状态机或者DP中扩展维度的思想,将奇偶点分开,把混沌的事情清晰化。
|
||||
// dist[i][0]:表示是偶数步的点,值是最小步数
|
||||
// dist[i][1]:表示是奇数步的点
|
||||
void bfs() {
|
||||
memset(dist, 0x3f, sizeof dist); // 预求最短,先设最大
|
||||
dist[1][0] = 0; // 1是起点,走0步到达即偶点,步数:0
|
||||
q.push({1, 0}); // 起点入队列
|
||||
|
||||
while (q.size()) {
|
||||
auto u = q.front();
|
||||
q.pop();
|
||||
|
||||
for (int i = h[u.x]; ~i; i = ne[i]) { // u点
|
||||
PII v = {e[i], u.y ^ 1};
|
||||
// u.y ^ 1 奇<->偶 互换
|
||||
// 由于上面的 y=u.y ^ 1,使得(u.x,u.y)只向(x,u.y的奇偶变换后数) 转移
|
||||
// 这样做,就起到的拆点作用,而并不是真正的把点拆开
|
||||
if (dist[v.x][v.y] > dist[u.x][u.y] + 1) { // 如果利用u可以更新v这个点
|
||||
dist[v.x][v.y] = dist[u.x][u.y] + 1; // 更新
|
||||
q.push(v); // v的最短距离更新了,入队列,去更新更多的点
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
cin >> n >> m >> Q;
|
||||
memset(h, -1, sizeof h);
|
||||
while (m--) {
|
||||
int a, b;
|
||||
cin >> a >> b;
|
||||
add(a, b), add(b, a);
|
||||
}
|
||||
|
||||
bfs();
|
||||
|
||||
while (Q--) {
|
||||
int a, l;
|
||||
cin >> a >> l;
|
||||
/*
|
||||
如果不写下面的特判,会被欺负死:
|
||||
3 1 1
|
||||
2 3
|
||||
1 2
|
||||
|
||||
第一行三个正整数 n,m 和 q,分别表示工人的数目、传送带的数目和工单的数目。
|
||||
也就是3个工人,1个传送带,1个工单
|
||||
传送带是2~3,也就是1是与世隔绝的
|
||||
|
||||
接下来 q 行,每行两个正整数 a 和 L,表示编号为 a 的工人想生产一个第 L 阶段的零件。
|
||||
1号工人,想生产2阶段的零件
|
||||
|
||||
由于1号工人是与世隔绝的,没有人给它提供1阶段的零件,肯定返回No,此时需要特判,返回答案
|
||||
*/
|
||||
if (h[1] == -1)
|
||||
puts("No");
|
||||
else if (l >= dist[a][l & 1]) // l是奇数,并且大于奇数最短路,则可以通过+2,+4,+6...等方法来回磨,凑够l就完事
|
||||
// l是偶数,并且大于偶数最短路,则可以通过+2,+4,+6...等方法来回磨,凑够l就完事
|
||||
puts("Yes");
|
||||
else
|
||||
puts("No");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
93
TangDou/CSP-J/AcWingTraining/T8/419.md
Normal file
93
TangDou/CSP-J/AcWingTraining/T8/419.md
Normal file
@@ -0,0 +1,93 @@
|
||||
## [$AcWing$ $419$. $FBI$树](https://www.acwing.com/problem/content/description/421/)
|
||||
|
||||
### 一、题目描述
|
||||
我们可以把由 $0$ 和 $1$ 组成的字符串分为三类:全 $0$ 串称为 $B$ 串,全 $1$ 串称为 $I$ 串,既含 $0$ 又含 $1$ 的串则称为 $F$ 串。
|
||||
|
||||
$FBI$ 树是一种二叉树,它的结点类型也包括 $F$ 结点,$B$ 结点和 $I$ 结点三种。
|
||||
|
||||
由一个长度为 $2^N$ 的 $01$ 串 $S$ 可以构造出一棵 $FBI$ 树 $T$,递归的构造方法如下:
|
||||
|
||||
$T$ 的根结点为 $R$,其类型与串 $S$ 的类型相同;
|
||||
若串 $S$ 的长度大于 $1$,将串 $S$ 从中间分开,分为等长的左右子串 $S_1$ 和 $S_2$
|
||||
;由左子串 $S_1$ 构造 $R$ 的左子树 $T_1$,由右子串 $S_2$ 构造 $R$ 的右子树 $T_2$。
|
||||
现在给定一个长度为 $2^N$ 的 $01$ 串,请用上述构造方法构造出一棵 $FBI$ 树,并输出它的后序遍历序列。
|
||||
|
||||
**输入格式**
|
||||
第一行是一个整数 $N$。
|
||||
|
||||
第二行是一个长度为 $2^N$ 的 $01$ 串。
|
||||
|
||||
**输出格式**
|
||||
包含一行,这一行只包含一个字符串,即 $FBI$ 树的后序遍历序列。
|
||||
|
||||
**数据范围**
|
||||
$0≤N≤10$
|
||||
|
||||
**输入样例**:
|
||||
```cpp {.line-numbers}
|
||||
3
|
||||
10001011
|
||||
```
|
||||
|
||||
**输出样例**:
|
||||
```cpp {.line-numbers}
|
||||
IBFBBBFIBFIIIFF
|
||||
```
|
||||
|
||||
### 二、算法
|
||||
|
||||
(递归,二叉树) $O((N+1)2^N)$
|
||||
对于样例来说,我们可以得到如下所示的满二叉树:
|
||||
|
||||

|
||||
|
||||
可以递归处理整棵树,我们发现当输入字符串固定时,整棵树也就固定了,因此可以将字符串当做递归函数传入的参数。
|
||||
|
||||
由于要输出后序遍历,因此需要先输出左子树和右子树,再输出当前节点的信息。
|
||||
|
||||
从图中可以发现,左子树所对应的字符串即是当前字符串的前半段,右子树所对应的字符串是后半段。因此依次递归处理字符串的前半段和后半段即可。
|
||||
|
||||
最后需要判断当前节点的类型,这里可以统计一下当前字符串中$0$和$1$的包含情况。
|
||||
|
||||
**时间复杂度**
|
||||
在每个递归函数内判断当前节点的类型时,均需要遍历一遍整个字符串,因此时间复杂度是线性的。从上图中可以发现,整棵树一共有 $N+1$ 层,每层总共会遍历 $O(N)$ 的长度,因此总时间复杂度是 $O(N+1)2^N$。
|
||||
|
||||
><font color='red' size=4><b>注:因为要判断每个字符串中$0$和$1$的个数,需要扫描一遍全部字符,就是整体上看是$2^N$,有$N+1$层</b></font>
|
||||
|
||||
### 三、实现代码
|
||||
```cpp {.line-numbers}
|
||||
#include <bits/stdc++.h>
|
||||
|
||||
using namespace std;
|
||||
|
||||
void dfs(string str) {
|
||||
if (str.size() > 1) {
|
||||
dfs(str.substr(0, str.size() / 2)); // 左子树
|
||||
dfs(str.substr(str.size() / 2)); // 右子树
|
||||
}
|
||||
// 我该输出啥?
|
||||
int one = 0, zero = 0;
|
||||
for (int i = 0; i < str.size(); i++)
|
||||
if (str[i] == '0')
|
||||
zero++;
|
||||
else
|
||||
one++;
|
||||
|
||||
if (one && zero)
|
||||
cout << 'F';
|
||||
else if (one)
|
||||
cout << 'I';
|
||||
else
|
||||
cout << 'B';
|
||||
}
|
||||
|
||||
int main() {
|
||||
int n;
|
||||
string str;
|
||||
cin >> n >> str;
|
||||
|
||||
dfs(str);
|
||||
return 0;
|
||||
}
|
||||
|
||||
```
|
||||
33
TangDou/CSP-J/AcWingTraining/T8/419_1.cpp
Normal file
33
TangDou/CSP-J/AcWingTraining/T8/419_1.cpp
Normal file
@@ -0,0 +1,33 @@
|
||||
#include <bits/stdc++.h>
|
||||
|
||||
using namespace std;
|
||||
|
||||
void dfs(string str) {
|
||||
if (str.size() > 1) {
|
||||
dfs(str.substr(0, str.size() / 2)); // 左子树
|
||||
dfs(str.substr(str.size() / 2)); // 右子树
|
||||
}
|
||||
// 我该输出啥?
|
||||
int one = 0, zero = 0;
|
||||
for (int i = 0; i < str.size(); i++)
|
||||
if (str[i] == '0')
|
||||
zero++;
|
||||
else
|
||||
one++;
|
||||
|
||||
if (one && zero)
|
||||
cout << 'F';
|
||||
else if (one)
|
||||
cout << 'I';
|
||||
else
|
||||
cout << 'B';
|
||||
}
|
||||
|
||||
int main() {
|
||||
int n;
|
||||
string str;
|
||||
cin >> n >> str;
|
||||
|
||||
dfs(str);
|
||||
return 0;
|
||||
}
|
||||
33
TangDou/CSP-J/AcWingTraining/T8/419_2.cpp
Normal file
33
TangDou/CSP-J/AcWingTraining/T8/419_2.cpp
Normal file
@@ -0,0 +1,33 @@
|
||||
#include <bits/stdc++.h>
|
||||
|
||||
using namespace std;
|
||||
string str;
|
||||
|
||||
void dfs(int L, int R) {
|
||||
if (R > L) {
|
||||
int mid = L + R >> 1;
|
||||
dfs(L, mid); // 左子树
|
||||
dfs(mid + 1, R); // 右子树
|
||||
}
|
||||
// 我该输出啥?
|
||||
int one = 0, zero = 0;
|
||||
for (int i = L; i <= R; i++)
|
||||
if (str[i] == '0')
|
||||
zero++;
|
||||
else
|
||||
one++;
|
||||
|
||||
if (one && zero)
|
||||
cout << 'F';
|
||||
else if (one)
|
||||
cout << 'I';
|
||||
else
|
||||
cout << 'B';
|
||||
}
|
||||
|
||||
int main() {
|
||||
int n;
|
||||
cin >> n >> str;
|
||||
dfs(0, str.size() - 1);
|
||||
return 0;
|
||||
}
|
||||
76
TangDou/CSP-J/AcWingTraining/T8/452.cpp
Normal file
76
TangDou/CSP-J/AcWingTraining/T8/452.cpp
Normal file
@@ -0,0 +1,76 @@
|
||||
#include <bits/stdc++.h>
|
||||
|
||||
using namespace std;
|
||||
const int N = 110;
|
||||
const int INF = 0x3f3f3f3f;
|
||||
int n, K, m, S, T;
|
||||
int c[N]; // 文化
|
||||
int cg[N][N]; // 互斥关系
|
||||
int g[N][N]; // 地图,不一定连通
|
||||
int d[N][N]; // floyd用的多源最短路数组
|
||||
int path[N], top; // 记录已经学过的文化
|
||||
int ans = INF; // 答案
|
||||
|
||||
void dfs(int u, int dist) { // 从u出发,dist:已经走的步数
|
||||
if (dist + d[S][u] >= ans) return; // 启发式剪枝,如果已经走的步数+从u到S的最短距离,已经大于目前的最小答案,那么,此路不通
|
||||
if (u == S) { // 如果走到了S,收集答案
|
||||
ans = dist;
|
||||
return;
|
||||
}
|
||||
|
||||
// 还没有走到起点,从哪个点走过来的
|
||||
for (int i = 1; i <= n; i++) {
|
||||
if (g[i][u] < INF) { // 可能从i点过来的
|
||||
bool flag = true; // 文件上没有排斥
|
||||
for (int j = 0; j < top; j++) { // 遍历一下所有已经走过的文化,是不是存在文化上的冲突
|
||||
if (cg[path[j]][c[i]]) { // 如果i国家的文化,与后面已知国家的文化冲突,那么一定不是从i过去的
|
||||
flag = false; // 不可能从i走到u
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (flag) { // 文化上没有排斥
|
||||
path[top++] = c[i]; // i号城市的文化c[i]进入数组
|
||||
dfs(i, dist + g[i][u]); // 继续反向搜索
|
||||
top--; // 回溯
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
// 加快读入
|
||||
ios::sync_with_stdio(false), cin.tie(0);
|
||||
|
||||
// 一般在数据量大于10W时考虑用scanf,如果scanf过不去,再考虑使用快读
|
||||
cin >> n >> K >> m >> S >> T;
|
||||
|
||||
// 先读入文化
|
||||
for (int i = 1; i <= n; i++) cin >> c[i];
|
||||
for (int i = 1; i <= K; i++) // 读入排斥关系
|
||||
for (int j = 1; j <= K; j++)
|
||||
cin >> cg[i][j];
|
||||
// 每个文化排斥自己
|
||||
for (int i = 1; i <= K; i++) cg[i][i] = 1;
|
||||
|
||||
// 两个国家之间可能有多条道路,可能有重边
|
||||
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);
|
||||
}
|
||||
// Floyd 用于启发式剪枝,提前预处理出任意两点间的最短距离
|
||||
memcpy(d, g, sizeof d);
|
||||
for (int k = 1; k <= n; k++)
|
||||
for (int i = 1; i <= n; i++)
|
||||
for (int j = 1; j <= n; j++)
|
||||
d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
|
||||
|
||||
path[top++] = c[T]; // 直播时忘记加入终点了,初始化终点
|
||||
dfs(T, 0); // 从后往前搜索,玄学倒序,专克出题人
|
||||
if (ans == INF) ans = -1; // 没有任何一个能走到S的,说明无解
|
||||
cout << ans << endl;
|
||||
return 0;
|
||||
}
|
||||
140
TangDou/CSP-J/AcWingTraining/T8/452.md
Normal file
140
TangDou/CSP-J/AcWingTraining/T8/452.md
Normal file
@@ -0,0 +1,140 @@
|
||||
## [$AcWing$ $452$. 文化之旅](https://www.acwing.com/problem/content/description/454/)
|
||||
|
||||
### 一、题目描述
|
||||
|
||||
有一位使者要游历各国,他每到一个国家,都能学到一种文化,但他不愿意学习任何一种文化超过一次(即如果他学习了某种文化,则他就不能到达其他有这种文化的国家)。
|
||||
|
||||
不同的国家可能有相同的文化。
|
||||
|
||||
不同文化的国家对其他文化的看法不同,**有些文化会排斥外来文化(即如果他学习了某种文化,则他不能到达排斥这种文化的其他国家)**。
|
||||
> <font color='red' size=4><b>注:仔细阅读,整明白是谁排斥谁,别整反了。</b></font>
|
||||
|
||||
现给定各个国家间的地理关系,各个国家的文化,每种文化对其他文化的看法,以及这位使者游历的起点和终点(在起点和终点也会学习当地的文化),国家间的道路距离,试求从起点到终点最少需走多少路。
|
||||
|
||||
**输入格式**
|
||||
第一行为五个整数 $N,K,M,S,T$,每两个整数之间用一个空格隔开,依次代表国家个数(国家编号为 $1$ 到 $N$),文化种数(文化编号为 $1$ 到 $K$),道路的条数,以及起点和终点的编号(保证 $S$ 不等于 $T$)。
|
||||
|
||||
第二行为 $N$ 个整数,每两个整数之间用一个空格隔开,其中第 $i$ 个数 $C_i$,表示国家 $i$ 的文化为 $C_i$。
|
||||
|
||||
接下来的 $K$ 行,每行 $K$ 个整数,每两个整数之间用一个空格隔开,记第 $i$ 行的第 $j$ 个数为 $a_{i,j}$,$a_{i,j}= 1$ 表示文化 $i$ 排斥外来文化 $j$($i$ 等于 $j$ 时表示排斥相同文化的外来人),$a_{i,j}= 0$ 表示不排斥(**注意 $i$ 排斥 $j$ 并不保证 $j$ 一定也排斥 $i$**)。
|
||||
|
||||
接下来的 $M$ 行,每行三个整数 $u,v,d$,每两个整数之间用一个空格隔开,表示国家 $u$ 与国家 $v$ 有一条距离为 $d$ 的可双向通行的道路(保证 $u$ 不等于 $v$,**两个国家之间可能有多条道路**)。
|
||||
|
||||
**输出格式**
|
||||
输出只有一行,一个整数,表示使者从起点国家到达终点国家最少需要走的距离数(如果无解则输出 $−1$)。
|
||||
|
||||
**数据范围**
|
||||
$2≤N≤100,1≤K≤100,1≤M≤N2,1≤Ci≤K,1≤u,v≤N,1≤d≤1000,1≤S,T≤N,S≠T$
|
||||
|
||||
|
||||
**样例**
|
||||
输入样例:
|
||||
```cpp {.line-numbers}
|
||||
2 2 1 1 2
|
||||
1 2
|
||||
0 1
|
||||
0 0
|
||||
1 2 10
|
||||
```
|
||||
|
||||
**输出样例**:
|
||||
```cpp {.line-numbers}
|
||||
10
|
||||
```
|
||||
|
||||
### 二、算法
|
||||
|
||||
**暴力+剪枝+ $floyd$**
|
||||
|
||||
有文化限制 不能直接最短路去做
|
||||
|
||||
数据范围较小 可以用$floyd$水过
|
||||
|
||||
**启发式剪枝**
|
||||
不考虑限制的最短路距离一定是小于等于考虑限制的距离
|
||||
$d[i][j]$表示$i->j$的最短距离 $ans$表示程序求出的解 $dist$表示从起点到$u$的距离
|
||||
那么
|
||||
$$\large dist + d[u][T] <= ans$$
|
||||
那么$dist + d[u][T] >= ans$ 的$ans$就是无用的,可以$continue$
|
||||
|
||||
再加一点玄学优化,倒着搜,正着搜会被卡,其余的直接暴力
|
||||
|
||||
### 三、实现代码
|
||||
```cpp {.line-numbers}
|
||||
#include <bits/stdc++.h>
|
||||
|
||||
using namespace std;
|
||||
const int N = 110;
|
||||
const int INF = 0x3f3f3f3f;
|
||||
int n, K, m, S, T;
|
||||
int c[N]; // 文化
|
||||
int cg[N][N]; // 互斥关系
|
||||
int g[N][N]; // 地图,不一定连通
|
||||
int d[N][N]; // floyd用的多源最短路数组
|
||||
int path[N], top; // 记录已经学过的文化
|
||||
int ans = INF; // 答案
|
||||
|
||||
void dfs(int u, int dist) { // 从u出发,dist:已经走的步数
|
||||
if (dist + d[S][u] >= ans) return; // 启发式剪枝,如果已经走的步数+从u到S的最短距离,已经大于目前的最小答案,那么,此路不通
|
||||
if (u == S) { // 如果走到了S,收集答案
|
||||
ans = dist;
|
||||
return;
|
||||
}
|
||||
|
||||
// 还没有走到起点,从哪个点走过来的
|
||||
for (int i = 1; i <= n; i++) {
|
||||
if (g[i][u] < INF) { // 可能从i点过来的
|
||||
bool flag = true; // 文件上没有排斥
|
||||
for (int j = 0; j < top; j++) { // 遍历一下所有已经走过的文化,是不是存在文化上的冲突
|
||||
if (cg[path[j]][c[i]]) { // 如果i国家的文化,与后面已知国家的文化冲突,那么一定不是从i过去的
|
||||
flag = false; // 不可能从i走到u
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (flag) { // 文化上没有排斥
|
||||
path[top++] = c[i]; // i号城市的文化c[i]进入数组
|
||||
dfs(i, dist + g[i][u]); // 继续反向搜索
|
||||
top--; // 回溯
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
// 加快读入
|
||||
ios::sync_with_stdio(false), cin.tie(0);
|
||||
|
||||
// 一般在数据量大于10W时考虑用scanf,如果scanf过不去,再考虑使用快读
|
||||
cin >> n >> K >> m >> S >> T;
|
||||
|
||||
// 先读入文化
|
||||
for (int i = 1; i <= n; i++) cin >> c[i];
|
||||
for (int i = 1; i <= K; i++) // 读入排斥关系
|
||||
for (int j = 1; j <= K; j++)
|
||||
cin >> cg[i][j];
|
||||
// 每个文化排斥自己
|
||||
for (int i = 1; i <= K; i++) cg[i][i] = 1;
|
||||
|
||||
// 两个国家之间可能有多条道路,可能有重边
|
||||
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);
|
||||
}
|
||||
// Floyd 用于启发式剪枝,提前预处理出任意两点间的最短距离
|
||||
memcpy(d, g, sizeof d);
|
||||
for (int k = 1; k <= n; k++)
|
||||
for (int i = 1; i <= n; i++)
|
||||
for (int j = 1; j <= n; j++)
|
||||
d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
|
||||
|
||||
path[top++] = c[T]; // 直播时忘记加入终点了,初始化终点
|
||||
dfs(T, 0); // 从后往前搜索,玄学倒序,专克出题人
|
||||
if (ans == INF) ans = -1; // 没有任何一个能走到S的,说明无解
|
||||
cout << ans << endl;
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
9
TangDou/CSP-J/AcWingTraining/T8/456.in
Normal file
9
TangDou/CSP-J/AcWingTraining/T8/456.in
Normal file
@@ -0,0 +1,9 @@
|
||||
10 8
|
||||
3 1 2 3
|
||||
3 1 3 5
|
||||
3 7 8 9
|
||||
4 1 3 5 6
|
||||
3 3 5 6
|
||||
3 1 5 9
|
||||
5 4 5 6 7 9
|
||||
3 1 5 10
|
||||
258
TangDou/CSP-J/AcWingTraining/T8/456.md
Normal file
258
TangDou/CSP-J/AcWingTraining/T8/456.md
Normal file
@@ -0,0 +1,258 @@
|
||||
##[$AcWing$ $456$ 车站分级](https://www.acwing.com/problem/content/458/)
|
||||
|
||||
### 一、题目描述
|
||||
一条 **单向** 的铁路线上,依次有编号为 $1, 2, …, n$ 的 $n$ 个火车站。
|
||||
|
||||
每个火车站都有一个级别,最低为 $1$ 级。
|
||||
|
||||
现有若干趟车次在这条线路上行驶,每一趟都满足如下要求:如果这趟车次停靠了火车站 $x$,则始发站、终点站之间所有级别 **大于等于** 火车站 $x$ 的都必须停靠。(**注意:起始站和终点站自然也算作事先已知需要停靠的站点**)
|
||||
|
||||
例如,下表是 $5$ 趟车次的运行情况。
|
||||
|
||||
其中,前 $4$ 趟车次均满足要求,而第 $5$ 趟车次由于停靠了 $3$ 号火车站($2$ 级)却未停靠途经的 $6$ 号火车站(亦为 $2$ 级)而不满足要求。
|
||||
|
||||
<center><img src='https://www.acwing.com/media/article/image/2019/03/11/19_8d0e0df443-1163900-20170818013814084-1540659827.jpg'></center>
|
||||
|
||||
现有 $m$ 趟车次的运行情况(**全部满足要求**),试推算这 $n$ 个火车站 **至少** 分为几个不同的级别。
|
||||
|
||||
**输入格式**
|
||||
第一行包含 $2$ 个正整数 $n,m$,用一个空格隔开。
|
||||
|
||||
第 $i+1$ 行($1≤i≤m$)中,首先是一个正整数 $s_i(2≤s_i≤n)$,表示第 $i$ 趟车次有 $s_i$ 个停靠站;接下来有 $s_i$ 个正整数,表示所有停靠站的编号,从小到大排列。
|
||||
|
||||
每两个数之间用一个空格隔开。输入保证所有的车次都满足要求。
|
||||
|
||||
**输出格式**
|
||||
输出只有一行,包含一个正整数,即 $n$ 个火车站最少划分的级别数。
|
||||
|
||||
**数据范围**
|
||||
$1≤n,m≤1000$
|
||||
|
||||
**输入样例**:
|
||||
```cpp {.line-numbers}
|
||||
9 3
|
||||
4 1 3 5 6
|
||||
3 3 5 6
|
||||
3 1 5 9
|
||||
```
|
||||
|
||||
**输出样例**:
|
||||
```cpp {.line-numbers}
|
||||
3
|
||||
```
|
||||
|
||||
|
||||
### 二、算法
|
||||
|
||||
**差分约束** 裸题。
|
||||
|
||||
计算 **当前线路中最小的级别**(包括始发站和终点站)。
|
||||
整条线路中所有 **大于等于** 这个级别的都 **必须停靠**, **所有未停靠的站点的级别一定小于这个级别**。
|
||||
|
||||
也就是说所有 <font color='red' size=4><b>未停靠的站点</b></font> 即为级别低,记为$A$,所有 <font color='red' size=4><b>停靠的站点</b></font> 级别一定比$A$的高,记作$B$。
|
||||
|
||||
得到公式
|
||||
$$\large B≥A+1$$
|
||||
|
||||
很明显是一道 **差分约束** 的问题。
|
||||
|
||||
根据差分约束的概念,我们从所有的$A$向所有的$B$连一条权值为$1$的有向边,描述$B>=A+1$。
|
||||
|
||||
然后根据差分约束的 **套路**,我们 **还要设一个界限才能求出最大值**。
|
||||
|
||||
因为所有车站级别都是正值,所以$A≥1$,也就是从$0$向所有的$A$中的点连一条权值为$1$ 的有向边。
|
||||
|
||||
但是由于实际数据范围较大,最坏情况下是有$1000$趟火车,每趟有$1000$个点,<font color='red' size=4><b>由于我们是用未停靠站向需停靠站连边,想要计算边数量的最大值是多少,</b></font>每趟上限有$500$个点停站,则有$(1000 - 500)$个点不停站,不停站的站点都向停站的站点连有向边,则总共有 $500∗500∗1000=2.5∗10^8$,差分约束的$spfa$有可能超时。
|
||||
> **解释**:为啥是$500$呢?因为共$1000$个点,$a+b=1000$,预使$a*b$最大,当然是$a=b=500$时。
|
||||
>
|
||||
|
||||
#### 拓扑排序优化
|
||||
|
||||
由于本题中的所有点的权值都是大于$0$,并且一定满足要求=>所有车站都等级森严=>不存在环=>可以 **拓扑排序得到拓扑图使用递推求解差分约束问题**。
|
||||
|
||||
|
||||
**整体思路**
|
||||
- ① 拓扑排序得拓扑图
|
||||
- ② **至少** =>要求的是 **最小值**=>所有条件的下界中取最大值=>**最长路**,因此我们,根据 **拓扑序跑最长路递推即可**。
|
||||
- ③ 答案为满足所有约束条件的解中最大值既是题目要求的最高的级别
|
||||
|
||||
**建图优化**
|
||||
|
||||
如果直接暴力建图就会建$O(nm)$ 条边,也就是$2∗10^8$个点,时间和空间都有可能超时。
|
||||
|
||||
我们可以在中间建一个虚拟节点,左边向虚拟点连$0$,虚拟点向右边连$1$等价于左边向右边连$1$。
|
||||
|
||||
这样只会建$O(n+m)$条边
|
||||
|
||||
注意的是本题一共$m$条线路,每条线路一个虚拟源点,所以一共会有$n + m$个点。
|
||||
|
||||
|
||||
可以在中间建一个虚拟节点,左边向虚拟点连$0$,虚拟点向右边连$1$等价于左边向右边连$1$。
|
||||
|
||||
这样只会建$O(n+m)$条边
|
||||
|
||||
|
||||
优化前:
|
||||
|
||||
<center><img src='https://img-blog.csdnimg.cn/20200731170038971.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NTY5Nzc3NA==,size_16,color_FFFFFF,t_70'></center>
|
||||
|
||||
<center><img src='https://img-blog.csdnimg.cn/202007311701072.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NTY5Nzc3NA==,size_16,color_FFFFFF,t_70'></center>
|
||||
|
||||
### 三、记忆化搜索
|
||||
```cpp {.line-numbers}
|
||||
#include <bits/stdc++.h>
|
||||
|
||||
using namespace std;
|
||||
const int N = 2010, M = 1000010;
|
||||
int n, m;
|
||||
int d[N];
|
||||
int st[N];
|
||||
|
||||
int e[M], h[N], idx, w[M], ne[M];
|
||||
void add(int a, int b, int c) {
|
||||
e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
|
||||
}
|
||||
|
||||
int dfs(int u) {
|
||||
if (~d[u]) return d[u]; // 记忆化搜索
|
||||
d[u] = 1; // 最少是一级
|
||||
for (int i = h[u]; ~i; i = ne[i]) {
|
||||
int j = e[i];
|
||||
d[u] = max(d[u], dfs(j) + w[i]);
|
||||
}
|
||||
return d[u];
|
||||
}
|
||||
|
||||
int main() {
|
||||
#ifndef ONLINE_JUDGE
|
||||
freopen("456.in", "r", stdin);
|
||||
#endif
|
||||
|
||||
ios::sync_with_stdio(false), cin.tie(0);
|
||||
cin >> n >> m;
|
||||
|
||||
memset(h, -1, sizeof h);
|
||||
for (int i = 1; i <= m; i++) {
|
||||
memset(st, 0, sizeof st);
|
||||
|
||||
int k;
|
||||
cin >> k;
|
||||
int S = n, T = 1;
|
||||
for (int j = 1; j <= k; j++) {
|
||||
int x;
|
||||
cin >> x;
|
||||
S = min(S, x);
|
||||
T = max(T, x);
|
||||
st[x] = 1;
|
||||
}
|
||||
|
||||
for (int j = S; j <= T; j++)
|
||||
if (st[j])
|
||||
add(n + i, j, 1);
|
||||
else
|
||||
add(j, n + i, 0);
|
||||
}
|
||||
|
||||
int res = 0;
|
||||
memset(d, -1, sizeof d);
|
||||
for (int i = 1; i <= n; i++) res = max(res, dfs(i));
|
||||
printf("%d\n", res);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### 四、优化建图+拓扑序+递推求解最长路
|
||||
```cpp {.line-numbers}
|
||||
#include <bits/stdc++.h>
|
||||
|
||||
using namespace std;
|
||||
const int N = 2010, M = 1000010;
|
||||
const int INF = 0x3f3f3f3f;
|
||||
int n, m;
|
||||
int in[N];
|
||||
int dist[N];
|
||||
int st[N];
|
||||
|
||||
// 邻接表
|
||||
int e[M], h[N], idx, w[M], ne[M];
|
||||
void add(int a, int b, int c) {
|
||||
e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
|
||||
}
|
||||
vector<int> path; // 拓扑路径
|
||||
void topsort() {
|
||||
queue<int> q;
|
||||
/*
|
||||
考虑极限情况:当每一个站点全部停靠的情况下,相当于左侧集合为空(不停靠的没有)
|
||||
也就没有左侧集合向虚拟点连边这件事,虚拟点的入度为0,此时,虚拟点需要入队列
|
||||
*/
|
||||
for (int i = 1; i <= n + m; i++)
|
||||
if (!in[i]) q.push(i);
|
||||
|
||||
while (q.size()) {
|
||||
int u = q.front();
|
||||
q.pop();
|
||||
path.push_back(u);
|
||||
for (int i = h[u]; ~i; i = ne[i]) {
|
||||
int j = e[i];
|
||||
in[j]--;
|
||||
if (in[j] == 0) q.push(j);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
// 加快读入
|
||||
ios::sync_with_stdio(false), cin.tie(0);
|
||||
// 建图
|
||||
memset(h, -1, sizeof h);
|
||||
cin >> n >> m; // n个火车站,m趟车次
|
||||
|
||||
for (int i = 1; i <= m; i++) {
|
||||
memset(st, 0, sizeof st); // 记录哪些是停靠点
|
||||
|
||||
int k;
|
||||
cin >> k; // 有多少个依靠站
|
||||
int S = n, T = 1;
|
||||
for (int j = 1; j <= k; j++) {
|
||||
int x; // 停靠站编号
|
||||
cin >> x;
|
||||
S = min(S, x); // 第一个停靠点
|
||||
T = max(T, x); // 最后一个停靠点
|
||||
st[x] = 1; // 记录哪些是停靠点,哪些不是停靠点
|
||||
}
|
||||
// 笛卡尔积式建图优化技巧
|
||||
int u = n + i; // 分配超级源点,虚拟点点号。多条线路,每次一个超级源点,共多建超级源点m个
|
||||
for (int j = S; j <= T; j++)
|
||||
if (st[j]) // 如果j不是停靠点
|
||||
add(u, j, 1), in[j]++; // 虚拟点向右侧集合中每个点连一条长度为1的边
|
||||
else
|
||||
add(j, u, 0), in[u]++; // 左侧集合向 虚拟点u 连一条长度为0的边
|
||||
}
|
||||
|
||||
// 求拓扑序
|
||||
topsort();
|
||||
|
||||
/*
|
||||
Q:for (int i = 1; i <= n; i) dist[i] = 1;
|
||||
y总这句话为什么不能这样写呀 for (int i = 1; i <= n + m; i) dist[i] = 1;
|
||||
|
||||
A:考虑一种边界情况,当全部站点都停靠的时候,此时虚拟节点是入度为0的节点,虚拟节点的等级dist应该为0,
|
||||
因为连向车站的边权已经是1,否则,如果虚拟节点的等级为1,那么连向的车站等级就会是2。
|
||||
*/
|
||||
|
||||
// 递推距离初始化
|
||||
for (int i = 1; i <= n; i++) dist[i] = 1; // n个站点的等级最少是1,而虚拟节点的等级最少是0
|
||||
// 拓扑路径+递推计算最长路
|
||||
for (auto u : path) // 枚举拓扑路径的每一个点
|
||||
for (int i = h[u]; ~i; i = ne[i]) {
|
||||
int j = e[i];
|
||||
dist[j] = max(dist[j], dist[u] + w[i]); // u要求j必须距离自己>=w[i]
|
||||
}
|
||||
|
||||
int res = 0;
|
||||
for (int i = 1; i <= n; i++) res = max(res, dist[i]); // 找出最大值,就是最小等级
|
||||
printf("%d\n", res);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
59
TangDou/CSP-J/AcWingTraining/T8/456_Dfs.cpp
Normal file
59
TangDou/CSP-J/AcWingTraining/T8/456_Dfs.cpp
Normal file
@@ -0,0 +1,59 @@
|
||||
#include <bits/stdc++.h>
|
||||
|
||||
using namespace std;
|
||||
const int N = 2010, M = 1000010;
|
||||
int n, m;
|
||||
int d[N];
|
||||
int st[N];
|
||||
|
||||
int e[M], h[N], idx, w[M], ne[M];
|
||||
void add(int a, int b, int c) {
|
||||
e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
|
||||
}
|
||||
|
||||
int dfs(int u) {
|
||||
if (~d[u]) return d[u]; // 记忆化搜索
|
||||
d[u] = 1; // 最少是一级
|
||||
for (int i = h[u]; ~i; i = ne[i]) {
|
||||
int j = e[i];
|
||||
d[u] = max(d[u], dfs(j) + w[i]);
|
||||
}
|
||||
return d[u];
|
||||
}
|
||||
|
||||
int main() {
|
||||
#ifndef ONLINE_JUDGE
|
||||
freopen("456.in", "r", stdin);
|
||||
#endif
|
||||
|
||||
ios::sync_with_stdio(false), cin.tie(0);
|
||||
cin >> n >> m;
|
||||
|
||||
memset(h, -1, sizeof h);
|
||||
for (int i = 1; i <= m; i++) {
|
||||
memset(st, 0, sizeof st);
|
||||
|
||||
int k;
|
||||
cin >> k;
|
||||
int S = n, T = 1;
|
||||
for (int j = 1; j <= k; j++) {
|
||||
int x;
|
||||
cin >> x;
|
||||
S = min(S, x);
|
||||
T = max(T, x);
|
||||
st[x] = 1;
|
||||
}
|
||||
|
||||
for (int j = S; j <= T; j++)
|
||||
if (st[j])
|
||||
add(n + i, j, 1);
|
||||
else
|
||||
add(j, n + i, 0);
|
||||
}
|
||||
|
||||
int res = 0;
|
||||
memset(d, -1, sizeof d);
|
||||
for (int i = 1; i <= n; i++) res = max(res, dfs(i));
|
||||
printf("%d\n", res);
|
||||
return 0;
|
||||
}
|
||||
91
TangDou/CSP-J/AcWingTraining/T8/456_Topsort.cpp
Normal file
91
TangDou/CSP-J/AcWingTraining/T8/456_Topsort.cpp
Normal file
@@ -0,0 +1,91 @@
|
||||
#include <bits/stdc++.h>
|
||||
|
||||
using namespace std;
|
||||
const int N = 2010, M = 1000010;
|
||||
const int INF = 0x3f3f3f3f;
|
||||
int n, m;
|
||||
int in[N];
|
||||
int d[N];
|
||||
int st[N];
|
||||
|
||||
// 邻接表
|
||||
int e[M], h[N], idx, w[M], ne[M];
|
||||
void add(int a, int b, int c) {
|
||||
e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
|
||||
}
|
||||
vector<int> path; // 拓扑路径
|
||||
void topsort() {
|
||||
queue<int> q;
|
||||
/*
|
||||
考虑极限情况:当每一个站点全部停靠的情况下,相当于左侧集合为空(不停靠的没有)
|
||||
也就没有左侧集合向虚拟点连边这件事,虚拟点的入度为0,此时,虚拟点需要入队列
|
||||
*/
|
||||
for (int i = 1; i <= n + m; i++)
|
||||
if (!in[i]) q.push(i);
|
||||
|
||||
while (q.size()) {
|
||||
int u = q.front();
|
||||
q.pop();
|
||||
path.push_back(u);
|
||||
for (int i = h[u]; ~i; i = ne[i]) {
|
||||
int j = e[i];
|
||||
in[j]--;
|
||||
if (in[j] == 0) q.push(j);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
// 加快读入
|
||||
ios::sync_with_stdio(false), cin.tie(0);
|
||||
// 建图
|
||||
memset(h, -1, sizeof h);
|
||||
cin >> n >> m; // n个火车站,m趟车次
|
||||
|
||||
for (int i = 1; i <= m; i++) {
|
||||
memset(st, 0, sizeof st); // 记录哪些是停靠点
|
||||
|
||||
int k;
|
||||
cin >> k; // 有多少个依靠站
|
||||
int S = n, T = 1;
|
||||
for (int j = 1; j <= k; j++) {
|
||||
int x; // 停靠站编号
|
||||
cin >> x;
|
||||
S = min(S, x); // 第一个停靠点
|
||||
T = max(T, x); // 最后一个停靠点
|
||||
st[x] = 1; // 记录哪些是停靠点,哪些不是停靠点
|
||||
}
|
||||
// 笛卡尔积式建图优化技巧
|
||||
int u = n + i; // 分配超级源点,虚拟点点号。多条线路,每次一个超级源点,共多建超级源点m个
|
||||
for (int j = S; j <= T; j++)
|
||||
if (st[j]) // 如果j不是停靠点
|
||||
add(u, j, 1), in[j]++; // 虚拟点向右侧集合中每个点连一条长度为1的边
|
||||
else
|
||||
add(j, u, 0), in[u]++; // 左侧集合向 虚拟点u 连一条长度为0的边
|
||||
}
|
||||
|
||||
// 求拓扑序
|
||||
topsort();
|
||||
|
||||
/*
|
||||
Q:for (int i = 1; i <= n; i) d[i] = 1;
|
||||
y总这句话为什么不能这样写呀 for (int i = 1; i <= n + m; i) d[i] = 1;
|
||||
|
||||
A:考虑一种边界情况,当全部站点都停靠的时候,此时虚拟节点是入度为0的节点,虚拟节点的等级d应该为0,
|
||||
因为连向车站的边权已经是1,否则,如果虚拟节点的等级为1,那么连向的车站等级就会是2。
|
||||
*/
|
||||
|
||||
// 递推距离初始化
|
||||
for (int i = 1; i <= n; i++) d[i] = 1; // n个站点的等级最少是1,而虚拟节点的等级最少是0
|
||||
// 拓扑路径+递推计算最长路
|
||||
for (auto u : path) // 枚举拓扑路径的每一个点
|
||||
for (int i = h[u]; ~i; i = ne[i]) {
|
||||
int j = e[i];
|
||||
d[j] = max(d[j], d[u] + w[i]); // u要求j必须距离自己>=w[i]
|
||||
}
|
||||
|
||||
int res = 0;
|
||||
for (int i = 1; i <= n; i++) res = max(res, d[i]); // 找出最大值,就是最小等级
|
||||
printf("%d\n", res);
|
||||
return 0;
|
||||
}
|
||||
54
TangDou/CSP-J/AcWingTraining/T8/471.cpp
Normal file
54
TangDou/CSP-J/AcWingTraining/T8/471.cpp
Normal file
@@ -0,0 +1,54 @@
|
||||
#include <bits/stdc++.h>
|
||||
using namespace std;
|
||||
|
||||
const int N = 110, INF = 0x3f3f3f3f;
|
||||
int n, m;
|
||||
int g[N][N]; // 原始地图
|
||||
int d[N][N]; // 记录最短距离,这个东西的作用是用来进行最优化剪枝,这个非常妙
|
||||
int dx[] = {-1, 0, 1, 0}, dy[] = {0, 1, 0, -1};
|
||||
|
||||
void dfs(int x, int y, int cost, int used) {
|
||||
if (d[x][y] <= cost) return; // 最优化剪枝,排除非最优路线
|
||||
d[x][y] = cost; // 记录最优路线花费
|
||||
if (x == m && y == m) return; // 递归出口
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
int nx = x + dx[i], ny = dy[i] + y;
|
||||
if (nx < 1 || nx > m || ny < 1 || ny > m) continue;
|
||||
|
||||
if (g[nx][ny] == -1) { // 下一个位置是无色
|
||||
if (used) continue; // 当前位置已使过魔法,下一个就无法继续使用魔法,无色不能变有色,无效
|
||||
// 如果当前位置没有使用过魔法的话,下一个位置是可以使用魔法的,那么变成什么颜色呢?
|
||||
// 你可以花费 2 个金币施展魔法让下一个无色格子暂时变为 你当前所处格子的颜色
|
||||
g[nx][ny] = g[x][y];
|
||||
dfs(nx, ny, cost + 2, 1); // 走入这个(nx,ny),标识费用+2,同时标注(nx,ny)的魔法是已使用状态,继续传递
|
||||
g[nx][ny] = -1; // 也可以不走这个(nx,ny),需要回溯
|
||||
} else if (g[nx][ny] == g[x][y]) // 下一个位置颜色相同
|
||||
dfs(nx, ny, cost, 0); // 直接走过去,费用不变,也没有使用魔法
|
||||
else
|
||||
dfs(nx, ny, cost + 1, 0); // 有色到有色,颜色不同,花费1个金币,没有使用魔法
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
// 加快读入
|
||||
ios::sync_with_stdio(false), cin.tie(0);
|
||||
cin >> m >> n;
|
||||
|
||||
memset(g, -1, sizeof g); // 标识无色是-1
|
||||
while (n--) {
|
||||
int a, b, c;
|
||||
cin >> a >> b >> c;
|
||||
g[a][b] = c; // 有颜色你就说是啥颜色吧
|
||||
}
|
||||
memset(d, 0x3f, sizeof d);
|
||||
|
||||
dfs(1, 1, 0, 0);
|
||||
|
||||
if (d[m][m] == INF)
|
||||
puts("-1");
|
||||
else
|
||||
cout << d[m][m] << endl;
|
||||
|
||||
return 0;
|
||||
}
|
||||
153
TangDou/CSP-J/AcWingTraining/T8/471.md
Normal file
153
TangDou/CSP-J/AcWingTraining/T8/471.md
Normal file
@@ -0,0 +1,153 @@
|
||||
## [$AcWing$ $471$. 棋盘](https://www.acwing.com/problem/content/description/473/)
|
||||
|
||||
### 一、题目描述
|
||||
有一个 $m×m$ 的棋盘,棋盘上每一个格子可能是红色、黄色或没有任何颜色的。
|
||||
|
||||
你现在要从棋盘的最左上角走到棋盘的最右下角。
|
||||
|
||||
任何一个时刻,你所站在的位置必须是有颜色的(不能是无色的),你只能向上、下、左、右四个方向前进。
|
||||
|
||||
当你从一个格子走向另一个格子时,如果两个格子的颜色相同,那你不需要花费金币;如果不同,则你需要花费 $1$ 个金币。
|
||||
|
||||
另外,你可以花费 $2$ 个金币施展魔法让下一个无色格子暂时变为 你当前所处格子的颜色。
|
||||
|
||||
但这个魔法不能连续使用,而且这个魔法的持续时间很短,也就是说,如果你使用了这个魔法,走到了这个暂时有颜色的格子上,你就不能继续使用魔法;只有当你离开这个位置,走到一个本来就有颜色的格子上的时候,你才能继续使用这个魔法,而当你离开了这个位置(施展魔法使得变为有颜色的格子)时,这个格子恢复为无色。
|
||||
|
||||
现在你要从棋盘的最左上角,走到棋盘的最右下角,求花费的最少金币是多少?
|
||||
|
||||
**输入格式**
|
||||
数据的第一行包含两个正整数 $m,n$,以一个空格分开,分别代表棋盘的大小,棋盘上有颜色的格子的数量。
|
||||
|
||||
接下来的 $n$ 行,每行三个正整数 $x,y,c$,分别表示坐标为 $(x,y)$ 的格子有颜色 $c$,其中 $c=1$ 代表黄色,$c=0$ 代表红色。
|
||||
|
||||
相邻两个数之间用一个空格隔开。棋盘左上角的坐标为 $(1,1)$,右下角的坐标为 $(m,m)$
|
||||
。
|
||||
|
||||
棋盘上其余的格子都是无色,保证棋盘的左上角,也就是 **$(1,1)$ 一定是有颜色的**。
|
||||
|
||||
**输出格式**
|
||||
输出一行,一个整数,表示花费的金币的最小值,如果无法到达,输出 $−1$。
|
||||
|
||||
**数据范围**
|
||||
$1≤m≤100,1≤n≤1000$
|
||||
|
||||
**输入样例**:
|
||||
```cpp {.line-numbers}
|
||||
5 7
|
||||
1 1 0
|
||||
1 2 0
|
||||
2 2 1
|
||||
3 3 1
|
||||
3 4 0
|
||||
4 4 1
|
||||
5 5 0
|
||||
```
|
||||
|
||||
**输出样例**:
|
||||
```cpp {.line-numbers}
|
||||
8
|
||||
```
|
||||
|
||||
### 二、题意抽象
|
||||
|
||||
走在一个棋盘上,棋盘上染着颜色,有三种颜色:红、黄、无,当你从一个格子走向另一个格子时,同色格子不花费,异色格子花费$1$,无色格子不能走,但是可以用魔法将其染成当前所处格子的颜色,花费$2$。求$(1,1)$到$(m,m)$的最短路。
|
||||
|
||||
### 三、解题思路
|
||||
因为这个数据范围非常小,是$1≤m≤100$,代表我们棋盘上至多有$10000$个格子,所以我们就有了一个爆搜的思路::问题来了,如何`dfs`求解?
|
||||
|
||||
**状态表示**:
|
||||
我们首先分析一下状态转移的时候必须要考虑的因素:
|
||||
|
||||
- ① 首先当然是状态点的坐标$(x,y)$,这个不存就不能做到正确的转移,会到别的点
|
||||
- ② 其次是花费,因为每个点到其他点的花费按照规则有一定的区别,所以也要存下花费
|
||||
- ③ 有没有用过魔法,因为不能用连续的两次魔法,所以如果这个点是用过魔法的,那转移过去的时候就不能也用魔法
|
||||
|
||||
这样一通分析下来,我们$dfs$需要的参数差不多就出来了
|
||||
|
||||
$$\large dfs(x,y,cost,used)$$
|
||||
|
||||
**状态转移**
|
||||
总感觉这写着像$DP$,但是又找不到别的词来形容,所以只能这样了$()$
|
||||
|
||||
首先我们继续进行分类:
|
||||
对于每个由$(x,y)$转移过来的节点$(nx,ny)$,我们进行如下讨论:
|
||||
|
||||
- ① 当超出棋盘时,不能转移,做 `可行性剪枝`。
|
||||
- ② 当$(nx,ny)$无色时,再分两类:
|
||||
- 如果在$(x,y)$点已经用过魔法,那么由于不能连续使用,不可转移
|
||||
- 如果在$(x,y)$点没有用过魔法,那么可以使用魔法染色,花费$2$
|
||||
- ③ 当$(nx,ny)$有色时,再分两类:
|
||||
- $(nx,ny)$与$(x,y)$同色:转移,花费$0$
|
||||
- $(nx,ny)$与$(x,y)$不同色:转移,花费$1$
|
||||
|
||||
总结起来就是
|
||||
- ① 不可转移
|
||||
- ②
|
||||
- $dfs(x,y,cost,1)$ → 无法转移,用过魔法
|
||||
- $dfs(x,y,cost,0)→dfs(nx,ny,cost+2,1)$
|
||||
- ③
|
||||
- $dfs(x,y,cost,used)→dfs(nx,ny,cost,0)$
|
||||
- $dfs(x,y,cost,used)→dfs(nx,ny,cost+1,0)$
|
||||
|
||||
**剪枝**
|
||||
这题不剪枝是过不了的,会爆栈。
|
||||
所以我们可以加一个很简单的剪枝,当走到某个点的花费已经比目前最优解贵了,就不搜了,因为无论如何都不能更好,没有搜索的价值。
|
||||
|
||||
|
||||
### 四、实现代码
|
||||
```cpp {.line-numbers}
|
||||
#include <bits/stdc++.h>
|
||||
using namespace std;
|
||||
|
||||
const int N = 110, INF = 0x3f3f3f3f;
|
||||
int n, m;
|
||||
int g[N][N]; // 原始地图
|
||||
int d[N][N]; // 记录最短距离,这个东西的作用是用来进行最优化剪枝,这个非常妙
|
||||
int dx[] = {-1, 0, 1, 0}, dy[] = {0, 1, 0, -1};
|
||||
|
||||
void dfs(int x, int y, int cost, int used) {
|
||||
if (d[x][y] <= cost) return; // 最优化剪枝,排除非最优路线
|
||||
d[x][y] = cost; // 记录最优路线花费
|
||||
if (x == m && y == m) return; // 递归出口
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
int nx = x + dx[i], ny = dy[i] + y;
|
||||
if (nx < 1 || nx > m || ny < 1 || ny > m) continue;
|
||||
|
||||
if (g[nx][ny] == -1) { // 下一个位置是无色
|
||||
if (used) continue; // 当前位置已使过魔法,下一个就无法继续使用魔法,无色不能变有色,无效
|
||||
// 如果当前位置没有使用过魔法的话,下一个位置是可以使用魔法的,那么变成什么颜色呢?
|
||||
// 你可以花费 2 个金币施展魔法让下一个无色格子暂时变为 你当前所处格子的颜色
|
||||
g[nx][ny] = g[x][y];
|
||||
dfs(nx, ny, cost + 2, 1); // 走入这个(nx,ny),标识费用+2,同时标注(nx,ny)的魔法是已使用状态,继续传递
|
||||
g[nx][ny] = -1; // 也可以不走这个(nx,ny),需要回溯
|
||||
} else if (g[nx][ny] == g[x][y]) // 下一个位置颜色相同
|
||||
dfs(nx, ny, cost, 0); // 直接走过去,费用不变,也没有使用魔法
|
||||
else
|
||||
dfs(nx, ny, cost + 1, 0); // 有色到有色,颜色不同,花费1个金币,没有使用魔法
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
// 加快读入
|
||||
ios::sync_with_stdio(false), cin.tie(0);
|
||||
cin >> m >> n;
|
||||
|
||||
memset(g, -1, sizeof g); // 标识无色是-1
|
||||
while (n--) {
|
||||
int a, b, c;
|
||||
cin >> a >> b >> c;
|
||||
g[a][b] = c; // 有颜色你就说是啥颜色吧
|
||||
}
|
||||
memset(d, 0x3f, sizeof d);
|
||||
|
||||
dfs(1, 1, 0, 0);
|
||||
|
||||
if (d[m][m] == INF)
|
||||
puts("-1");
|
||||
else
|
||||
cout << d[m][m] << endl;
|
||||
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user