【好题选讲】图论阴间题选讲 I

更多知识

【好题选讲】图论阴间题选讲 II(同余最短路,线段树优化建图及其它杂题)

P1462 通往奥格瑞玛的道路

题目描述
给定一个 \(n\) 个城市、\(m\) 条双向道路的图,每个城市有过路费 \(f_i\),每条道路通行会损失血量 \(c_i\)。从城市 \(1\) 出发前往城市 \(n\),初始血量为 \(b\)。求所有可行路径中,路径上城市收费最大值的最小值。若无法到达则输出 AFK

数据范围

  • 城市数 \(1 \leq n \leq 10^4\)
  • 道路数 \(1 \leq m \leq 5 \times 10^4\)
  • 血量 \(1 \leq b \leq 10^9\)
  • 过路费 \(0 \leq f_i \leq 10^9\)
  • 血量损失 \(1 \leq c_i \leq 10^9\)

Dijstra 算法可以在维护过程中动态的决定各点能否参与,于是二分经过的点权的最大值,跑 Dijstra 即可.时间复杂度 \(O ( m \log m \log k )\)

code

Show me the code
#define psb push_back
#define mkp make_pair
#define rep(i,a,b) for( int i=(a); i<=(b); ++i)
#define per(i,a,b) for( int i=(a); i>=(b); --i)
#define rd read()
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll read(){
  ll x=0,f=1;
  char c=getchar();
  while(c>'9'||c<'0'){if(c=='-') f=-1;c=getchar();}
  while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
  return x*f;
}
const int N=1e4+4;
ll nw[N];
struct e{
  ll v,w;
  bool operator<(const e &x) const{
    return x.w<w;
  }
};
ll n,m,b;
vector<e> edge[N];
ll dist[N];
bool vis[N];
bool check(ll maxf){
  if(nw[1]>maxf)return 0;
  memset(dist,0x7f,sizeof dist);
  memset(vis,0,sizeof vis);
  ll init=dist[0];
  priority_queue<e> q;
  q.push(e{1,0});
  dist[1]=0;
  while(q.size()){
    int u=q.top().v;
    ll w=q.top().w;
    q.pop();
    if(vis[u])continue;
    dist[u]=w;
    vis[u]=1;
    for(int i=0;i<edge[u].size();i++){
      int v=edge[u][i].v;
      ll w=edge[u][i].w;
      if(nw[v]>maxf)continue;
      if(dist[v]>dist[u]+w&&dist[u]+w<=b){
        dist[v]=dist[u]+w;
        q.push(e{v,dist[v]});
      }
    }
  }
  if(dist[n]<=b)return 1;
  else return 0;
}
int main(){
  
  cin>>n>>m>>b;
  ll htt=0;
  for(int i=1;i<=n;i++){
    cin>>nw[i];
    htt=max(htt,nw[i]);
  }
  for(int i=1;i<=m;i++){
    ll u,v,w;
    cin>>u>>v>>w;
    edge[u].push_back(e{v,w});
    edge[v].push_back(e{u,w});
  }
  
  for(int i=0;i<=n;i++)dist[i]=1e16;
  memset(vis,0,sizeof vis);
  ll init=dist[0];
  priority_queue<e> q;
  q.push(e{1,0});
  dist[1]=0;
  while(q.size()){
    int u=q.top().v;
    ll w=q.top().w;
    q.pop();
    if(vis[u])continue;
    dist[u]=w;
    vis[u]=1;
    for(int i=0;i<edge[u].size();i++){
      int v=edge[u][i].v;
      ll w=edge[u][i].w;
      if(dist[v]>dist[u]+w){
        dist[v]=dist[u]+w;
        q.push(e{v,w});
      }
    }
  }
  ll l=0,r=1e9;
  while(l<r){
    ll mid=l+r>>1;
    if(check(mid)){
      r=mid;
    }
    else l=mid+1;
  }
  if(l==r&&r==(int)1e9)cout<<"AFK";
  else cout<<l;

  return 0;
}

CF1693C Keshi in Search of AmShZ

