最短路

1 单源最短路径

1.1 dijkstra

使用“堆优化的”dijkstra 算法,每次对刚刚加入的点进行一次拓展,然后找出 dis 里面最大的一个。

过程:重复这些操作:

  1. \(S\) 集合中,选取一个最短路长度最小的结点,移到 \(T\) 集合中。
  2. 对那些刚刚被加入 \(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\) 是最佳选项。

然后考虑能不能推到每一条边上(也就是是否有结论:答案一定等于某条边的边权乘以某个数)。考虑这张图:

img

假设最后走了若干条边 \(b_1,b_2,..,b_k\)。那么规约到上个引理。因此我们最后只会走一条边。假设这条边的边权是 \(t\)

考虑它和其他边合并的过程。如果遇到比它大的边,那么使用这条边权还不如使用自己。否则,可以直接继续用小边,不会用到自己。

因此我们证明了,答案一定等于某条边的边权乘以某个数。

依然考虑上述图,是做了七次操作,如下图:

image-20221229200607801

发现是先将一个节点移到一条 \(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\) 组成的边花费的最小代价。


考虑正解:image

这么一个流程,我们怎么实现能够不退化地完成这个 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;
}

posted @ 2022-07-18 10:22  OIer某罗  阅读(97)  评论(0编辑  收藏  举报