最短路
1 单源最短路径
1.1 dijkstra
使用“堆优化的”dijkstra 算法,每次对刚刚加入的点进行一次拓展,然后找出 dis 里面最大的一个。
过程:重复这些操作:
- 从 \(S\) 集合中,选取一个最短路长度最小的结点,移到 \(T\) 集合中。
- 对那些刚刚被加入 \(T\) 集合的结点的所有出边执行松弛操作。
正确性:只适用于所有边权为正数的图。是用数学归纳法证明正确性,假设 \(T\) 集合已有的节点的 \(dis\) 都是真正的最短距离。那么对于目前 \(S\) 中最短距离的节点,不会有新的路径可以松弛它。因为其他点的距离都比它大,而且图上只有负权边。
时间复杂度:每成功松弛一条边 ,就将 \(v\) 插入二叉堆中(如果 \(v\) 已经在二叉堆中,直接修改相应元素的权值即可),1 操作直接取堆顶结点即可。共计 \(O(m)\) 次二叉堆上的插入(修改)操作,\(O(n)\) 次删除堆顶操作,而插入(修改)和删除的时间复杂度均为 \(O(\log n)\),时间复杂度为 \(O((n+m)\log n)\)。(比优先队列实现优一些,优先队列是 \(O((n+m) \log m)\) 的。
注意 std::priority_queue 是一个大根堆。
值得注意的是有 \(O(n^2)\) 的写法,对稠密图(例如网络流的原始对偶算法)要记住。
#include<bits/stdc++.h>
using namespace std;
#define f(i, a, b) for(int i = a; i <= b; i++)
#define mod9 998244353
#define mod1 1000000007
typedef long long ll;
typedef unsigned long long ull;
typedef long double ld;
typedef pair<int, int> pii;
typedef pair<ll, ll> pll;
vector<pii> g[100010];
int n, m, s;
int dep[100010];
priority_queue<pii> q; //dep, i
bool in[100010];
void dijkstra() {
dep[s] = 0;
q.push(make_pair(0, s));
while(!q.empty()) {
int now = q.top().second, d = -q.top().first;
q.pop();
if(in[now]) continue;
in[now] = 1;
if(!g[now].empty()) {
f(i, 0, g[now].size() - 1) {
int next = g[now][i].first, dis = g[now][i].second;
if(dep[now] + dis < dep[next]) {
q.push(make_pair(-dep[now] - dis, next));
dep[next] = dep[now] + dis;
}
}
}
}
f(i, 1, n) cout << dep[i] << " ";
}
int main(){
ios::sync_with_stdio(0);
cin.tie(NULL);
cout.tie(NULL);
cin >> n >> m >> s;
f(i, 1, m) {
int x, y, z;
cin >> x >> y >> z;
g[x].push_back(make_pair(y, z));
}
memset(dep, 0x3f, sizeof(dep));
dijkstra();
return 0;
}
1.1 最短路径数
使用 dijkstra,\(dp[i]\) 表示当前找到的 \(1 \sim i\) 的最短路径数。每次如果 \(i\) 扩展到的点 \(j\) 目前的 \(dis_j\) 大于 \(dis_i+w_{ij}\),那么说明之前找的不是最短路,将 \(dp[j]\) 覆盖为 \(dp[i]\),并且向优先队列里加入 \(j\)。否则将 \(dp[j]\) 加上 \(dp[i]\),并且不用向优先队列里加入 \(j\)(本来 dijkstra 就不需要将一样的再扩展一遍)
板子:(这里是无权图)
#include<bits/stdc++.h>
using namespace std;
#define f(i, a, b) for(int i = (a); i <= (b); i++)
#define cl(i, n) i.clear(),i.resize(n);
#define endl '\n'
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
#define int long long
const int inf = 1e9;
int n, m;
vector<int> g[1000010];
int dp[1000010];
bool vis[1000010]; //是否已经在最短路内
int d[1000010];
priority_queue<pii> q; //dis, ind
signed main() {
ios::sync_with_stdio(0);
cin.tie(NULL);
cout.tie(NULL);
cin >> n >> m;
f(i, 1, m) {
int x, y; cin >> x >> y;
g[x].push_back(y); g[y].push_back(x);
}
memset(d, 0x3f, sizeof(d));
d[1] = 0; dp[1] = 1;
q.push(make_pair(0, 1));
while(!q.empty()) {
int now = q.top().second, dis = -q.top().first; q.pop();
if(vis[now]) continue;
vis[now] = 1;
f(i, 0, (int)g[now].size() -1 ) {
int nxt = g[now][i];
if(d[nxt] > d[now] + 1) {
dp[nxt] = dp[now]; d[nxt] = d[now] + 1;
q.push(make_pair(-d[now] - 1, nxt));
}
else if(d[nxt] == d[now] + 1) {
dp[nxt] += dp[now]; d[nxt] = d[now] + 1;
}
}
}
f(i, 1, n) cout << dp[i] % mod << endl;
return 0;
}
NOIP2017 逛公园
题意:给定一个非负权 \(N\) 点 \(M\) 边有向图,假设 \(1\) 到 \(n\) 的最短路径长度为 \(d\),求 \(1\) 到 \(n\) 的长度不超过 \(d+K\) 的路线的数量。如果有无穷多条,输出 \(-1\)。
\(N \le 10^5, M \le 2 \times 10^5, K \le 50,\) 部分测试点有 \(0\) 边。
其实是为了看这个题...
遇到这种题目很容易想到最短路径计数。其实就是它的推广。
考虑 \(K=0\) 的情况就是最短路径计数问题。
\(K < 50\),我们可以自然地想到做一个推广:\(dp[i][k]\) 表示从 \(1\) 到 \(i\),距离为 \(dis[i] + k\) 的路径总数。考虑转移:从 \(i\) 走到 \(j\) 的距离是 \(w[i][j]\),那么多费的步数就是 \(w[i][j] - dis[j] + dis[i]\)。
考虑到有一些点不会到达 \(n\),我们可以考虑优化:先对反图跑一遍 dijkstra,求出 \(dis[i]\) 是 \(i\) 到 \(n\) 的距离。然后我们可以有记忆化搜索(是用 dfs 实现的动态规划,其本质是 dfs,如果我们的状态转移方向是 \(i \leftarrow j(j<i)\),那么我们的 dfs 入口是 \(i\),如果发现 \(dp[j]\) 还没有算出来那就 dfs 前往 j,否则直接引用已经算过的 \(dp[j]\) 的值。所以它是 dfs 的壳子优化了 dp 的算法):\(dp[x][j] = \sum \limits_{y, x \rightarrow y} dp[y][j - cost], cost = w_{xy} + dis_y - dis_x, dis_x = mindistance(x,n)\)。
这样做还顺便把零环判了:如果重复 dfs 到已经访问过的 \(dp[x][j]\) 说明出现了零环!不用 tarjan 等等就可以。
时间复杂度 \(O((n+m) \log n + nK)\)
跑的嗖嗖快!
(其实细节挺多,比如啥时候给 dp 赋值等等,学一学记忆化搜索大神怎么写的)
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define f(i, a, b) for(int i = (a); i <= (b); i++)
#define cl(i, n) i.clear(),i.resize(n);
#define endl '\n'
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
const int inf = 1e9;
int n, m, k, p;
struct edge{
int a, b, c;
}e[200010];
vector<pii> g[100010]; int dis[100010]; bool vis[100010];
bool vis1[100010][55]; bool huan; int dp[100010][55];
priority_queue<pii> q;
void dijkstra(int x) {
dis[x] = 0; q.push(make_pair(dis[x], x));
while(!q.empty()) {
int now = q.top().second; q.pop();
if(vis[now]) continue;
f(i, 0, (int)g[now].size() - 1) {
int nxt = g[now][i].first, d = g[now][i].second;
if(dis[now] + d < dis[nxt]) {
dis[nxt] = dis[now] + d;
q.push(make_pair(-dis[nxt], nxt));
}
}
}
return;
}
int dfs(int x, int j) {
if(vis1[x][j]) { huan = 1; return 0; }
if(dp[x][j] != -1) return dp[x][j];
int res = 0;
vis1[x][j] = 1;
f(i, 0, (int)g[x].size() - 1) {
int y = g[x][i].first;
int cost = g[x][i].second + dis[y] - dis[x];
if(j - cost < 0 || j - cost > k) continue;
res = (res + dfs(y, j - cost)) % p;
if(huan) return 0;
}
if(x == n && j == 0) res = 1;
vis1[x][j] = 0; dp[x][j] = res;
return res;
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(NULL);
cout.tie(NULL);
int t; cin >> t;
while(t--) {
cin >> n >> m >> k >> p; f(i, 1, n) g[i].clear();
while(!q.empty()) q.pop();
f(i, 1, m) {
cin >> e[i].a >> e[i].b >> e[i].c;
g[e[i].b].push_back(make_pair(e[i].a, e[i].c));
}
memset(dis, 0x3f, sizeof(dis)); memset(vis, 0, sizeof(vis));
dijkstra(n);
f(i, 1, n) g[i].clear();
f(i, 1, m) g[e[i].a].push_back(make_pair(e[i].b, e[i].c));
int ans = 0;
huan = 0; memset(vis1, 0, sizeof(vis1));
memset(dp, -1, sizeof(dp));
f(i, 0, k) {ans = (ans + dfs(1, i)) % p;}
cout << (huan ? -1 : ans) << endl;
}
return 0;
}
ARC150C
题意:给定 \(n\) 点 \(m\) 边无向连通图 \(G\) 和数组 \(A_{1,...,n},B_{1,...,k}\),判断是否有如下性质:
- 对于所有从 \(1\) 到 \(n\) 的简单路径 \(\{v_1,v_2,...,v_j\}(v_1 = 1, v_j = n)\),\(B\) 是 \(A_{v_1},A_{v_2},...A_{v_j}\) 的(不必要连续的)子序列。
分析:这是一个可以学习的新型套路:如果遇到“所有”的路径(不一定是简单路径)那么考虑转化为最短路问题,令 \(w_i\) 为从 \(1\) 走到 \(i\) 的路径中只要经过下一层的点就一定将其与 \(b\) 的下一层匹配,能匹配到的点的最小值(类似数学里的“任意”就要求出最劣的点然后看是否满足条件)。然后看 \(w_n\) 是否等于 \(k\) 即可。这个用 dijkstra 实现不难。(在这道题里不依赖于简单路径,可以有环)
赛时想到建立圆方树,但是首先这个 \(b\) 里可能有重复的元素没有办法将整张图分层;其次点双连通分量中每两个点只是“有两条以上简单路径”而不知道具体是多少,于是不可做。
地铁换乘
https://oj-szshs.bopufund.com/p/44?tid=634a4cb48ce73726d6d08d5f
题意:给定 \(n\) 点 \(m\) 边地铁图,第 \(i\) 条边表示连接两个站的 \(c_i\) 号线地铁,花费为 \(t_i\)。在一个站点可以换乘,从第 \(i\) 号线换到第 \(j\) 号线,花费 \(|x - y|\) 的时间。求从 \(1\) 号站点到达每个站点的最短时间。
分析:这道题利用的是拆点的思路,把每个地铁所拥有的颜色都建一个点,然后颜色与颜色之间建 \(|x-y|\) 的边,然后把所有 \(1\) 的某一个颜色的 \(dis\) 设为 \(0\),跑 dijkstra 即可。
注意实现细节,特别是没有边的时候 \(dis_1 = 0\)。
CF1749E
题意:Monocarp正在玩Minecraft,他想建造一堵仙人掌墙。他想在一块大小为n×m单元的沙地上建造它。最初,场地的一些单元里有仙人掌。请注意,在Minecraft中,两个仙人掌不能生长在相邻的单元格上——而初始场地符合这一限制。Monocarp可以种植新的仙人掌(它们也必须满足前述条件)。他不能砍掉已经长在田地上的任何仙人掌——他没有斧头,而且仙人掌对他的手来说太扎手了。
莫诺卡普认为,如果从田地的最上面一排到最下面一排没有路径,那么这堵墙就是完整的。
路径上的每两个连续单元都是相邻的。
属于该路径的单元格中没有仙人掌。
你的任务是种植最小数量的仙人掌来建造一堵墙(或报告说这是不可能的)。
\(n,m \le 2 \times 10^5, \sum nm \le 4 \times 10^5\)
分析:
又是 dijkstra。
我们抽象出模型,需要做的是构造一条从左到右的路。
然后一条路上的点 \(i+j\) 奇偶性完全相同。
左边是起点,右边是终点,边权是是否要建造仙人掌。
点数 \(\le 2 \times 10^5\),边数 \(\le 8 \times 10^5\)。
能过。
实现:
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define f(i, a, b) for(int i = (a); i <= (b); i++)
#define cl(i, n) i.clear(),i.resize(n);
#define endl '\n'
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
const int inf = 1e9;
int n, m;
vector<vector<char>> c;
priority_queue<pair<int, pair<pii,pii>>> q; //-dis, cur, lst
vector<vector<int>> dis;
vector<vector<bool>> vis;
vector<vector<pii>> lst;
int dx[]={1,-1,0,0};
int dy[]={0,0,1,-1};
int ddx[]={1,1,-1,-1};
int ddy[]={-1,1,-1,1};
int ans=inf, cho;
bool ok(int x,int y){
f(i,0,3){
int nx=x+dx[i],ny=y+dy[i];
if(nx<0||nx>n||ny<0||ny>m)continue;
if(c[nx][ny]=='#')return 0;
}
return 1;
}
void dijkstra() {
while(!q.empty()) {
int d=-q.top().first;
pii cur=q.top().second.first,las=q.top().second.second;
q.pop();
if(vis[cur.first][cur.second])continue;
vis[cur.first][cur.second]=1;
dis[cur.first][cur.second]=d;
lst[cur.first][cur.second]=las;
int x=cur.first,y=cur.second;
f(i,0,3){
int nx=x+ddx[i],ny=y+ddy[i];
if(nx<1||nx>n||ny<1||ny>m)continue;
if(ok(nx,ny)){
if(c[nx][ny]=='#') q.push({-d,{{nx,ny},cur}});
else q.push({-(d+1),{{nx,ny},cur}});
}
}
}
f(i,1,n){
if(ans>dis[i][m]){
ans=min(ans,dis[i][m]); cho=i;
}
}
//找不到
return;
}
void walk(int x, int y) {
if(x == 0 && y == 0) return;
c[x][y]='#';
walk(lst[x][y].first,lst[x][y].second);
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(NULL);
cout.tie(NULL);
time_t start = clock();
//think twice,code once.
//think once,debug forever.
int T; cin >> T;
while(T--) {
ans=inf;
cin>>n>>m;
c.resize(n+5);
f(i,0,n+1)c[i].resize(m+5);
vis.resize(n+5);
f(i,0,n+1)vis[i].resize(m+5);
lst.resize(n+5);
f(i,0,n+1)lst[i].resize(m+5);
dis.resize(n+5);
f(i,0,n+1)dis[i].resize(m+5);
f(i,1,n)f(j,1,m)dis[i][j]=inf;
f(i,1,n)f(j,1,m)vis[i][j]=0;
f(i,1,n)f(j,1,m)cin>>c[i][j];
//处理 i+j 为奇数的
for(int i=1;i<=n;i+=2){
if(ok(i,1)) {
q.push({-(c[i][1]=='.'), {{i,1},{0,0}}});
}
}
dijkstra();
f(i,1,n)f(j,1,m)dis[i][j]=inf;
f(i,1,n)f(j,1,m)vis[i][j]=0;
for(int i=2;i<=n;i+=2){
if(ok(i,1)) {
q.push({-(c[i][1]=='.'), {{i,1},{0,0}}});
}
}
dijkstra();
if(ans==inf)cout<<"NO\n";
else {
cout<<"YES\n";
walk(cho,m);
f(i,1,n){f(j,1,m)cout<<c[i][j];cout<<endl;}
}
}
time_t finish = clock();
//cout << "time used:" << (finish-start) * 1.0 / CLOCKS_PER_SEC <<"s"<< endl;
return 0;
}
CF1737D
敲头,上次没补明白这次还不会!
【题意】
有一个包含 \(n\) 个点和 \(m\) 条边的无向图,每条边 \(i\) 连接着点 \(u_i\) 和 \(v_i\),权值为 \(w_i\),通过这条边要用 \(w_i\) 微秒。
Ela 需要从 \(1\) 号点走到 \(n\) 号点,但他觉得原本的路径太长了。好在,Wiring Wizard 可以帮他改变路径。
具体地,对于任意三点 \(u,v,t\)(\(u\) 和 \(t\) 可以相同),若 \(u\) 与 \(v\) 之间有边 \(i\),且 \(v\) 与 \(t\) 有边 \(j\),那么他可以用 \(w_i\) 微秒断开边 \(i\),并且在 \(u\) 与 \(t\) 之间连一条权值为 \(w_i\) 的边。他可以改任意条边,也可以不改。
Ela 想知道,他至少要花多久时间才能从 \(1\) 号点走到 \(n\) 号点(时间包括修改边的时间)。
有 \(t\) 组数据。
\(1\le t\le 100\)
\(2\le n\le 500\ ,\ n-1\le m\le 250000\)
\(1\le u_i,v_i\le n\ ,\ 1\le w_i\le 10^9\)
\(\Sigma{n}\le 500\ ,\ \Sigma{m}\le 250000\)
保证输入的图为联通图,无自环,但可能有重边。
【分析】
首先观察到如果某一条路径上有 \(a_1,...,a_k\) 这些点,那么缩成 \(k \times \min \limits_{i = 1}^ k a_i\) 是最佳选项。
然后考虑能不能推到每一条边上(也就是是否有结论:答案一定等于某条边的边权乘以某个数)。考虑这张图:
假设最后走了若干条边 \(b_1,b_2,..,b_k\)。那么规约到上个引理。因此我们最后只会走一条边。假设这条边的边权是 \(t\)。
考虑它和其他边合并的过程。如果遇到比它大的边,那么使用这条边权还不如使用自己。否则,可以直接继续用小边,不会用到自己。
因此我们证明了,答案一定等于某条边的边权乘以某个数。
依然考虑上述图,是做了七次操作,如下图:
发现是先将一个节点移到一条 \(1\) 到 \(n\) 的路径上,然后另一个节点拉到一起做一个自环,然后两边拉伸。考虑做自环的点的编号为 \(k\),先移动的节点是 \(u\)。那么答案为 \(w \times (dis_{1,k} + dis_{k, n} + dis_{u, k} + 1 + 1)\)。值得注意的是,这个 \(dis\) 都是不包含原边的。但是没关系。如果只有走这条边才能到达 \(k\),那么 \(k\) 一定非最佳选择。(因为 \(dis_{1,k}+dis_{k,n}\) 太大)
但是有个特例:如果本身就是 \(1 \sim n\) 上的某一条路径上的一条边,那么不用做自环,直接就可以进行伸展。对于这部分的答案,我们抽象出来:
定义一条由若干条边组成的路径的距离为 (这个路径上边权最小值 × 走过的边数)。求 \(1 \sim n\) 的最短距离。
dijkstra 是不行的,不符合贪心性质。应该怎么办?考虑固定最小那条边,然后用边的两头的 distance 计算。这个显然正确。那么总复杂度 \(O(nm)\)。
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define f(i, a, b) for(int i = (a); i <= (b); i++)
#define cl(i, n) i.clear(),i.resize(n);
#define endl '\n'
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
const int inf = 1e14;
void cmax(int &x, int y) {if(x < y) x = y;}
void cmin(int &x, int y) {if(x > y) x = y;}
//调不出来给我对拍!
vector<int> num[550][550];
int mn[550][550];
int dis[550][550];int n,m,ans;
void floyd() {
f(k,1,n)f(i,1,n)f(j,1,n){
cmin(dis[i][j],dis[i][k]+dis[k][j]);
}
}
struct edge{
int u,v,w;
}e[250010];
signed main() {
ios::sync_with_stdio(0);
cin.tie(NULL);
cout.tie(NULL);
//time_t start = clock();
//think twice,code once.
//think once,debug forever.
int T; cin >> T;
while(T--) {
cin>>n>>m;
f(i,1,n)f(j,1,n)num[i][j].clear();
f(i,1,n)f(j,1,n)mn[i][j]=inf;
f(i,1,m){
int u,v,w;cin>>u>>v>>w;
num[u][v].push_back(w);
num[v][u].push_back(w);
cmin(mn[u][v],w);
cmin(mn[v][u],w);
e[i]=(edge){u,v,w};
}
f(i,1,n)f(j,1,n){
if(i==j)dis[i][j]=0;
else if(num[i][j].empty())dis[i][j]=inf;
else dis[i][j]=1;
}
floyd();
ans=inf;
f(i,1,n)f(j,1,n){
cmin(ans, mn[i][j] * (dis[1][i] + dis[j][n] + 1));
}
f(i,1,m){
int u=e[i].u,v=e[i].v,w=e[i].w;
f(k,1,n){
int xx=min(dis[u][k],dis[v][k]);
cmin(ans,w*(xx+1+dis[1][k]+dis[k][n]+1));
}
}
cout<<ans<<endl;
}
//time_t finish = clock();
//cout << "time used:" << (finish-start) * 1.0 / CLOCKS_PER_SEC <<"s"<< endl;
return 0;
}
1.2 bellman-ford 和 spfa
单源最短路径,包含负权边,可以判负环。
考虑源点为 \(1\)。进行 \(n-1\) 次“增广”操作,每次对所有边进行松弛,就是 bellman-ford,时间复杂度为 \(O(nm)\)。
正确性证明:“增广” \(k\) 次之后,任何从 \(1\) 到 \(i\) 的经历 \(k+1\) 个点以下的路径均会被考虑进 \(i\) 的最短路。
用一个队列维护可以增广其他人的所有点。增广到的点如果还在队内,那么就不入队了,否则入队。
注意出队的时候要把标记删掉。
时间复杂度为 \(O(nm)\),强于 bellman-ford。
void spfa() {
f(i,1,n)dep[i]=(1ll<<31) - 1;
dep[s] = 0;
queue<ll> q; q.push(s);inq[s]=1;
while(!q.empty()) {
ll now = q.front(); q.pop();inq[now]=0;
for(pll i : g[now]){
ll nxt=i.first, w=i.second;
if(dep[now]+w<dep[nxt]) {
dep[nxt]=dep[now]+w;
if(!inq[nxt]){inq[nxt]=1;q.push(nxt);}
}
}
}
}
1.3 同余最短路
P2371 墨墨的不等式
\(a_1,a_2,...,a_n\) 乘上非负系数能够表出的有哪些数?
\(n \le 12, a_i \le 10^5\)
【分析】
考虑对于 \(a_i\) 的某个同余类,能表出的为一段无限的后缀,因为可以不断加上 \(a_i\) 构成。
那么只需要保存每个同余类第一个能被表示的数就好了。
因此考虑一个同余类是一个点,用其他数建边,\(dis\) 表示的是这个第一个被表示的数除以 \(a_1\) 的结果。
\(a_1\) 取最小值较好。
ARC084B https://atcoder.jp/contests/abc077/tasks/arc084_b
【题意】
给定 \(k\),求一个 \(k\) 的倍数,使得其数码和最小。
【分析】
神仙题,考虑对数位处理。对 \(k\) 的倍数考虑不好搞,考虑每个数都可以从 \(1\) 通过 \(+1\) 和 \(\times 10\) 得到,那么从 \(i\) 向 \((i + 1) \bmod k\) 和 \(10i \bmod k\) 连边,数码和 \(+1, +0\)。01-BFS 即可。
int k; cin >> k;
deque<pii> q; q.push_back({1, 1});
while(!q.empty()) {
pii now = q.front(); q.pop_front();
if(vis[now.first]) {continue;}
vis[now.first] = 1; dp[now.first] = now.second;
q.push_back({(now.first+1)%k, dp[now.first]+1}); q.push_front({10*now.first%k, dp[now.first]});
}
cout << dp[0] << endl;
1.4 01BFS
JOI2023 C
【题意】
给定一个 \(n \times m\) 的 \(01\) 网格,有一个 \(k \times k\) 的印章,可以花费 \(1\) 的代价盖在任何一个地方并把这些格子都变成 \(0\)。求使得起点和终点之间有一条只由 \(0\) 组成的边花费的最小代价。
考虑正解:
这么一个流程,我们怎么实现能够不退化地完成这个 01-BFS 呢?
先看代码:
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define f(i, a, b) for(int i = (a); i <= (b); i++)
#define cl(i, n) i.clear(),i.resize(n);
#define endl '\n'
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
const int inf = 1e9;
//#define cerr if(false)cerr
//#define freopen if(false)freopen
#define watch(x) cerr << (#x) << ' '<<'i'<<'s'<<' ' << x << endl
void pofe(int number, int bitnum) {
string s; f(i, 0, bitnum) {s += char(number & 1) + '0'; number >>= 1; }
reverse(s.begin(), s.end()); cerr << s << endl;
return;
}
void cmax(int &x, int y) {if(x < y) x = y;}
void cmin(int &x, int y) {if(x > y) x = y;}
//调不出来给我对拍!
struct node{int r,c;}s,g;
bool in(int x,int l,int r){return x>=l&&x<=r;}
int r,c,n; bool ok(int x,int y){return in(x,1,r)&&in(y,1,c);}
deque<tuple<int,int,int,int>> q;
vector<vector<int>> a,d;
int dx[]={0,0,-1,1,-1,1,-1,1};
int dy[]={-1,1,0,0,1,1,-1,-1};
signed main() {
ios::sync_with_stdio(0);
cin.tie(NULL);
cout.tie(NULL);
//freopen();
//freopen();
//time_t start = clock();
//think twice,code once.
//think once,debug forever.
cin>>r>>c>>n; a.resize(r+10),d.resize(r+10);
f(i,1,r)a[i].resize(c+10),d[i].resize(c+10);
cin>>s.r>>s.c>>g.r>>g.c;
f(i,1,r)
f(j,1,c){
char ch;cin>>ch; a[i][j]=(ch=='#'?1:0),d[i][j]=inf;
}
q.push_back({0,n,s.r,s.c}); //return 0;
while(d[g.r][g.c]==inf){
auto [i,j,k,l] = q.front();
q.pop_front();
if(d[k][l] != inf) continue;
d[k][l] = i;//cout <<k<<" "<<l<<" "<<d[k][l]<<endl;
if(j < n) {
f(t, 0, 7) {
if(!ok(k+dx[t],l+dy[t]))continue;
q.push_back({i,j+1,k+dx[t],l+dy[t]});
}
}
else {
f(t, 0, 3) {
if(!ok(k+dx[t],l+dy[t]))continue;
if(a[k+dx[t]][l+dy[t]]==0){
q.push_front({i,j,k+dx[t],l+dy[t]});
}
else {
q.push_back({i+1,1,k+dx[t],l+dy[t]});
}
}
}
}
cout<<d[g.r][g.c]<<endl;
//time_t finish = clock();
//cout << "time used:" << (finish-start) * 1.0 / CLOCKS_PER_SEC <<"s"<< endl;
return 0;
}
/*
2023/x/xx
start thinking at h:mm
start coding at h:mm
finish debugging at h:mm
*/
(注意,auto [i,j,k,l] = ...
这个东西是 c++17 的,ccf 是不能用的。)
主要就是,考虑记录层数和其距离。
首先,不会出现一个点被遍历两次依然有效的情况。证明和 bfs 的证明是差不多的(前提是第一次的时候以步数为第一关键字,层数为第二关键字是最小的):考虑该位置可以拓展到什么位置。显然步数相同的时候,层数更小的可以覆盖层数更大的范围。步数不同的时候,可以先走若干步到步数相同的时候,显然更是能覆盖了。因此一个点不会有两次有用。
其次,01bfs 的时候其实也是以步数为第一关键字,层数为第二关键字分层。这就代表了,我们步数增加的时候也要放到队尾。
有时候状态间的层次是隐藏起来的,但是它确实存在,就可以 bfs。
2 全源最短路
2.1 floyd
多源最短路径。有负环的时候在一个连通块里面没有最短路径,要注意判断。
考虑三角形不等式:
\(dist[x] > dist[y] + w\),这时若存在 \(y \rightarrow x\) 且边权为 \(w\) 的边,那么就可以将 \(dist[x]\) 更新为 \(dist[y] + w\)。
考虑三重循环枚举 \(k, i, j\),并使用 \(dist[i][k] + dist[k][j]\) 松弛 \(dist[i][j]\)。这样进行之后,\(dist[i][j]\) 会是 \(i\) 到 \(j\) 的最短距离。
正确性证明:假设最短距离上有 \(x\) 个点,那么每次遍历到这些点中间的一个,都会松弛两边的点。最后这 \(x\) 个点都会被松弛到。
注意,只有 \(k,i,j\) 的顺序枚举是对的。
时间复杂度 \(O(n^3)\)。
2.2 johnson
可以替代 floyd,利用势能函数,就是更难写一些。
应用:可以有负权的图,并且可以判负环,可以求全源最短路径。
方法:这个方法改编自求 \(n\) 次 SPFA。其可以通过仅 \(1\) 次 SPFA 实现重新赋边权的操作,改成正权图。在网络流中也可以用这个操作进行优化 SPFA。建立一个虚点 \(0\),并且建立 \(0 \rightarrow [1, n]\) 边权为 \(0\) 的边。SPFA 求出来 \(0\) 到每个点的距离 \(h_i\)。
这里用 \(h\),是因为有人把它叫做势能,意思是这个数值在某一个路径中只和开头和结尾两个点有关,和路径无关。待会就知道为什么。
求出 \(h_i\) 之后,我们给每一条原图上的边 \((u \rightarrow v), w\),重新赋边权得到 \((u \rightarrow v), w + h_u - h_v\)。这样使得从 \(i\) 到 \(j\) 的某一条原图上的路径到了新图上距离为 \(w_{i, a_1} + h_i - h_{a_1} + w_{a_1, a_2} + h_{a_1} - h_{a_2} + ... + w_{a_k, j} + h_{a_k} - h_j\),就等于原先的距离加上 \(h_i - h_j\),因此大小关系保持不变。并且换回原来的距离只需要减去这个数即可。
由 \(h_j \ge w_{i, j} + h_i\) 得到 \(w_{i, j} + h_i - h_j\) 一定大于 \(0\)。因此可以使用 dijkstra 求出最短路。
int n,m;
int dep[3010];int dis[3010];
vector<pii> vt[3010];
void bf(){
f(i,1,n)dep[i]=inf;
f(i,1,n+1){
bool change=0;
f(j,0,n)for(pii k:vt[j]){
if(dep[k.first] > dep[j] + k.second){
dep[k.first] = dep[j] + k.second;
change=1;
}
}
if(!change)break;
if(i==n+1){cout<<-1<<endl;exit(0);}
}
f(j,1,n)for(pii &k:vt[j]){
k.second=k.second+dep[j]-dep[k.first];
}
}
bool vis[3010];
void js(int s){
f(i,1,n)dis[i]=inf,vis[i]=0;
priority_queue<pii>q; q.push({0,s});
while(!q.empty()){
cerr<<q.top().first<<" "<<q.top().second<<endl;
pii now=q.top();q.pop();
if(vis[now.second])continue;
vis[now.second]=1;dis[now.second]=-now.first;
for(pii i:vt[now.second]){
assert(i.second >= 0);
if(!vis[i.first]){
q.push({-dis[now.second]-i.second,i.first});
}
}
}return;
int ans=0;
f(i,1,n){
if(dis[i]==inf)ans+=i*(int)1e9;
else ans+=i*(dis[i]+dep[i]-dep[s]);
}
cout<<ans<<endl;
}
signed main() {
cin>>n>>m;
f(i,1,m){int u,v,w;cin>>u>>v>>w;
vt[u].push_back({v,w});}
f(i,1,n)vt[0].push_back({i,0});
bf();
f(i,1,n)js(i);
return 0;
}