题目描述
给定一个包含 \(n\) 个城市和 \(m\) 条有向道路的图。Keshi 初始位于城市 \(1\),目标到达城市 \(n\)。每天 AmShZ 可以执行以下操作之一:

  1. 封锁一条道路,之后 Keshi 将无法使用该道路
  2. 让 Keshi 随机选择一条未被封锁的出边移动

求保证 Keshi 能在最少天数 \(d\) 内到达城市 \(n\) 的最小 \(d\) 值。

数据范围

  • 城市数 \(2 \leq n \leq 2 \times 10^5\)
  • 道路数 \(1 \leq m \leq 2 \times 10^5\)
  • 保证存在从 \(1\)\(n\) 的路径

这个题体现了 Dijstra 可以当 DP 用的性质。

首先这题题面就非常的 DP。题目要求 \(1\)\(n\) 的最小时间,但由于小 K 是随机移动的。我们必须要给 Keshi 指出要向哪条路径走,也就是要在一点把其它出边花时间都断掉,只留我们决策要去的点的路径。

考虑一个从起点向终点转移的 DP,\(f_i\) 表示从起点到 \(i\) 所需时间的最小值,转移时枚举各出边的点,加上封住其它出边与移动所需的代价,转移方程是 \(v \in \operatorname{out}_u ,f_v=max\{f_v,f_u+|\operatorname{out}_u|-1 +1\}\)。但不要忘了我们在图上,好消息是我们的转移没有顺序,坏消息是图上会有环,会影响答案吗?

其实是不会的,你若把 \(f_i\) 看作一种距离,\(|\operatorname{out}_u|-1 +1\) 的值看作边权,你会发现边权是非负的,于是环不会有影响。

更进一步的,我们其实可以直接在 Dijstra 算法中使用这样的距离计算方式优化时间复杂度,原因是原式对转移顺序没有任何要求。

总结一下,若最优化 DP 没有转移顺序,转移时变化量已知、恒定且非负,此时可以应用 Dijstra 算法。

code

Show me the code


P5304 旅行者

题目描述
给定一个包含 \(n\) 个城市和 \(m\) 条有向边的图,边权为正整数。从给定的 \(k\) 个关键城市中,求所有关键城市对之间的最短路径的最小值(即找出最近的关键城市对的最短距离)。

数据范围

  • 数据组数 \(T \leq 5\)
  • 城市数 \(n \leq 10^5\)
  • 边数 \(m \leq 5 \times 10^5\)
  • 关键城市数 \(2 \leq k \leq n\)
  • 边权 \(1 \leq z \leq 2 \times 10^9\)

计算两两间最短路似乎是个很逆天的任务。

首先来思考一下 Brute Force,也就是 \(k\) 遍单源最短路,每次虽然可以精确地找出最短路,但效率还是太低下了,别忘了我们还有多源多汇最短路的科技。我们能否将 \(t\) 个点分成两个集合,求两集合间的最短路。若最短路最小的两点可以被分到不同的集合中,则最短路的长必然是我们分的一组或多组集合中最短路的最小值。

要分成两组,我觉得你应想到二进制拆位了。我们枚举二进制里的第 \(i\) 位,把 \(i\) 位为 \(0\) 的点放进一个集合里,把 \(i\) 位为 \(1\) 的点放进一个集合里。由于最短路最小的两点不可能相同,即它们一定有一次被分到了不同的集合里,我们把所有最短路取最小值即得答案。

很好玩的小技巧!

code

Show me the code


ABC232G Modulo Shortest Path

题目描述

给定一个包含 \(N\) 个节点的有向完全图(除自环外),再给你两个长 \(N\) 的序列 \(A,B\),每条边 \(i \to j\) 的权值为 \((A_i + B_j) \bmod M\)。求从节点 \(1\) 到节点 \(N\) 的最短路径长度。

数据范围

- \(2 \leq N \leq 2 \times 10^5\)

- \(2 \leq M \leq 10^9\)

- \(0 \leq A_i, B_j < M\)

依然是一个很好玩的题。

首先让我们想想如果没有取模操作怎么办。

原图是个完全图,意思是边很多,这样我们有两种处理方式,一是减边,二是优化建图。在最小生成树章节中我们用了不少减边操作,这是因为最小生成树仅需要边小即可,但是最短路可不一定只走边较小的一些边,于是我们优化建图,一般是类似网络流的建虚点的方式优化建图。

回到上面的题目,因为点之间的边权由两个数之和决定,我们新建一个中转节点 \(T\),再给每个点分入点和出点。对于入点 \(i\),向 \(T\) 连接 \(A_i\) 边权的有向边,对于出点 \(i\),从 \(T\)\(i\) 连一条 \(B_i\) 的有向边,出点向入点连边权 \(0\) 的有向边。

于是你就可以在这个图上跑最短路了。

现在考虑加上取模怎么做,发现题目中 \(A_i,B_i<M\),也就是说取模生效时只有一种情况就是 \(M\le A_i+B_i<2\times M\),此时的边权是 \(A_i+B_i -M\) 这告诉我们取模的状态数是有限的。

为了方便,我们给每个点根据 \(B_i\) 的大小排序,我们枚举 \(A_i\),你会发现在此时 \(B_i\) 中一定会有前一部分的点对应边权为 \(A_i+B_i\),后一部分的点对应边权为 \(A_i+B_i -M\)

但是现在还能简单的用两个中转节点吗?因为每个点可能的前缀和后缀不一定相同,此时全都压进一个点显然是错误的。

既然提到了边权的情况有前缀后缀的特点,我们就建前缀、后缀中转节点。定义前缀节点 \(i\) 表示中转 \(1\sim i\) 的节点,定义后缀节点 \(j\) 表示中转 \(j \sim N\) 的节点。由定义,对于前缀节点 \(i,i+1\) 要从 \(i+1\)\(i\) 边权为 \(0\) 的有向边,对于后缀节点 \(j,j+1\) 要从 \(j\)\(j+1\) 边权为 \(0\) 的有向边。

这样,我们可以二分找到分界点 \(k\),给 \(k\) 的前缀节点连接边权为 \(A_i\) 的边,给 \(k\) 的后缀节点连接边权为 \(A_i-M\) 的边,对于所有前缀后缀节点,都向他们对应的点 \(i\) 连边权为 \(B_i\) 的边。

最后让前缀、后缀点对应的原点都连上一个 \(B_i\) 的边。

于是这个精妙的优化的图就被我们重建出来了。这个图上的边数有限,但是两点之间的路径经过的边权与原来的完全图是完全等价的。

但是还没完,有个边权是:

\[A_i-M \]

负边权!

SPFA 肯定是不行的,我们想想怎么把这个负边权等价掉。

首先因为我们已经把 \(B_i\) 按权排序了,于是 \(B_{i+1}-B_i > 0\) 是显然,然后如果我们把后缀点之间的边这样连,在后缀点上\(i\)\(j\) 的边权之和就是 \(B_j-B_i\)。因为是向后缀点连边,我们还有 \(A_i+B_k-M > 0\),若我们把从原点 \(i\)\(k\) 的后缀点连的边权给到 \(A_i+B_k-M\)

此时我们从任意后缀点到对应原点的边权就可以给到 \(0\) 了。前缀点不用变。

你会发现这样路径上的边权之和正好是 \(A_i+B_j -M\) ,而且图上没有负权边,于是这题就做完了!

code

Show me the code
#define psb push_back
#define mkp make_pair
#define rep(i,a,b) for( int i=(a); i<=(b); ++i)
#define per(i,a,b) for( int i=(a); i>=(b); --i)
#define rd read()
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll read(){
  ll x=0,f=1;
  char c=getchar();
  while(c>'9'||c<'0'){if(c=='-') f=-1;c=getchar();}
  while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
  return x*f;
}
const int N=2e5+5;
struct work{
  ll a,b;int id;
}wk[N];
int pre[N],suc[N];
struct e{
  ll v,w;
  bool operator<(const e &x )const{return x.w<w;}
};
vector<e> edge[N*6];
int reff[N];
bool cmp(work x,work y){return x.b<y.b;}
bool cmp1(work x,work y){return x.b<y.b;}
ll dist[N*6];
bool vis[N*6];
int main(){
  
  ll n,m;
  cin>>n>>m;
  for(int i=1;i<=n;i++)cin>>wk[i].a;
  for(int i=1;i<=n;i++){cin>>wk[i].b;wk[i].id=i;}
  for(int i=1;i<=n;i++)pre[i]=n+i;  
  for(int i=1;i<=n;i++)suc[i]=2*n+i;
  sort(wk+1,wk+1+n,cmp);
  int s=0,t=0;
  for(int i=1;i<=n;i++){
    if(i<n)edge[suc[i]].push_back(e{suc[i+1],wk[i+1].b-wk[i].b});
    edge[suc[i]].push_back(e{i,0});
    if(wk[i].id==1)s=i;
    if(wk[i].id==n)t=i;
  }
  for(int i=n;i>=1;i--){
    if(i>1)edge[pre[i]].push_back(e{pre[i-1],0});
    edge[pre[i]].push_back(e{i,wk[i].b});
  }
  for(int i=1;i<=n;i++){
    int wp=lower_bound(wk+1,wk+1+n,work{0,m-wk[i].a},cmp)-wk;  
    if(wp==1+n)edge[i].push_back(e{pre[n],wk[i].a});
    else{
    	edge[i].push_back(e{suc[wp],wk[i].a+wk[wp].b-m});
    	edge[i].push_back(e{pre[wp],wk[i].a});		
    }
  }
  for(int i=0;i<=N*6-13;i++)dist[i]=1e16;
  priority_queue<e> aq;
	dist[s]=0;
	aq.push((e){s,0});
	while(aq.size()){
		ll u=aq.top().v,w=aq.top().w;
		aq.pop();
		if(vis[u])continue;
		vis[u]=1;
		dist[u]=w;
		for(int i=0;i<edge[u].size();i++){
			if(dist[u]+edge[u][i].w<dist[edge[u][i].v]){
			    if(!vis[edge[u][i].v])
					aq.push((e){edge[u][i].v,dist[u]+edge[u][i].w});
			} 
		}
	}
  cout<<dist[t];
  
  return 0;
}

P4926 倍杀测量者

题目描述
给定 \(n\) 位选手的分数关系:

  • 当选手 \(A\) 的分数 \(S_A\) 满足 \(S_A \geq k \cdot S_B\) 时,称 \(A\)\(k\) 倍杀 \(B\)
  • 当选手 \(A\) 的分数 \(S_A\) 满足 \(S_A \geq (k - T) \cdot S_B\) 时,触发第一类 Flag
  • 当选手 \(A\) 的分数 \(S_A\) 满足 \(S_A \geq (k + T) \cdot S_B\) 时,触发第二类 Flag

给定部分选手的分数和两类 Flag 约束,求保证至少一个 Flag 被触发的最大实数 \(T\)

数据范围

  • 选手数 \(1 \leq n \leq 1000\)
  • Flag 数 \(1 \leq s \leq 1000\)
  • 已知分数数 \(1 \leq t \leq n\)
  • 倍杀系数 \(1 \leq k \leq 10\)
  • 分数值 \(1 \leq x \leq 10^9\)
  • 要求输出精度绝对误差 \(\leq 10^{-4}\)

这么多不等关系我觉得你应该想到差分约束了,但是我们只会加法的差分约束,这里可是乘法。

还记得有个函数叫对数函数吗?

\[\log(A\times B)= \log A+\log B \]

于是我们可以用 \(\log\) 来把乘法变成加法。

我们实数二分 \(T\),每次差分约束判断有无解,由于 \(n\le 1000\) 而且输出精度要求不高所以是随便跑的。

code

Show me the code


P5590 赛车游戏

题目描述
给定一个包含 \(n\) 个点和 \(m\) 条有向边的图。需要为每条边分配一个 \(1\)\(9\) 的整数权值,使得所有从点 \(1\) 到点 \(n\) 的路径长度相同。如果无解则输出 \(-1\)

数据范围

  • 点数 \(n \leq 1000\)
  • 边数 \(m \leq 2000\)
  • 保证图中无重边和自环
  • 需要输出原始输入的所有边并按相同顺序给出赋权方案

输出要求
若存在解,需按照输入边的顺序输出每条边分配的 \(1 \sim 9\) 的权值。

首先如果图中有个环肯定就无解了,然后给每个边分配边权来让每条边都相等是困难的,但是给边分配边权来让路径的值等于一个给定值是简单的。

为什么呢?我们有个结论:

对于有向带权图 \(G = \left( V, E \right)\),定义:

  • \(f_i\) 为从节点1到节点i的最短路径长度
  • \(w\left( i,j \right)\) 为边 \(e = \left( i,j \right)\) 的权值
  • 假设图中每条边都属于至少一条从 \(1\)\(n\) 的路径

那么以下两个条件互为充要条件:

  1. 对所有边 \(e = \left( i,j \right) \in E\),满足 \(f_i + w\left( i,j \right) = f_j\)
  2. 从节点1到节点n的所有可能路径长度相等。
证明 by deepseek 充分性

假设:所有边满足 \(f_i + w\left( i,j \right) = f_j\)

证明

  • 任取一条从1到n的路径:\(1 \to u_1 \to u_2 \to \cdots \to u_k \to n\)
  • 路径总权值为:

    \[\sum_{i=1}^{k} w\left( u_{i-1}, u_i \right) = \sum_{i=1}^{k} \left( f_{u_i} - f_{u_{i-1}} \right) = f_n - f_1 = f_n \]

    (因为 \(f_1 = 0\)
  • 因此所有路径长度都等于 \(f_n\),证毕

必要性:

假设:所有 \(1\)\(n\) 的路径长度相等

证明(反证法):

  1. 假设存在某边 \(e = \left( i,j \right)\) 使得 \(f_i + w\left( i,j \right) > f_j\)
    • 则存在不经过e的更短路径到达j,与"所有路径长度相等"矛盾
  2. 假设存在某边 \(e = \left( i,j \right)\) 使得 \(f_i + w\left( i,j \right) < f_j\)
    • 则可更新 \(f_j\) 为更小值,与最短路径定义矛盾
  3. 因此必须对所有边满足 \(f_i + w\left( i,j \right) = f_j\)

综上命题得证。

\(\text{}\)

因此,我们让 \(w(i,j)=f_j-f_i\),因为边权只能是 \(1 \sim 9\),于是 \(1\le f_j-f_i \le 9\)。我们就可以得到两个差分约束关系,据此建图求出所有 \(f\),然后输出边权即可。

P6175 无向图的最小环问题

题目描述
给定一个无向图,求图中至少包含 3 个不同节点的环,且环上边的长度之和最小。如果不存在这样的环,输出 No solution.

数据范围

  • 节点数 \(1 \leq n \leq 100\)
  • 边数 \(1 \leq m \leq 5000\)
  • 边长度 \(1 \leq d \leq 10^5\)

输出要求
输出最小环的边权和,若无解则输出 No solution.(注意包含句号)

Floyd 经典题,考虑一个环是怎么组成的。

Floyd 按照顺序尝试向最短路中加入节点,因此当我们尝试向 \(i,j\) 的最短路加入点 \(k\) 时,只有 \(1\sim k\) 的点可能出现在最短路中。此时我们尝试枚举 \(k+1\sim n\) 的点,若此点 \(u\)\(i,j\) 邻接,则此时的 \((i,j)\)\(e(i,u)\)\(e(u,j)\) 可以构成一个环,然后这么枚举就行。

这里可以预处理出两点对之间由共同临界着哪些点。然后这样枚举可以有更好的复杂度。

code

Show me the code
#define psb push_back
#define mkp make_pair
#define rep(i,a,b) for( int i=(a); i<=(b); ++i)
#define per(i,a,b) for( int i=(a); i>=(b); --i)
#define rd read()
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll read(){
  ll x=0,f=1;
  char c=getchar();
  while(c>'9'||c<'0'){if(c=='-') f=-1;c=getchar();}
  while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
  return x*f;
}
const int N=200;
vector<int> edge[N];  
int dp[N][N];
int dist[N][N];
map<pair<int,int>,vector<int> > mp;
int main(){
  
  int n,m;
  cin>>n>>m;
  memset(dp,0x3f,sizeof dp);
  memset(dist,0x3f,sizeof dist);
  for(int i=1;i<=m;i++){
    int u,v,w;
    cin>>u>>v>>w;
    dp[u][v]=min(dp[u][v],w);dp[v][u]=min(dp[v][u],w);
    dist[u][v]=min(dist[u][v],w);dist[v][u]=min(dist[v][u],w);
    edge[u].push_back(v),edge[v].push_back(u);
  }
  for(int i=1;i<=n;i++){
    for(int j=0;j<edge[i].size();j++){
      for(int k=j+1;k<edge[i].size();k++){
        int c=i;
        int u=edge[i][j];int v=edge[i][k];
        if(u>v)swap(u,v);
        mp[mkp(u,v)].push_back(c);
      }
    }
  }
  int ans=0x3f3f3f3f;
  for(int k=1;k<=n;k++){
    for(int i=1;i<=n;i++){
      for(int j=1;j<=n;j++){
      	if(i==j)continue;
        dp[i][j]=min(dp[i][j],dp[i][k]+dp[k][j]);
        int u=i,v=j;
        if(u>j)swap(u,v);
        vector<int> hi=mp[mkp(u,v)];
        for(int o=0;o<hi.size();o++){
          int cjj=hi[o];
          if(cjj<=k)continue;
          ans=min(ans,dp[u][v]+dist[u][cjj]+dist[v][cjj]);
        }
      }
    }
  }
  if(ans==0x3f3f3f3f)cout<<"No solution.";
  else cout<<ans;

  return 0;
}

P2966 [USACO09DEC] Cow Toll Paths G

题目描述
给定一个无向图,包含 \(n\) 个点和 \(m\) 条边,每条边有权值 \(w_i\),每个点有权值 \(c_i\)。对于 \(q\) 次询问,每次给出起点 \(s_i\) 和终点 \(t_i\),求从 \(s_i\)\(t_i\) 的路径中,边权之和加上路径上点权最大值的总和的最小值。

数据范围

  • 点数 \(1 \leq n \leq 250\)
  • 边数 \(1 \leq m \leq 10^4\)
  • 询问数 \(1 \leq q \leq 10^4\)
  • 边权 \(1 \leq w_i \leq 10^5\)
  • 点权 \(1 \leq c_i \leq 10^5\)
  • 保证图中无自环,但可能有重边

输出要求
对于每个询问,输出所求的最小总代价。

还是考虑 Floyd 按照顺序尝试向最短路中加入节点的性质,我们把点按照点权排序并重新映射,按照排序的顺序加入点,此时两点之间的最短路上点权最大的点是两端点和新加入的这个点的其中之一,注意不能忘了两个端点。

注意把最短路和答案分开存,Floyd 的时候更新答案即可。

另外要细心映射,该用映射的和不用的地方要分清。

code

Show me the code
#define psb push_back
#define mkp make_pair
#define rep(i,a,b) for( int i=(a); i<=(b); ++i)
#define per(i,a,b) for( int i=(a); i>=(b); --i)
#define rd read()
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define int long long
ll read(){
  ll x=0,f=1;
  char c=getchar();
  while(c>'9'||c<'0'){if(c=='-') f=-1;c=getchar();}
  while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
  return x*f;
}
const int N=300;
struct work{
  int nw;
  int id;
}ni[N];
int dp[N][N];
int dist[N][N];
int ans[N][N];
int reff[N];
bool cmp(work x,work y){return x.nw<y.nw;}
signed main(){

  int n,m,q;
  cin>>n>>m>>q;
  for(int i=0;i<=273;i++){
    for(int j=0;j<=273;j++){dp[i][j]=1e16;ans[i][j]=1e16;dist[i][j]=1e16;}
  }
  for(int i=1;i<=n;i++){cin>>ni[i].nw;ni[i].id=i;}
  sort(ni+1,ni+1+n,cmp);
  for(int i=1;i<=n;i++){reff[ni[i].id]=i;}  
  for(int i=1;i<=m;i++){
    int u,v,w;
    cin>>u>>v>>w;
    u=reff[u];v=reff[v];
    dp[u][v]=min(dp[u][v],w);
    dp[v][u]=min(dp[v][u],w);
    dist[u][v]=min(dist[u][v],w);
    dist[v][u]=min(dist[v][u],w);
  }
  for(int k=1;k<=n;k++){
    for(int i=1;i<=n;i++){
      for(int j=1;j<=n;j++){
        int pre=dp[i][j];
        dp[i][j]=min(dp[i][j],dp[i][k]+dp[k][j]);
        ans[i][j]=min(ans[i][j],dp[i][j]+max(ni[i].nw,max(ni[k].nw,ni[j].nw)));
      }
    }
  } 
  for(int i=1;i<=q;i++){
    int u,v;
    cin>>u>>v;
    u=reff[u];v=reff[v];
    cout<<ans[u][v]<<'\n';
  }
  
  return 0;
}

P4306 [JSOI2010] 连通数

题目描述
给定一个 \(N\) 个顶点的有向图,计算图中所有可达顶点对的数量(包括每个顶点到自身)。图的邻接关系由 \(N \times N\) 的 0/1 矩阵表示。

数据范围

  • 顶点数 \(1 \leq N \leq 2000\)
  • 邻接矩阵中 '1' 表示存在有向边,'0' 表示不存在

输出要求
输出图中所有可达顶点对的总数(若顶点 \(i\) 能到达顶点 \(j\),则 \((i,j)\) 算作一个可达对)

首先显然可以缩点然后在 DAG 上 DP,但是 \(N\) 这么小没必要。

我们有图上的传递闭包问题,就是已知一个有向图中任意两点之间是否有连边,要求判断任意两点是否连通。这个也可以用 Floyd。

你可以这样理解就是两点之间有最短路那么他们就是连通的,但是我们只用存状态就可以了,于是用 bool 然后你可以写出这些:

Show me the code
#define psb push_back
#define mkp make_pair
#define rep(i,a,b) for( int i=(a); i<=(b); ++i)
#define per(i,a,b) for( int i=(a); i>=(b); --i)
#define rd read()
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll read(){
  ll x=0,f=1;
  char c=getchar();
  while(c>'9'||c<'0'){if(c=='-') f=-1;c=getchar();}
  while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
  return x*f;
}
const int N=2005;
bool dist[N][N];
int main(){
  
  int n;
  cin>>n;
  for(int i=1;i<=n;i++){
    string s;cin>>s;
    dist[i][i]=1;
    for(int j=0;j<n;j++)
      if(s[j]=='1')
        dist[i][j+1]=1;
  }
  for(int k=1;k<=n;k++)
    for(int i=1;i<=n;i++)
      for(int j=1;j<=n;j++)
        dist[i][j]=dist[i][k]||dist[k][j];
  int cnt=0;
  for(int i=1;i<=n;i++)
    for(int j=1;j<=n;j++)
      if(dist[i][j])cnt++;
  cout<<cnt;
  
  return 0;
}

压行压到亲妈都不认了

但是 \(N\le 2000\),用 bitset 还能优化。

因为 bitset 可以 \(O(\frac{n}{w})\) 的合并两个 \(01\) 串,于是 \(O(\frac{n^3}{w})\) 就可以过了。

具体的,如果 \(i\) 能到 \(k\),那么 \(i\) 也可以到 \(k\) 能到的所有点,枚举、判断并取或即可。

code

Show me the code
#define psb push_back
#define mkp make_pair
#define rep(i,a,b) for( int i=(a); i<=(b); ++i)
#define per(i,a,b) for( int i=(a); i>=(b); --i)
#define rd read()
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll read(){
  ll x=0,f=1;
  char c=getchar();
  while(c>'9'||c<'0'){if(c=='-') f=-1;c=getchar();}
  while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
  return x*f;
}
const int N=2005;
bitset<N> dist[N];
int main(){
  
  int n;
  cin>>n;
  for(int i=1;i<=n;i++){
    string s;cin>>s;
    dist[i][i]=1;
    for(int j=0;j<n;j++){
      if(s[j]=='1'){
        dist[i][j+1]=1;
      }
    }
  }
  for(int k=1;k<=n;k++){
    for(int i=1;i<=n;i++){
      if(dist[i][k]) dist[i]=dist[i]|dist[k];
    }
  }
  int cnt=0;
  for(int i=1;i<=n;i++){
    for(int j=1;j<=n;j++){
      if(dist[i][j])cnt++;
    }
  }
  cout<<cnt;
  
  return 0;
}
posted @ 2025-04-29 23:37  hm2ns  阅读(33)  评论(2)    收藏  举报