拆点,边拆点,虚点相关题目
P4568 [JLOI2011] 飞行路线
题目描述
Alice 和 Bob 现在要乘飞机旅行,他们选择了一家相对便宜的航空公司。该航空公司一共在 \(n\) 个城市设有业务,设这些城市分别标记为 \(0\) 到 \(n-1\),一共有 \(m\) 种航线,每种航线连接两个城市,并且航线有一定的价格。
Alice 和 Bob 现在要从一个城市沿着航线到达另一个城市,途中可以进行转机。航空公司对他们这次旅行也推出优惠,他们可以免费在最多 \(k\) 种航线上搭乘飞机。那么 Alice 和 Bob 这次出行最少花费多少?
输入格式
第一行三个整数 \(n,m,k\),分别表示城市数,航线数和免费乘坐次数。
接下来一行两个整数 \(s,t\),分别表示他们出行的起点城市编号和终点城市编号。
接下来 \(m\) 行,每行三个整数 \(a,b,c\),表示存在一种航线,能从城市 \(a\) 到达城市 \(b\),或从城市 \(b\) 到达城市 \(a\),价格为 \(c\)。
输出格式
输出一行一个整数,为最少花费。
输入输出样例 #1
输入 #1
5 6 1
0 4
0 1 5
1 2 5
2 3 5
3 4 5
2 3 3
0 2 100
输出 #1
8
说明/提示
数据规模与约定
对于 \(30\%\) 的数据,\(2 \le n \le 50\),\(1 \le m \le 300\),\(k=0\)。
对于 \(50\%\) 的数据,\(2 \le n \le 600\),\(1 \le m \le 6\times10^3\),\(0 \le k \le 1\)。
对于 \(100\%\) 的数据,\(2 \le n \le 10^4\),\(1 \le m \le 5\times 10^4\),\(0 \le k \le 10\),\(0\le s,t,a,b < n\),\(a\ne b\),\(0\le c\le 10^3\)。
这题是经典的 分层图最短路 / DP + Dijkstra。
1. 题意转化
有 \(n\) 个城市,\(m\) 条无向边,每条边有花费。
从 \(s\) 到 \(t\),最多可以选择 \(k\) 条边免费,求最小花费。
因为免费次数最多只有 \(k\le 10\),所以可以把状态多开一维:
其中:
- \(0 \le u < n\)
- \(0 \le j \le k\)
最终答案就是:\(\min_{0 \le j \le k} dp[t][j]\)
因为题目说的是“最多 \(k\) 次”,不是必须用完 \(k\) 次。
2. 状态转移
对于一条边 \((u,v,w)\),有两种选择:
不免费乘坐
花费 \(w\):\(dp[v][j] = \min(dp[v][j], dp[u][j] + w)\)
免费乘坐
前提是 \(j < k\),花费 \(0\):\(dp[v][j+1] = \min(dp[v][j+1], dp[u][j])\)
因为边是无向边,所以从 \(v\) 到 \(u\) 也要做同样转移。
3. 复杂度
状态数:\(n \times (k+1)\)
每个状态最多枚举它的所有出边,所以总转移数大约是:\(O(m(k+1))\)
Dijkstra 堆优化后:\(O(m(k+1)\log(n(k+1)))\)
因为 \(n \le 10^4\),\(m \le 5\times 10^4\),\(k \le 10\),完全可以过。
空间:\(O(nk + m)\)
点击查看代码
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int INT=1e18;
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int n,m,k;
cin>>n>>m>>k;
int s,t;
cin>>s>>t;
vector<vector<pair<int,int>>> q(n+10);
for(int i=1;i<=m;i++)
{
int a,b,c;
cin>>a>>b>>c;
q[a].push_back({b,c});
q[b].push_back({a,c});
}
vector<vector<int>> dp(n+10,vector<int>(k+10,INT));
for(int i=0;i<=k;i++) dp[s][i]=0;
priority_queue<pair<int,pair<int,int>>> w;
w.push({0,{s,0}});
while(w.size())
{
int d=-w.top().first;
int u=w.top().second.first;
int use=w.top().second.second;
w.pop();
if(d>dp[u][use]) continue;
for(auto i:q[u])
{
int a=i.first,b=i.second;
if(dp[a][use]>dp[u][use]+b)
{
dp[a][use]=dp[u][use]+b;
w.push({-dp[a][use],{a,use}});
}
if(use<k&&dp[a][use+1]>dp[u][use])
{
dp[a][use+1]=dp[u][use];
w.push({-dp[a][use+1],{a,use+1}});
}
}
}
int minn=INT;
for(int i=0;i<=k;i++) minn=min(minn,dp[t][i]);
if(minn==INT) cout<<-1;
else cout<<minn;
return 0;
}
AT_abc277_e [ABC277E] Crystal Switches
题目描述
【题面翻译】
给定一张 \(n\) 个点 \(m\) 条边的无向图。每条边有一个权值 \(w \in \{0, 1\}\)。\(w = 0\) 表示这条边无法通过,\(w = 1\) 则可以通过。
有 \(k\) 个点上面有按钮 \(s_i\)。
你现在位于 \(1\) 号点。每次,你可以做两件事情中的一件:
- 移动。移到相邻的一个点上,注意这条边一定是可以通行的。
- 按开关。此时,全部路的边权取反。即:\(w = 0\) 变成 \(1\),\(w = 1\) 变成 \(0\)。
请问你是否能够到达 \(n\) 号点。如果可以,求出最少移动次数。
输入格式
第一行三个数 \(n, m, k\)。
接下来 \(m\) 行,每行三个数 \(u_i, v_i, w_i\)表示一条连接 \(u_i\) 与 \(v_i\) 的边。
最后一行 \(k\) 个数,表示按钮的位置。
输出格式
如果无法到达,输出 \(-1\)。否则输出最少移动次数。
输入输出样例 #1
输入 #1
5 5 2
1 3 0
2 3 1
5 4 1
2 1 1
1 4 0
3 4
输出 #1
5
输入输出样例 #2
输入 #2
4 4 2
4 3 0
1 2 1
1 2 0
2 1 1
2 4
输出 #2
-1
说明/提示
\(2 \le n \le 2 \times 10^5\)
\(1 \le m \le 2 \times 10^5\)
\(1 \le k \le n\)
保证 \(1 \le u_i, v_i \le n\),且 \(u_i \ne v_i\)。
保证 \(1 \le s_1 < s_2 < \cdots < s_k \le n\)。
这题核心是:拆点 + 0-1 BFS。
1. 状态设计
每个点有两种状态:
st = 0:当前没有反转,边权保持原样。st = 1:当前已经反转了奇数次,边权全部取反。
所以一个城市 v 拆成两个点:
id(v, 0) = v * 2 + 0
id(v, 1) = v * 2 + 1
起点是 id(1, 0),因为一开始没有按过开关。
终点是 n,但可能在状态 0 或状态 1 到达,所以答案是:
min(dist[id(n,0)], dist[id(n,1)])
2. 建图规则
移动一次,花费 1 次操作。
按开关,不花费移动次数,所以边权是 0。
原始边 w = 1
这条边本来就能走。
所以在状态 0 下可以走:
id(u,0) <-> id(v,0) 边权 1
但在状态 1 下,因为边权取反,它变成不能走,所以不连。
原始边 w = 0
这条边本来不能走。
但在状态 1 下,取反后变成能走:
id(u,1) <-> id(v,1) 边权 1
状态 0 下不能走,所以不连。
按钮点
在按钮点 s,可以按开关,状态在 0 和 1 之间切换,不消耗移动次数:
id(s,0) <-> id(s,1) 边权 0
3. 为什么用 0-1 BFS
建完图后,所有边权只有:
- 移动边:权值
1 - 开关边:权值
0
求最短路,边权只有 0 和 1,所以用 0-1 BFS。
实现方式:
- 用双端队列
deque - 遇到边权
0,从队头入队:push_front - 遇到边权
1,从队尾入队:push_back
这样能保证队列里距离单调,等价于 Dijkstra,但更快。
4. 复杂度
点数:2n
边数:最多 2m + 2k
0-1 BFS 复杂度:\(O(n + m + k)\)
空间:\(O(n + m + k)\)
可以轻松通过 n, m <= 2e5。
点击查看代码
#include <bits/stdc++.h>
#define int long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=2e5+5;
const int mod=998244353;
const int inf=1e18;
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int n,m,k;
cin>>n>>m>>k;
vector<vector<pair<int,int>>>g(2*n+5);
auto id=[&](int v,int st){return v*2+st;};
for(int i=0;i<m;i++)
{
int u,v,a;
cin>>u>>v>>a;
if(a==1)
{
g[id(u,0)].push_back({id(v,0),1});
g[id(v,0)].push_back({id(u,0),1});
}
else
{
g[id(u,1)].push_back({id(v,1),1});
g[id(v,1)].push_back({id(u,1),1});
}
}
for(int i=0;i<k;i++)
{
int s;
cin>>s;
g[id(s,0)].push_back({id(s,1),0});
g[id(s,1)].push_back({id(s,0),0});
}
vector<int>dist(2*n+5,inf);
deque<int>dq;
int start=id(1,0);
dist[start]=0;
dq.push_front(start);
while(!dq.empty())
{
int u=dq.front();
dq.pop_front();
for(auto e:g[u])
{
int v=e.fi;
int w=e.se;
if(dist[v]>dist[u]+w)
{
dist[v]=dist[u]+w;
if(w==0)dq.push_front(v);
else dq.push_back(v);
}
}
}
int ans=min(dist[id(n,0)],dist[id(n,1)]);
if(ans>=inf)cout<<-1<<"\n";
else cout<<ans<<"\n";
return 0;
}
CF1473E Minimum Path
题目描述
给定一个包含 \(n\) 个顶点和 \(m\) 条边的无向连通带权图。保证图中没有自环和重边。
我们定义一条由 \(k\) 条编号为 \(e_1, e_2, \dots, e_k\) 的边组成的路径的权值为 \(\sum\limits_{i=1}^{k}{w_{e_i}} - \max\limits_{i=1}^{k}{w_{e_i}} + \min\limits_{i=1}^{k}{w_{e_i}}\),其中 \(w_i\) 表示图中第 \(i\) 条边的权值。
你的任务是,对于每个 \(i\)(\(2 \le i \le n\)),求出从第 \(1\) 个顶点到第 \(i\) 个顶点的路径的最小权值。
输入格式
第一行包含两个整数 \(n\) 和 \(m\)(\(2 \le n \le 2 \cdot 10^5\),\(1 \le m \le 2 \cdot 10^5\)),分别表示图中的顶点数和边数。
接下来的 \(m\) 行,每行包含三个整数 \(v_i, u_i, w_i\)(\(1 \le v_i, u_i \le n\),\(1 \le w_i \le 10^9\),\(v_i \neq u_i\)),表示第 \(i\) 条边的两个端点和权值。
输出格式
输出 \(n-1\) 个整数,第 \(i\) 个整数表示从第 \(1\) 个顶点到第 \(i\) 个顶点的最小路径权值(\(2 \le i \le n\))。
输入输出样例 #1
输入 #1
5 4
5 3 4
2 1 1
3 2 2
2 4 2
输出 #1
1 2 2 4
输入输出样例 #2
输入 #2
6 8
3 1 1
3 6 2
5 4 2
4 2 2
6 1 1
5 2 1
3 2 3
1 5 4
输出 #2
2 1 4 3 1
输入输出样例 #3
输入 #3
7 10
7 5 5
2 3 3
4 7 1
5 3 6
2 7 6
6 2 6
3 7 6
4 2 1
3 1 4
1 7 4
输出 #3
3 4 2 7 7 3
这题要求对于每个点 \(i\),求从 \(1\) 到 \(i\) 的路径最小权值,路径权值定义为:
可以改写为:
等价于:在路径中选一条边免费(减去它的权值),再选一条边双倍花费(加上它的权值)。但必须保证免费的是最大边,双倍的是最小边。由于最短路会自然选择最优顺序,我们可以用分层图建模。
状态设计
每个点拆成 \(4\) 层,表示操作的使用情况:
- 层 \(0\):还没有使用任何操作。
- 层 \(1\):已经使用过“免费”操作(减去了一条边的权值)。
- 层 \(2\):已经使用过“双倍”操作(加上了一条边的权值)。
- 层 \(3\):两种操作都已经使用。
用 \(id = u + s \cdot n\) 表示第 \(s\) 层的节点 \(u\),其中 \(s \in \{0,1,2,3\}\),\(u \in [1,n]\)。
起点是节点 \(1\) 的第 \(0\) 层,即 \(id = 1\),距离为 \(0\)。
转移
对于一条边 \((u, v, w)\),从当前状态 \((u, s)\) 出发:
-
同层移动:不改变操作状态,正常走边。
\[(v, s) \leftarrow (u, s) + w \] -
从层 \(0\) 出发:
- 选择当前边免费:进入层 \(1\),花费 \(0\)。\[(v, 1) \leftarrow (u, 0) + 0 \]
- 选择当前边双倍:进入层 \(2\),花费 \(2w\)。\[(v, 2) \leftarrow (u, 0) + 2w \]
- 免费和双倍选同一条边:进入层 \(3\),花费 \(w\)。\[(v, 3) \leftarrow (u, 0) + w \]
- 选择当前边免费:进入层 \(1\),花费 \(0\)。
-
从层 \(1\) 出发(已免费):
- 再选择当前边双倍:进入层 \(3\),花费 \(2w\)。\[(v, 3) \leftarrow (u, 1) + 2w \]
- 再选择当前边双倍:进入层 \(3\),花费 \(2w\)。
-
从层 \(2\) 出发(已双倍):
- 再选择当前边免费:进入层 \(3\),花费 \(0\)。\[(v, 3) \leftarrow (u, 2) + 0 \]
- 再选择当前边免费:进入层 \(3\),花费 \(0\)。
为什么这样是对的
原式要求减去最大边、加上最小边。如果先免费(减去某条边),再双倍(加上某条边),那么得到的是 \(\sum - w_1 + w_2\)。只有当 \(w_1 \ge w_2\) 时,\(w_1\) 才是最大边,\(w_2\) 才是最小边,结果才正确。如果顺序反了,会得到 \(\sum - w_{\text{小}} + w_{\text{大}}\),这个值比真实值大,不会成为最短路。因此最短路会自动选择正确的顺序(先免费大边,再双倍小边),无需额外判断。
最短路算法
所有边权非负(\(w \ge 1\),\(2w \ge 2\),\(0\) 也有),所以用 Dijkstra 求最短路。状态数为 \(4n\),边数约为 \(4m\),复杂度为:
答案
对于每个 \(i \in [2, n]\),到达节点 \(i\) 且两种操作都使用过的状态就是最优的,即输出:
如果不可达,距离为初始的 \(\infty\),但题目保证图连通,所以一定可达。
代码对应
int s = (id - 1) / n; // 当前层数
int u = id - s * n; // 当前节点
id 从 \(1\) 开始,所以 \(id=1\) 对应 \(s=0, u=1\)。
转移中:
nid = v + s*n:同层移动。s==0时:nid1 = v + 1*n,花费d+0:免费。nid2 = v + 2*n,花费d+2*w:双倍。nid3 = v + 3*n,花费d+w:同边免费+双倍。
s==1时:nid3 = v + 3*n,花费d+2*w:再双倍。
s==2时:nid3 = v + 3*n,花费d+0:再免费。
最后输出:
for(int i=2; i<=n; i++) cout << dist[i + 3*n] << " ";
复杂度
- 时间:\(O((n+m)\log n)\)
- 空间:\(O(n+m)\)
可以轻松通过 \(n, m \le 2 \times 10^5\) 的数据。
点击查看代码
#include <bits/stdc++.h>
#define int long long
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=2e5+5;
const int mod=998244353;
const int inf=1e18;
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int n,m;
cin>>n>>m;
vector<vector<pair<int,int>>> g(n+1);
for(int i=0;i<m;i++)
{
int u,v,w;
cin>>u>>v>>w;
g[u].push_back({v,w});
g[v].push_back({u,w});
}
int total=4*n+5;
vector<int> dist(total,inf);
priority_queue<pair<int,int>,vector<pair<int,int>>,greater<pair<int,int>>> pq;
dist[1]=0;
pq.push({0,1});
while(!pq.empty())
{
auto [d,id]=pq.top();
pq.pop();
if(d!=dist[id]) continue;
int s=(id-1)/n;
int u=id-s*n;
for(auto [v,w]:g[u])
{
int nid=v+s*n;
if(dist[nid]>d+w)
{
dist[nid]=d+w;
pq.push({dist[nid],nid});
}
if(s==0)
{
int nid1=v+1*n;
if(dist[nid1]>d+0)
{
dist[nid1]=d+0;
pq.push({dist[nid1],nid1});
}
int nid2=v+2*n;
if(dist[nid2]>d+2*w)
{
dist[nid2]=d+2*w;
pq.push({dist[nid2],nid2});
}
int nid3=v+3*n;
if(dist[nid3]>d+w)
{
dist[nid3]=d+w;
pq.push({dist[nid3],nid3});
}
}
else if(s==1)
{
int nid3=v+3*n;
if(dist[nid3]>d+2*w)
{
dist[nid3]=d+2*w;
pq.push({dist[nid3],nid3});
}
}
else if(s==2)
{
int nid3=v+3*n;
if(dist[nid3]>d+0)
{
dist[nid3]=d+0;
pq.push({dist[nid3],nid3});
}
}
}
}
for(int i=2;i<=n;i++) cout<<dist[i+3*n]<<" ";
return 0;
}
AT_abc164_e [ABC164E] Two Currencies
题目描述
有 \(n\) 个城市,它们由 \(m\) 条双向道路连接,保证它们能够彼此到达。第 \(i\) 条道路连接 \(u_i,v_i\),需要花费 \(x_i\) 个银币,耗费 \(t_i\) 秒的时间。每个城市处都有兑换银币处,第 \(i\) 个城市中你可以用 \(1\) 个金币兑换 \(c_i\) 个银币,可以兑换无限次,不过兑换 \(1\) 次需要花费 \(d_i\) 秒的时间。你一开始在 \(1\) 号城市,有 \(s\) 个银币和无限多的金币,求到其它城市需要耗费的最小时间。
\(1 \leq n \leq 50\),\(n - 1 \le m \le 100\),\(1 \leq x_i \leq 50\),\(1 \leq t_i,d_i \leq 10^9\),\(1 \leq s,c_i \leq 10^9\)
输入格式
- 第一行 \(n,m,s\)
- 接下来 \(m\) 行 \(u_i,v_i,x_i,t_i\)
- 接下来 \(n\) 行 \(c_i,d_i\)
输出格式
输出 \(n - 1\) 行,第 \(i\) 行一个整数表示到第 \(i + 1\) 个城市耗费的最小时间。
输入输出样例 #1
输入 #1
3 2 1
1 2 1 2
1 3 2 4
1 11
1 2
2 5
输出 #1
2
14
输入输出样例 #2
输入 #2
4 4 1
1 2 1 5
1 3 4 4
2 4 2 2
3 4 1 1
3 1
3 1
5 2
6 4
输出 #2
5
5
7
输入输出样例 #3
输入 #3
6 5 1
1 2 1 1
1 3 2 1
2 4 5 1
3 5 11 1
1 6 50 1
1 10000
1 3000
1 700
1 100
1 1
100 1
输出 #3
1
9003
14606
16510
16576
输入输出样例 #4
输入 #4
4 6 1000000000
1 2 50 1
1 3 50 5
1 4 50 7
2 3 50 2
2 4 50 4
3 4 50 3
10 2
4 4
5 5
7 7
输出 #4
1
3
5
输入输出样例 #5
输入 #5
2 1 0
1 2 1 1
1 1000000000
1 1
输出 #5
1000000001
说明/提示
制約
- \(2 \leq\ N \leq 50\)
- \(N-1\ \leq\ M\ \leq\ 100\)
- \(0\ \leq\ S\ \leq\ 10^9\)
- \(1\ \leq\ A_i\ \leq\ 50\)
- \(1\ \leq\ B_i,C_i,D_i\ \leq\ 10^9\)
- \(1\ \leq\ U_i\ <\ V_i\ \leq\ N\)
- \((U_i,V_i)=(U_j,V_j)\) なる \(i,j(i\ \neq\ j)\) は存在しない
- 都市 $ 1 $ から都市 $ t=2,...,N $ にいくつかの鉄道路線を使って移動することができる。
- 入力はすべて整数である。
这题是 带资源的 Dijkstra / 分层最短路。
1. 关键观察:银币上限
虽然 \(s\) 和 \(c_i\) 可以达到 \(10^9\),但真正有用的银币数量并不多。
- 城市数 \(n \le 50\)
- 每条道路最多花费 \(x_i \le 50\) 银币
- 最优路径不会重复经过城市,所以最多走 \(n-1\) 条边
因此,走完一条最优路径最多需要:
取一个稍大的上限:
const int MAXS = 2500;
所有超过 \(2500\) 的银币都可以直接截断到 \(2500\),因为多余的银币不会让答案更优。
2. 状态设计
设:
其中:
- \(1 \le u \le n\)
- \(0 \le j \le MAXS\)
起点是城市 \(1\),初始银币为 \(s\),但截断到 \(MAXS\):
int init_s = min(s, (long long)MAXS);
dist[1][init_s] = 0;
3. 转移
从状态 \((u, j)\) 出发,有两种操作:
1. 兑换银币
在城市 \(u\),可以用 \(1\) 金币换 \(c_u\) 银币,花费 \(d_u\) 时间。
如果 \(j < MAXS\),可以兑换:
代码:
if(silver < MAXS)
{
int ns = min((long long)MAXS, silver + c[u]);
int nt = time + d[u];
if(nt < dist[u][ns])
{
dist[u][ns] = nt;
pq.push({nt, u, ns});
}
}
这里只写了一次兑换。但因为兑换后的状态会再次进入优先队列,如果还需要兑换,会继续从这个新状态再兑换一次,所以可以无限次兑换。
2. 移动
从 \(u\) 到 \(v\),需要花费 \(x\) 银币和 \(t\) 时间。
如果 \(j \ge x\):
代码:
if(silver >= e.x)
{
int ns = silver - e.x;
int nt = time + e.t;
if(nt < dist[e.to][ns])
{
dist[e.to][ns] = nt;
pq.push({nt, e.to, ns});
}
}
4. 为什么用 Dijkstra
所有转移的时间花费都是非负的:
- 兑换时间 \(d_u \ge 1\)
- 移动时间 \(t_i \ge 1\)
所以可以用 Dijkstra 求最短路。状态数是 \(n \times (MAXS+1)\),边数大约是 \(n \times MAXS \times (1 + \text{度数})\),总复杂度可以接受。
5. 最终答案
对于每个城市 \(i \in [2, n]\),答案是:
因为到达城市时,身上有多少银币不重要,取时间最小的即可。
代码:
for(int i = 2; i <= n; i++)
{
int ans = inf;
for(int j = 0; j <= MAXS; j++)
ans = min(ans, dist[i][j]);
cout << ans << "\n";
}
6. 复杂度
- 状态数:\(O(n \cdot MAXS)\)
- 转移数:每个状态最多一次兑换,以及所有出边,所以 \(O(n \cdot MAXS \cdot (1 + m/n))\)
- Dijkstra 堆优化:\(O(n \cdot MAXS \cdot \log(n \cdot MAXS) + m \cdot MAXS \cdot \log(n \cdot MAXS))\)
由于 \(n \le 50\),\(MAXS = 2500\),总状态数约 \(125000\),完全可过。
点击查看代码
#include <bits/stdc++.h>
#define int long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=2e5+5;
const int mod=998244353;
const int inf=1e18;
struct Edge{int to,x,t;};
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int n,m,s;
cin>>n>>m>>s;
vector<vector<Edge>> g(n+1);
for(int i=0;i<m;i++)
{
int u,v,x,t;
cin>>u>>v>>x>>t;
g[u].push_back({v,x,t});
g[v].push_back({u,x,t});
}
vector<int> c(n+1),d(n+1);
for(int i=1;i<=n;i++) cin>>c[i]>>d[i];
const int MAXS=2500;
vector<vector<int>> dist(n+1,vector<int>(MAXS+1,inf));
priority_queue<tuple<int,int,int>,vector<tuple<int,int,int>>,greater<tuple<int,int,int>>> pq;
int init_s=min(s,(long long)MAXS);
dist[1][init_s]=0;
pq.push({0,1,init_s});
while(!pq.empty())
{
auto [time,u,silver]=pq.top();
pq.pop();
if(time!=dist[u][silver])continue;
if(silver<MAXS)
{
int ns=min((long long)MAXS,silver+c[u]);
int nt=time+d[u];
if(nt<dist[u][ns])
{
dist[u][ns]=nt;
pq.push({nt,u,ns});
}
}
for(auto &e:g[u])
{
if(silver>=e.x)
{
int ns=silver-e.x;
int nt=time+e.t;
if(nt<dist[e.to][ns])
{
dist[e.to][ns]=nt;
pq.push({nt,e.to,ns});
}
}
}
}
for(int i=2;i<=n;i++)
{
int ans=inf;
for(int j=0;j<=MAXS;j++) ans=min(ans,dist[i][j]);
cout<<ans<<"\n";
}
return 0;
}
CF1846G Rudolf and CodeVid-23
题目描述
一种名为“CodeVid-23”的新病毒正在程序员中传播。Rudolf 作为一名程序员,也未能幸免。
共有 \(n\) 种症状,编号从 \(1\) 到 \(n\),感染后可能出现。起初,Rudolf 已经有了一些症状。他去药店买了 \(m\) 种药。
对于每种药,已知需要服用的天数,以及它能消除的症状集合。不幸的是,药物通常有副作用。因此,对于每种药,还知道服用时会出现的症状集合。
Rudolf 发现,同时服用多种药物非常不健康。
Rudolf 希望尽快痊愈。因此,他请你计算消除所有症状所需的最少天数,或者判断是否无法治愈。
输入格式
第一行包含一个整数 \(t\) \((1 \le t \le 100)\),表示测试用例的数量。
接下来是每个测试用例的描述。
每个测试用例的第一行包含两个整数 \(n, m\) \((1 \le n \le 10, 1 \le m \le 10^3)\),分别表示症状数和药物数。
第二行包含一个长度为 \(n\) 的仅由 \(0\) 和 \(1\) 组成的字符串,描述 Rudolf 当前的症状。如果第 \(i\) 个字符为 \(1\),则 Rudolf 有第 \(i\) 种症状,否则没有。
接下来有 \(3 \cdot m\) 行,描述每种药物。
每种药物的第一行包含一个整数 \(d\) \((1 \le d \le 10^3)\),表示该药物需要服用的天数。
接下来的两行分别为长度为 \(n\) 的仅由 \(0\) 和 \(1\) 组成的字符串,分别描述该药物能消除的症状和服用后出现的副作用症状。
在第一行中,第 \(i\) 位为 \(1\) 表示该药物能消除第 \(i\) 种症状,否则不能。
在第二行中,第 \(i\) 位为 \(1\) 表示服用该药物后会出现第 \(i\) 种症状,否则不会。
不同的药物可能有相同的消除症状和副作用集合。如果某药物能消除某种症状,则该症状不会出现在副作用中。
所有测试用例中 \(m\) 的总和不超过 \(10^3\)。
输出格式
对于每个测试用例,输出一个整数,表示 Rudolf 消除所有症状所需的最少天数。如果无法消除所有症状,输出 \(-1\)。
输入输出样例 #1
输入 #1
4
5 4
10011
3
10000
00110
3
00101
00000
3
01010
00100
5
11010
00100
4 1
0000
10
1011
0100
2 2
11
2
10
01
3
01
10
2 3
11
3
01
10
3
10
00
4
10
01
输出 #1
8
0
-1
6
说明/提示
在第一个样例中,可以先使用第 \(4\) 种药物,此时症状变为“00101”。然后使用第 \(2\) 种药物,所有症状都消失,总天数为 \(5+3=8\)。另一种方案是依次使用第 \(1,3,2\) 种药物,也能消除所有症状,但总天数为 \(3+3+3=9\)。
在第二个样例中,初始时没有任何症状,因此治疗天数为 \(0\)。
在第三个样例中,没有办法消除所有症状。
这题是 状态压缩 + Dijkstra 最短路。
1. 题目抽象
有 \(n\) 种症状,\(n \le 10\),所以可以用一个二进制数表示当前所有症状:
- 第 \(i\) 位为 \(1\) 表示有第 \(i\) 种症状。
- 第 \(i\) 位为 \(0\) 表示没有第 \(i\) 种症状。
总状态数最多:
初始症状是一个二进制状态 st,目标是状态 0(没有任何症状)。
有 \(m\) 种药,每种药:
- 花费 \(d_i\) 天
- 能消除某些症状,用二进制
rem[i]表示,某位为 \(1\) 表示能消除该症状 - 会产生某些副作用,用二进制
add[i]表示,某位为 \(1\) 表示会出现该症状
服用一种药后,症状变化为:
意思是:
mask & ~rem[i]:把能消除的症状位清零| add[i]:把副作用症状位置为 \(1\)
2. 算法:Dijkstra
把每个症状状态看作图上的一个点,初始状态 st 是起点,状态 0 是终点。
每种药相当于一条边:
- 从当前状态
mask转移到new_mask - 边权是服药天数
d[i]
因为所有边权 \(d_i \ge 1\),是正权图,所以可以用 Dijkstra 求从 st 到 0 的最短路径。
状态数最多 \(1024\),每个状态最多尝试 \(m\) 种药,总边数约:
而所有测试用例的 \(m\) 总和不超过 \(1000\),所以总计算量完全可接受。
3. 代码对应解释
读入并构建初始状态
int st = 0;
for(int i = 0; i < n; i++)
if(s[i] == '1') st |= (1 << i);
把字符串 s 转成二进制掩码。
预处理每种药
vector<int> d(m+1), rem(m+1), add(m+1);
for(int i = 1; i <= m; i++)
{
cin >> d[i];
string a, b;
cin >> a >> b;
rem[i] = add[i] = 0;
for(int j = 0; j < n; j++)
{
if(a[j] == '1') rem[i] |= (1 << j);
if(b[j] == '1') add[i] |= (1 << j);
}
}
rem[i]:药物 \(i\) 能消除的症状集合add[i]:药物 \(i\) 的副作用症状集合
Dijkstra 初始化
int tot = 1 << n;
vector<int> dis(tot, inf);
priority_queue<pair<int,int>, vector<pair<int,int>>, greater<pair<int,int>>> pq;
dis[st] = 0;
pq.push({0, st});
dis[mask] 表示从初始状态到达 mask 所需的最少天数。
Dijkstra 主循环
while(!pq.empty())
{
int co = pq.top().fi, mask = pq.top().se;
pq.pop();
if(co != dis[mask]) continue;
if(mask == 0)
{
ans = co;
break;
}
for(int i = 1; i <= m; i++)
{
int n_mask = (mask & ~rem[i]) | add[i];
int n_co = co + d[i];
if(n_co < dis[n_mask])
{
dis[n_mask] = n_co;
pq.push({n_co, n_mask});
}
}
}
- 取出当前距离最小的状态。
- 如果已经是
0,说明找到了最短路径,直接记录答案并退出。 - 否则枚举所有药物,计算新状态,更新距离。
输出答案
cout << ans << "\n";
如果无法到达 0,ans 保持初始值 -1,输出 -1。
4. 复杂度
- 状态数:\(O(2^n)\),最大 \(1024\)
- 每个状态枚举 \(m\) 种药
- 总边数:\(O(2^n \cdot m)\)
- Dijkstra 堆优化:\(O(2^n \cdot m \cdot \log(2^n))\)
由于 \(n \le 10\),\(m\) 总和 \(\le 1000\),总时间非常小。
空间:\(O(2^n + m)\)。
点击查看代码
#include <bits/stdc++.h>
#define int long long
#define COUT cout<<"\n--------------------------------------------------------------\n"
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=2e5+5;
const int mod=998244353;
const int inf=1e18;
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int T;
cin>>T;
while(T--)
{
int n,m;
cin>>n>>m;
string s;
cin>>s;
int st=0;
for(int i=0;i<n;i++) if(s[i]=='1') st|=(1<<i);
vector<int> d(m+1),rem(m+1),add(m+1);
for(int i=1;i<=m;i++)
{
cin>>d[i];
string a,b;
cin>>a>>b;
rem[i]=add[i]=0;
for(int j=0;j<n;j++)
{
if(a[j]=='1') rem[i]|=(1<<j);
if(b[j]=='1') add[i]|=(1<<j);
}
}
int tot=1<<n;
vector<int> dis(tot,inf);
priority_queue<pair<int,int>,vector<pair<int,int>>,greater<pair<int,int>>> pq;
dis[st]=0;
pq.push({0,st});
int ans=-1;
while(!pq.empty())
{
int co=pq.top().fi,mask=pq.top().se;
pq.pop();
if(co!=dis[mask]) continue;
if(mask==0)
{
ans=co;
break;
}
for(int i=1;i<=m;i++)
{
int n_mask=(mask&~rem[i])|add[i];
int n_co=co+d[i];
if(n_co<dis[n_mask])
{
dis[n_mask]=n_co;
pq.push({n_co,n_mask});
}
}
}
cout<<ans<<"\n";
}
return 0;
}
//cout<<fixed<<setprecision(1);
P2939 [USACO09FEB] Revamping Trails G
题目描述
农夫约翰每天都会认真检查他的奶牛。他会穿越一些编号为 \(1\) 到 \(M\) 的小径(\(1 \leq M \leq 50,000\)),从牧场 \(1\) 一直走到牧场 \(N\)(对于测试数据给出的路径图,这段旅程总是可能的)。农夫约翰的农场上有 \(N\) 个牧场(\(1 \leq N \leq 10,000\)),它们通过双向泥土小径连接在一起。每条小径 \(i\) 连接牧场 \(P1_i\) 和 \(P2_i\)(\(1 \leq P1_i \leq N,1 \leq P2_i \leq N\)),需要 \(T_i\)(\(1 \leq T_i \leq 10^6\))单位时间来穿越。
他想要改造农场上的一些小径,以节省长途旅行的时间。具体来说,他将选择 \(K\)(\(1 \leq K \leq 20\))条小径将其改造成高速公路,这将有效地将小径的穿越时间减少到 \(0\)。帮助 FJ 决定改造哪些小径以最小化从牧场 \(1\) 到 \(N\) 的最终时间。
输入格式
-
第 \(1\) 行:三个用空格分隔的整数:\(N,M\) 和 \(K\)。
-
第 \(2\) 行到第 \(M+1\) 行:第 \(i+1\) 行描述小径 \(i\),包含三个用空格分隔的整数:\(P1_i\)、\(P2_i\) 和 \(T_i\)。
输出格式
共 \(1\) 行:改造不超过 \(K\) 条边后的最短路径长度。
输入输出样例 #1
输入 #1
4 4 1
1 2 10
2 4 10
1 3 1
3 4 100
输出 #1
1
说明/提示
\(K\) 为 \(1\);将小径 \(3 \to 4\) 改造成高速公路,时间从 \(100\) 变为 \(0\)。新的最短路径为 \(1 \to 3 \to 4\),总穿越时间现在为 \(1\)。
这题是经典 分层图最短路,和 P4568 飞行路线几乎一样。
1. 题意
有 \(N\) 个牧场,\(M\) 条双向边,每条边有耗时 \(T_i\)。
最多可以把 \(K\) 条边的耗时变成 \(0\),求从 \(1\) 到 \(N\) 的最小总耗时。
2. 思路:分层图
把图分成 \(K+1\) 层,第 \(j\) 层表示“已经改造了 \(j\) 条边”。
节点编号:
表示第 \(j\) 层的城市 \(u\),其中 \(0 \le j \le K\)。
对于原图每条边 \((u, v, w)\):
- 正常走:同层转移,花费 \(w\)\[u + jn \to v + jn \quad (w) \]
- 改造这条边:进入下一层,花费 \(0\)\[u + jn \to v + (j+1)n \quad (0) \]
因为是双向边,两个方向都要建。
起点是第 \(0\) 层的 \(1\) 号点,即节点 1。
终点是 \(N\) 号点,但可能在任意一层,所以最终答案是:
3. 代码详解
int n, m, k;
cin >> n >> m >> k;
vector<vector<edge>> g(k * n + n + 1);
总节点数 \((K+1) \times N\),数组开 k*n+n+1。
for(int i = 1; i <= m; i++)
{
int u, v, w;
cin >> u >> v >> w;
for(int j = 0; j <= k; j++)
{
g[u + j * n].push_back({v + j * n, w});
g[v + j * n].push_back({u + j * n, w});
if(j < k)
{
g[u + j * n].push_back({v + (j + 1) * n, 0});
g[v + j * n].push_back({u + (j + 1) * n, 0});
}
}
}
- 同层:正常走,权值
w。 - 跨层:改造这条边,权值
0,层数 \(j \to j+1\)。
priority_queue<pair<int,int>> q;
vector<int> dis(n * k + n + 1, inf);
dis[1] = 0;
vector<int> e(k * n + n + 1, 0);
q.push({0, 1});
优先队列默认大根堆,但存入 -dis,所以堆顶是距离最小的。
e 是访问标记。
while(!q.empty())
{
int id = q.top().se;
q.pop();
if(e[id]) continue;
e[id] = 1;
for(auto i : g[id])
{
int v = i.to, w = i.w;
if(dis[v] > dis[id] + w)
{
dis[v] = dis[id] + w;
q.push({-dis[v], v});
}
}
}
标准 Dijkstra 懒惰删除。
int ans = inf;
for(int i = 0; i <= k; i++)
ans = min(ans, dis[n + i * n]);
cout << ans;
因为最多改造 \(K\) 条边,不一定用完,所以取所有层中到 \(N\) 的最小值。
4. 复杂度
- 节点数:\(O(NK)\),最大约 \(210000\)。
- 边数:\(O(MK)\),最大约 \(2 \times 10^6\)。
- Dijkstra 堆优化:\(O(MK \log(NK))\),可以过。
点击查看代码
#include <bits/stdc++.h>
#define int long long
#define COUT cout<<"\n--------------------------------------------------------------\n"
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=2e5+5;
const int mod=998244353;
const int inf=1e18;
struct edge{int to,w;};
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int n,m,k;
cin>>n>>m>>k;
vector<vector<edge>> g(k*n+n+1);
for(int i=1;i<=m;i++)
{
int u,v,w;
cin>>u>>v>>w;
for(int j=0;j<=k;j++)
{
g[u+j*n].push_back({v+j*n,w});
g[v+j*n].push_back({u+j*n,w});
if(j<k)
{
g[u+j*n].push_back({v+(j+1)*n,0});
g[v+j*n].push_back({u+(j+1)*n,0});
}
}
}
priority_queue<pair<int,int>> q;
vector<int> dis(n*k+n+1,inf);
dis[1]=0;
vector<int> e(k*n+n+1,0);
q.push({0,1});
while(!q.empty())
{
int id=q.top().se;
q.pop();
if(e[id]) continue;
e[id]=1;
for(auto i:g[id])
{
int v=i.to,w=i.w;
if(dis[v]>dis[id]+w)
{
dis[v]=dis[id]+w;
q.push({-dis[v],v});
}
}
}
int ans=inf;
for(int i=0;i<=k;i++) ans=min(ans,dis[n+i*n]);
cout<<ans;
return 0;
}
//cout<<fixed<<setprecision(1);
AT_abc342_e [ABC342E] Last Train
题目描述
AtCoder 国有 \(N\) 个车站,分别为车站 \(1\)、车站 \(2\)、……、车站 \(N\)。
AtCoder 国中存在 \(M\) 条列车信息。第 \(i\) 条信息(\(1\leq i\leq M\))由六个正整数 \((l_i, d_i, k_i, c_i, A_i, B_i)\) 组成,表示如下内容:
- 对于 \(t=l_i, l_i+d_i, l_i+2d_i, \ldots, l_i+(k_i-1)d_i\) 的每一个 \(t\),都有如下列车:
- 在时刻 \(t\) 从车站 \(A_i\) 出发,在时刻 \(t+c_i\) 到达车站 \(B_i\)。
除此之外,不存在其他列车,也无法通过其他方式从一个车站移动到另一个不同的车站。
此外,换乘所需时间可以忽略不计。
定义从车站 \(S\) 到达车站 \(N\) 的最晚出发时刻为 \(f(S)\)。
更严格地说,\(f(S)\) 是满足以下所有条件的整数四元组序列 \(\big((t_i, c_i, A_i, B_i)\big)_{i=1,2,\ldots,k}\) 中 \(t\) 的最大值:
- \(t\leq t_1\)
- \(A_1=S, B_k=N\)
- 对所有 \(1\leq i<k\),有 \(B_i=A_{i+1}\)
- 对所有 \(1\leq i\leq k\),存在在时刻 \(t_i\) 从车站 \(A_i\) 出发、在时刻 \(t_i+c_i\) 到达车站 \(B_i\) 的列车
- 对所有 \(1\leq i<k\),有 \(t_i+c_i\leq t_{i+1}\)
若不存在这样的 \(t\),则 \(f(S)=-\infty\)。
请计算 \(f(1), f(2), \ldots, f(N-1)\)。
输入格式
输入按以下格式从标准输入给出。
\(N\) \(M\)
\(l_1\) \(d_1\) \(k_1\) \(c_1\) \(A_1\) \(B_1\)
\(l_2\) \(d_2\) \(k_2\) \(c_2\) \(A_2\) \(B_2\)
\(\vdots\)
\(l_M\) \(d_M\) \(k_M\) \(c_M\) \(A_M\) \(B_M\)
输出格式
输出 \(N-1\) 行。第 \(k\) 行若 \(f(k)\neq -\infty\),则输出 \(f(k)\),否则输出 Unreachable。
输入输出样例 #1
输入 #1
6 7
10 5 10 3 1 3
13 5 10 2 3 4
15 5 10 7 4 6
3 10 2 4 2 5
7 10 2 3 5 6
5 3 18 2 2 3
6 3 20 4 2 1
输出 #1
55
56
58
60
17
输入输出样例 #2
输入 #2
5 5
1000000000 1000000000 1000000000 1000000000 1 5
5 9 2 6 2 3
10 4 1 6 2 3
1 1 1 1 3 5
3 1 4 1 5 1
输出 #2
1000000000000000000
Unreachable
1
Unreachable
输入输出样例 #3
输入 #3
16 20
4018 9698 2850 3026 8 11
2310 7571 7732 1862 13 14
2440 2121 20 1849 11 16
2560 5115 190 3655 5 16
1936 6664 39 8822 4 16
7597 8325 20 7576 12 5
5396 1088 540 7765 15 1
3226 88 6988 2504 13 5
1838 7490 63 4098 8 3
1456 5042 4 2815 14 7
3762 6803 5054 6994 10 9
9526 6001 61 8025 7 8
5176 6747 107 3403 1 5
2014 5533 2031 8127 8 11
8102 5878 58 9548 9 10
3788 174 3088 5950 3 13
7778 5389 100 9003 10 15
556 9425 9458 109 3 11
5725 7937 10 3282 2 9
6951 7211 8590 1994 15 12
输出 #3
720358
77158
540926
255168
969295
Unreachable
369586
466218
343148
541289
42739
165772
618082
16582
591828
说明/提示
限制条件
- \(2\leq N\leq 2\times 10^5\)
- \(1\leq M\leq 2\times 10^5\)
- \(1\leq l_i, d_i, k_i, c_i\leq 10^9\ (1\leq i\leq M)\)
- \(1\leq A_i, B_i\leq N\ (1\leq i\leq M)\)
- \(A_i\neq B_i\ (1\leq i\leq M)\)
- 输入均为整数
样例解释 1
AtCoder 国中运行的列车如下图所示(图中未包含发车和到达时间信息)。

以车站 \(2\) 到车站 \(6\) 的最晚到达时刻为例。如下图所示,可以在时刻 \(56\) 从车站 \(2\) 出发,依次经过车站 \(2\rightarrow 3\rightarrow 4\rightarrow 6\) 到达车站 \(6\)。

无法在时刻 \(56\) 之后从车站 \(2\) 出发到达车站 \(6\),因此 \(f(2)=56\)。
样例解释 2
存在一列在时刻 \(10^{18}\) 从车站 \(1\) 出发、在时刻 \(10^{18}+10^9\) 到达车站 \(5\) 的列车。此后没有更晚出发的列车,因此 \(f(1)=10^{18}\)。
另外,在时刻 \(14\) 从车站 \(2\) 出发、在时刻 \(20\) 到达车站 \(3\) 的列车,既由第 \(2\) 条信息,也由第 \(3\) 条信息保证存在。
如上所述,可能存在多条信息重复描述同一列车。
这题是 反向建图 + Dijkstra 倒推最晚出发时间。
1. 题意转化
有 \(N\) 个车站,\(M\) 条列车信息。每条信息描述从 \(A_i\) 到 \(B_i\) 的一系列列车:
- 发车时刻:\(l_i, l_i+d_i, l_i+2d_i, \dots, l_i+(k_i-1)d_i\)
- 行程时间:\(c_i\)
即第 \(j\) 班车在 \(t = l_i + j d_i\) 从 \(A_i\) 出发,在 \(t + c_i\) 到达 \(B_i\)。
定义 \(f(S)\) 为从车站 \(S\) 出发,能够到达车站 \(N\) 的最晚出发时刻。若无法到达,则 \(f(S) = -\infty\)。
2. 反向思考
从终点 \(N\) 倒推。设 \(f[v]\) 表示从车站 \(v\) 出发,能到达 \(N\) 的最晚出发时刻。
显然:
因为已经在 \(N\),不需要再出发。
对于一条从 \(A\) 到 \(B\) 的列车,如果在 \(B\) 能够赶上的最晚出发时刻是 \(f[B]\),那么从 \(A\) 出发乘坐这趟车,必须满足:
即:
其中 \(t\) 是发车时刻。所以我们希望找到满足这个条件的最大 \(t\),然后更新 \(f[A] = \max(f[A], t)\)。
3. 如何找最大发车时刻
发车时刻是等差数列:
条件 \(t + c \le f[B]\) 等价于:
即:
所以满足条件的最大整数 \(i\) 为:
如果这个 \(i < 0\),说明连最早一班车都赶不上,直接跳过。
然后发车时刻:
用 \(t\) 去更新 \(f[A]\)。
4. 代码实现
建图
读入时,把每条信息反向存储:从 \(B\) 连向 \(A\),保存列车的参数。
struct node { int l, d, k, c, A; };
vector<vector<node>> g(n + 1);
cin >> l >> d >> k >> c >> a >> b;
g[b].push_back({l, d, k, c, a});
这里 g[b] 中存的是所有到达 \(b\) 的列车信息,其中 a 是出发站。
初始化
vector<int> f(n + 1, -1);
f[n] = 4e18; // 无穷大
priority_queue<pair<int,int>> pq; // 大根堆
pq.push({f[n], n});
Dijkstra 倒推
while (!pq.empty()) {
auto [cf, v] = pq.top(); pq.pop();
if (cf != f[v]) continue;
for (auto &e : g[v]) {
int l = e.l, d = e.d, k = e.k, c = e.c, A = e.A;
if (cf - c < l) continue; // 赶不上最早班车
int mt = cf - c;
int i = (mt - l) / d;
if (i < 0) continue;
i = min(i, k - 1);
int t = l + i * d;
if (t > f[A]) {
f[A] = t;
pq.push({t, A});
}
}
}
输出
for (int i = 1; i < n; i++) {
if (f[i] < 0) cout << "Unreachable\n";
else cout << f[i] << "\n";
}
5. 复杂度
- 节点数 \(N \le 2 \times 10^5\)
- 边数 \(M \le 2 \times 10^5\)
- 优先队列 Dijkstra:\(O(M \log N)\)
时间空间均可接受。
点击查看代码
#include <bits/stdc++.h>
#define int long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=2e5+5;
const int mod=998244353;
const int inf=1e18;
struct node{int l,d,k,c,A;};
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int n,m;
cin>>n>>m;
vector<vector<node>> g(n+1);
for(int i=0;i<m;i++)
{
int l,d,k,c,a,b;
cin>>l>>d>>k>>c>>a>>b;
g[b].push_back({l,d,k,c,a});
}
vector<int>f(n+1,-1);
f[n]=4e18;
priority_queue<pair<int,int>> pq;
pq.push({f[n],n});
while(!pq.empty())
{
auto[cf,v]=pq.top();
pq.pop();
if(cf!=f[v])continue;
for(auto&e:g[v])
{
int l=e.l,d=e.d,k=e.k,c=e.c,A=e.A;
if(cf-c<l) continue;
int mt=cf-c;
int i=(mt-l)/d;
if(i<0) continue;
i=min(i,k-1);
int t=l+i*d;
if(t>f[A])
{
f[A]=t;
pq.push({t,A});
}
}
}
for(int i=1;i<n;i++)
{
if(f[i]<0) cout<<"Unreachable\n";
else cout<<f[i]<<"\n";
}
return 0;
}
CF1037E Trips
题目描述
题目大意:
一共有\(n\)个人,他们开始互不认识,而每天早上不认识的两个人会变成朋友。一共有\(m\)天,每天晚上有的人要去旅行,去旅行的人必须满足ta有至少\(k\)个朋友也去旅行
求每天去旅行的最大人数
输入格式
第一行3个整数,表示\(n,m,k (1 \le n \le 2\cdot 10 ^ 5,\ 0 \le m \le 2\cdot 10 ^ 5,1 \le k < n)\)
往下m行,每行两个整数\(x,y\),表示这天早上\(x\)和\(y\)会变成朋友
输出格式
共\(m\)行,每行一个整数,表示每天晚上最多能去旅游的人数
输入输出样例 #1
输入 #1
4 4 2
2 3
1 2
1 3
1 4
输出 #1
0
0
3
3
输入输出样例 #2
输入 #2
5 8 2
2 1
4 2
5 4
5 2
4 3
5 1
4 1
3 2
输出 #2
0
0
0
3
3
4
4
5
输入输出样例 #3
输入 #3
5 7 2
1 5
3 2
2 5
3 4
1 2
5 3
1 3
输出 #3
0
0
0
0
3
4
4
这题是 倒序删边 + 维护 k-核。
1. 题意抽象
有 \(n\) 个人,\(m\) 天,每天早上一对陌生人成为朋友。
每天晚上选一些人去旅行,要求:每个人至少有 \(k\) 个朋友也去旅行。
等价于:在当前图中,找出最大的 \(k\)-核。
\(k\)-核:不断删除度数小于 \(k\) 的节点,最后剩下的节点集合。
每天晚上的最大旅行人数 = 当前图的 \(k\)-核大小。
2. 正向做的问题
如果每天加边后重新求 \(k\)-核,复杂度太高。
但注意到边是逐渐增加的,而删除节点容易维护。
所以考虑 倒序处理:从最后一天(所有边都加完)开始,倒着删除边,同时维护 \(k\)-核大小。
3. 算法步骤
第一步:建图并计算最终 \(k\)-核
读入所有边,建立邻接表(用 set 方便删边),统计每个点的度数 deg。
把所有度数 < k 的点入队。
不断出队,标记该点死亡 alive=false,active--(存活点数),并让它的邻居度数减 1。
如果邻居度数 < k,也入队。
队列清空后,active 就是最后一天(所有边都在)的 \(k\)-核大小。
所以 ans[m] = active。
第二步:倒序删边
从第 \(m\) 天倒推到第 \(1\) 天:
- 先记录当前
active作为第 \(i\) 天的答案(但循环中我们提前算好了ans[m],然后从 \(i=m\) 到 \(2\),先删第 \(i\) 天的边,再得到第 \(i-1\) 天的答案)。 - 删除第 \(i\) 天的边
(x, y):- 如果
x和y都还活着,从邻接表中删掉这条边,deg[x]--,deg[y]--。 - 如果
deg[x] < k,把x入队;y同理。 - 然后再次运行“删除度数
< k的点”的过程,更新active。
- 如果
ans[i-1] = active。
最后输出 ans[1] 到 ans[m]。
4. 代码关键点
vector<set<int>> adj(n+1);
vector<int> deg(n+1,0);
vector<pair<int,int>> edges(m+1);
adj[u]用set存邻居,方便erase。edges[i]存第 \(i\) 天加入的边。
queue<int> q;
for(int i=1;i<=n;i++) if(deg[i]<k) q.push(i);
int active = n;
while(!q.empty()) { ... }
ans[m] = active;
先算出最后一天的 \(k\)-核。
for(int i=m;i>=2;i--)
{
int x = edges[i].fi, y = edges[i].se;
if(alive[x] && alive[y])
{
adj[x].erase(y);
adj[y].erase(x);
deg[x]--; deg[y]--;
if(deg[x] < k) q.push(x);
if(deg[y] < k) q.push(y);
while(!q.empty()) { ... } // 继续删除
}
ans[i-1] = active;
}
倒序删边,维护 \(k\)-核。
5. 为什么只删除存活节点之间的边
如果 x 或 y 已经死亡,说明它不在 \(k\)-核中。
它的边不会影响存活节点的度数,因为死亡节点已经被移除,它们的边不计入存活节点的度数。
所以只处理 alive[x] && alive[y] 的情况是正确的。
6. 复杂度
- 每个节点最多被删除一次。
- 每条边最多被处理两次(初始建图和倒序删除)。
- 使用
set删边是 \(O(\log n)\)。
总时间复杂度:\(O((n+m)\log n)\)。
空间复杂度:\(O(n+m)\)。
可以轻松通过 \(n,m \le 2\times 10^5\)。
点击查看代码
#include <bits/stdc++.h>
#define int long long
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=2e5+5;
const int mod=998244353;
const int inf=1e18;
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int n,m,k;
cin>>n>>m>>k;
vector<pair<int,int>> edges(m+1);
vector<set<int>> adj(n+1);
vector<int> deg(n+1,0);
for(int i=1;i<=m;i++)
{
int x,y;
cin>>x>>y;
edges[i]={x,y};
adj[x].insert(y);
adj[y].insert(x);
deg[x]++;
deg[y]++;
}
vector<int> ans(m+1,0);
vector<bool> alive(n+1,true);
queue<int> q;
for(int i=1;i<=n;i++) if(deg[i]<k) q.push(i);
int active=n;
while(!q.empty())
{
int u=q.front();
q.pop();
if(!alive[u]||deg[u]>=k) continue;
alive[u]=false;
active--;
for(int v:adj[u])
{
if(alive[v])
{
deg[v]--;
if(deg[v]<k) q.push(v);
}
}
}
ans[m]=active;
for(int i=m;i>=2;i--)
{
int x=edges[i].fi;
int y=edges[i].se;
if(alive[x]&&alive[y])
{
adj[x].erase(y);
adj[y].erase(x);
deg[x]--;
deg[y]--;
if(deg[x]<k) q.push(x);
if(deg[y]<k) q.push(y);
while(!q.empty())
{
int u=q.front();
q.pop();
if(!alive[u]||deg[u]>=k) continue;
alive[u]=false;
active--;
for(int v:adj[u])
{
if(alive[v])
{
deg[v]--;
if(deg[v]<k) q.push(v);
}
}
}
}
ans[i-1]=active;
}
for(int i=1;i<=m;i++) cout<<ans[i]<<"\n";
return 0;
}
P6822 [PA 2012 Finals] Tax
题目描述
给出一个 \(n\) 个点 \(m\) 条边的无向图,经过一个点的代价是进入和离开这个点的两条边的边权的较大值,求从起点 \(1\) 到点 \(n\) 的最小代价。起点的代价是离开起点的边的边权,终点的代价是进入终点的边的边权。
输入格式
第一行两个数 \(n,m\),分别表示点数和边数。
接下来 \(m\) 行,每行三个数 \(a,b,c\),表示 \(a,b\) 之间存在一条长度为 \(c\) 的边。
输出格式
一行一个数,表示答案。
输入输出样例 #1
输入 #1
4 5
1 2 5
1 3 2
2 3 1
2 4 4
3 4 8
输出 #1
12
说明/提示
\(1\leq n\leq 10^5\),\(1\leq m\leq 2\times 10^5\),\(1\leq c\leq 10^6\)。
这题的核心是:经过一个点的代价 = 进入边和离开边权值的较大值。
直接以原图点跑最短路不行,因为代价取决于前后两条边。所以把每条有向边拆成一个状态点,在边之间转移。
1. 拆边建图
对于原图每条无向边 \((u,v,w)\),拆成两条有向边:
- \(u \to v\),编号
id - \(v \to u\),编号
id^1(互为反向边)
用 e[u] 存从 \(u\) 出发的所有有向边:{w, id}。
2. 点内转移
对于每个点 \(u\),把它所有的出边按边权 \(w\) 从小到大排序。
进入 \(u\) 后,选择同一条边离开
如果通过某条有向边 \(v1^1\) 进入 \(u\),那么 \(v1^1\) 的反向边 \(v1\) 就是从 \(u\) 出发的边,且权值相同为 \(w1\)。
经过 \(u\) 的代价是 \(\max(w1,w1)=w1\)。
所以建边:
ee[v1^1].emplace_back(w1, v1);
在 \(u\) 点切换到另一条出边
排序后,设相邻两条出边为 \(v1\)(权 \(w1\))和 \(v2\)(权 \(w2\)),且 \(w1 \le w2\)。
- 如果已经选了 \(v1\) 离开,代价已付 \(\max(w_{\text{in}}, w1)\)。
现在改选 \(v2\),代价应为 \(\max(w_{\text{in}}, w2)\)。
因为 \(w1 \le w2\),额外代价是 \(w2 - w1\)。
所以建边:
ee[v1].emplace_back(w2 - w1, v2);
- 反过来,从 \(v2\) 改选 \(v1\),代价不增加,建边权 \(0\):
ee[v2].emplace_back(0, v1);
这样,通过相邻边之间的“差值”边,就能以最小代价从任意出边切换到任意出边,最终总代价等于 \(\max(\text{进入边}, \text{离开边})\)。
3. 虚拟源点与汇点
- 起点 \(1\):没有进入边,代价就是离开边的权值。
建虚拟源点 \(s\),向所有从 \(1\) 出发的边 \(v\) 连边权 \(w\):
for(auto [w,v] : e[1]) ee[s].emplace_back(w, v);
- 终点 \(n\):没有离开边,代价就是进入边的权值。
所有进入 \(n\) 的边是e[n]中边的反向边v^1。
建虚拟汇点 \(t\),从v^1向 \(t\) 连边权 \(w\):
for(auto [w,v] : e[n]) ee[v^1].emplace_back(w, t);
4. 跑最短路
在拆点后的图 ee 上跑 Dijkstra,从 \(s\) 到 \(t\) 的最短距离就是答案。
5. 代码关键点
cnt从 \(0\) 开始,每条无向边分配两个连续编号,^1得到反向边。e[u]存的是从 \(u\) 出发的有向边,pair<w, id>。- 排序
e[u]后,相邻边建立差值边。 ee是拆点后的邻接表,节点总数 \(2m+2\)。dijkstra(s)求到 \(t\) 的最短路。
6. 复杂度
- 节点数:\(O(m)\)
- 边数:\(O(m + \sum \deg(u)) = O(m)\)
- 排序每个点的出边:总 \(O(m \log m)\)
- Dijkstra:\(O(m \log m)\)
可以轻松通过 \(n \le 10^5, m \le 2\times 10^5\)。
代码实现正确,核心就是拆边 + 排序建辅助边,把“点代价”转化为“边间转移代价”。
点击查看代码
#include<bits/stdc++.h>
#define int long long
#define pii pair<int,int>
using namespace std;
const int N=400010;
vector<pii> e[N],ee[N];
int n,m,cnt,d[N],vis[N];
void dijkstra(int s)
{
memset(d,0x3f,sizeof d);
priority_queue<pii,vector<pii>,greater<pii>> q;
q.emplace(d[s]=0,s);
while(!q.empty())
{
int u=q.top().second; q.pop();
if(vis[u]) continue;
vis[u]=1;
for(auto [w,v]:ee[u]) if(d[v]>d[u]+w) q.emplace(d[v]=d[u]+w,v);
}
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>n>>m;
for(int i=1,u,v,w; i<=m; i++)
{
cin>>u>>v>>w;
e[u].emplace_back(w,cnt++);
e[v].emplace_back(w,cnt++);
}
int s=cnt,t=cnt+1;
for(int i=1; i<=n; i++)
{
sort(e[i].begin(),e[i].end());
for(int j=0; j<e[i].size(); j++)
{
int v1=e[i][j].second,w1=e[i][j].first,v2=e[i][j+1].second,w2=e[i][j+1].first;
ee[v1^1].emplace_back(w1,v1);
if(j<e[i].size()-1)
{
ee[v1].emplace_back(w2-w1,v2);
ee[v2].emplace_back(0,v1);
}
}
}
for(auto [w,v]:e[1]) ee[s].emplace_back(w,v);
for(auto [w,v]:e[n]) ee[v^1].emplace_back(w,t);
dijkstra(s);
cout<<d[t];
}
P1983 [NOIP 2013 普及组] 车站分级
题目背景
NOIP2013 普及组 T4
题目描述
一条单向的铁路线上,依次有编号为 \(1, 2, …, n\) 的 \(n\) 个火车站。每个火车站都有一个级别,最低为 \(1\) 级。现有若干趟车次在这条线路上行驶,每一趟都满足如下要求:如果这趟车次停靠了火车站 \(x\),则始发站、终点站之间所有级别大于等于火车站 \(x\) 的都必须停靠。
注意:起始站和终点站自然也算作事先已知需要停靠的站点。
例如,下表是 \(5\) 趟车次的运行情况。其中,前 \(4\) 趟车次均满足要求,而第 \(5\) 趟车次由于停靠了 \(3\) 号火车站(\(2\) 级)却未停靠途经的 \(6\) 号火车站(亦为 \(2\) 级)而不满足要求。

现有 \(m\) 趟车次的运行情况(全部满足要求),试推算这 \(n\) 个火车站至少分为几个不同的级别。
输入格式
第一行包含 \(2\) 个正整数 \(n, m\),用一个空格隔开。
第 \(i + 1\) 行 \((1 ≤ i ≤ m)\) 中,首先是一个正整数 \(s_i\ (2 ≤ s_i ≤ n)\),表示第 \(i\) 趟车次有 \(s_i\) 个停靠站;接下来有 \(s_i\) 个正整数,表示所有停靠站的编号,从小到大排列。每两个数之间用一个空格隔开。输入保证所有的车次都满足要求。
输出格式
一个正整数,即 \(n\) 个火车站最少划分的级别数。
输入输出样例 #1
输入 #1
9 2
4 1 3 5 6
3 3 5 6
输出 #1
2
输入输出样例 #2
输入 #2
9 3
4 1 3 5 6
3 3 5 6
3 1 5 9
输出 #2
3
说明/提示
对于 \(20\%\) 的数据,\(1 ≤ n, m ≤ 10\);
对于 \(50\%\) 的数据,\(1 ≤ n, m ≤ 100\);
对于 \(100\%\) 的数据,\(1 ≤ n, m ≤ 1000\)。
这题是 偏序关系 + 拓扑排序求最长路。
1. 题意转化
每趟车次停靠的站级别都较高,未停靠的站级别都较低。
具体地,对于一趟车次,设始发站为 \(w_1\),终点站为 \(w_a\)。
在 \([w_1, w_a]\) 区间内:
- 停靠站集合 \(S\)
- 未停靠站集合 \(T\)
题目条件等价于:
因此可以建一条有向边 \(u \to v\),表示 \(u\) 的级别小于 \(v\)。
所有这样的边构成一个有向无环图(DAG)。
要求给每个点分配最小正整数级别,满足所有边 \(u \to v\) 都有 \(\text{level}(u) < \text{level}(v)\)。
这等价于求这个 DAG 的最长链长度(节点数),也就是最少需要的级别数。
2. 建图
对于每趟车次:
- 读入停靠站,用
t[]标记哪些站停靠。 - 停靠站列表
w[]已经按编号从小到大排列。 - 遍历区间 \((w[1], w[a])\) 内的每个站
l:- 如果
l是停靠站,跳过。 - 否则
l是未停靠站,向所有停靠站w[j]建边l -> w[j]。
- 如果
- 用
f[l][w[j]]去重,避免重复建边。
3. 拓扑排序求最长路
- 统计每个点的入度
q[]。 - 入度为 0 的点级别初始为 1,入队。
- 队列存
(节点, 当前级别)。 - 每次取出队首,更新答案
ans = max(ans, 级别)。 - 遍历该点的所有出边,将目标点入度减 1。
- 如果目标点入度变为 0,则其级别为当前级别 +1,入队。
- 最终
ans就是最少级别数。
4. 代码关键点
bool t[1010], f[1010][1010];
vector<int> q(n+10, 0);
vector<vector<int>> e(n+10);
t:当前车次停靠站标记。f:去重,防止同一条边重复添加。q:入度数组。e:邻接表。
for(int l = w[1] + 1; l < w[a]; l++) {
if(t[l]) continue;
for(int j = 1; j <= a; j++) {
if(!f[l][w[j]]) {
e[l].push_back(w[j]);
q[w[j]]++;
f[l][w[j]] = 1;
}
}
}
- 未停靠站
l向所有停靠站w[j]连边。
queue<pair<int,int>> r;
for(int i = 1; i <= n; i++)
if(!q[i]) r.push({i, 1});
while(!r.empty()) {
int u = r.front().fi, cut = r.front().se;
r.pop();
ans = max(ans, cut);
for(auto i : e[u]) {
q[i]--;
if(q[i] == 0) r.push({i, cut + 1});
}
}
- 拓扑排序,同时记录级别,取最大值。
5. 复杂度
- 建图:最坏 \(O(m \cdot n^2)\),但 \(n, m \le 1000\),且去重后实际边数可控。
- 拓扑排序:\(O(n + E)\)。
- 空间:\(O(n^2)\) 用于去重数组,\(n \le 1000\) 可接受。
6. 总结
核心思想:
停靠站级别 > 未停靠站级别,转化为 DAG 上的偏序关系。
用拓扑排序求最长链长度,即为最少级别数。
代码实现正确,可以 AC。
点击查看代码
#include <bits/stdc++.h>
#define int long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n"
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
inline int r()
{
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-') f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
return x*f;
}
const int N=2e5+5;
const int mod=998244353;
bool t[1010],f[1010][1010];
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int n=r(),m=r(),ans=0,a;
vector<int> q(n+10,0);
vector<vector<int>> e(n+10);
for(int i=1;i<=m;i++)
{
a=r();
memset(t,0,sizeof(t));
vector<int> w(a+1);
for(int l=1;l<=a;l++)
{
w[l]=r();
t[w[l]]=1;
}
for(int l=w[1]+1;l<w[a];l++)
{
if(t[l]) continue;
for(int j=1;j<=a;j++)
{
if(f[l][w[j]]==0)
{
e[l].push_back(w[j]);
q[w[j]]++;
f[l][w[j]]=1;
}
}
}
}
queue<pair<int,int>> r;
for(int i=1;i<=n;i++) if(!q[i]) r.push({i,1});
while(!r.empty())
{
int u=r.front().fi,cut=r.front().se;
r.pop();
ans=max(ans,cut);
for(auto i:e[u])
{
q[i]--;
if(q[i]==0) r.push({i,cut+1});
}
}
cout<<ans;
return 0;
}
//cout<<fixed<<setprecision(1);
AT_abc270_f [ABC270F] Transportation
题目描述
在 AtCoder 国有 \(N\) 个岛屿,最初每个岛屿上都没有机场和港口,岛屿之间也没有道路。作为国王的高桥君决定为这些岛屿之间提供交通方式。具体来说,高桥君可以无限次地选择并执行以下三种操作之一:
- 选择满足 \(1\leq i\leq N\) 的 \(i\),支付费用 \(X_i\),在岛屿 \(i\) 上建造机场。
- 选择满足 \(1\leq i\leq N\) 的 \(i\),支付费用 \(Y_i\),在岛屿 \(i\) 上建造港口。
- 选择满足 \(1\leq i\leq M\) 的 \(i\),支付费用 \(Z_i\),在岛屿 \(A_i\) 和岛屿 \(B_i\) 之间建造一条双向道路。
高桥君的目标是,使得对于任意两个不同的岛屿 \(U\) 和 \(V\),都可以从岛屿 \(U\) 出发,通过无限次地选择并执行以下三种操作之一,到达岛屿 \(V\):
- 如果岛屿 \(S\) 和 \(T\) 都有机场,可以从 \(S\) 移动到 \(T\)。
- 如果岛屿 \(S\) 和 \(T\) 都有港口,可以从 \(S\) 移动到 \(T\)。
- 如果存在连接岛屿 \(S\) 和 \(T\) 的道路,可以从 \(S\) 移动到 \(T\)。
请你求出高桥君达成目标所需的最小总费用。
输入格式
输入以如下格式从标准输入读入。
\(N\) \(M\)
\(X_1\) \(X_2\) \(\ldots\) \(X_N\)
\(Y_1\) \(Y_2\) \(\ldots\) \(Y_N\)
\(A_1\) \(B_1\) \(Z_1\)
\(A_2\) \(B_2\) \(Z_2\)
\(\vdots\)
\(A_M\) \(B_M\) \(Z_M\)
输出格式
输出高桥君达成目标所需的最小总费用。
输入输出样例 #1
输入 #1
4 2
1 20 4 7
20 2 20 3
1 3 5
1 4 6
输出 #1
16
输入输出样例 #2
输入 #2
3 1
1 1 1
10 10 10
1 2 100
输出 #2
3
输入输出样例 #3
输入 #3
7 8
35 29 36 88 58 15 25
99 7 49 61 67 4 57
2 3 3
2 5 36
2 6 89
1 6 24
5 7 55
1 3 71
3 4 94
5 6 21
输出 #3
160
说明/提示
限制条件
- \(2 \leq N \leq 2\times 10^5\)
- \(1 \leq M \leq 2\times 10^5\)
- \(1 \leq X_i \leq 10^9\)
- \(1 \leq Y_i \leq 10^9\)
- \(1 \leq A_i < B_i \leq N\)
- \(1 \leq Z_i \leq 10^9\)
- 若 \(i \neq j\),则 \((A_i,B_i) \neq (A_j,B_j)\)
- 所有输入均为整数
样例解释 1
高桥君可以按如下方式建设交通设施:
- 支付费用 \(X_1=1\),在岛屿 \(1\) 上建造机场。
- 支付费用 \(X_3=4\),在岛屿 \(3\) 上建造机场。
- 支付费用 \(Y_2=2\),在岛屿 \(2\) 上建造港口。
- 支付费用 \(Y_4=3\),在岛屿 \(4\) 上建造港口。
- 支付费用 \(Z_2=6\),在岛屿 \(1\) 和岛屿 \(4\) 之间建造道路。
此时目标已经达成,总费用为 \(16\)。不存在费用不超过 \(15\) 的达成目标的方法,因此输出 \(16\)。
样例解释 2
可以有机场、港口、道路三者之一未被建设的情况。
由 ChatGPT 4.1 翻译
这题是 最小生成树 + 虚拟节点。
1. 题意转化
有 \(N\) 个岛屿,三种交通方式:
- 机场:在岛屿 \(i\) 建机场花费 \(X_i\)。如果两个岛屿都有机场,它们可以互相到达。
- 港口:在岛屿 \(i\) 建港口花费 \(Y_i\)。如果两个岛屿都有港口,它们可以互相到达。
- 道路:在 \(A_i\) 和 \(B_i\) 之间建路花费 \(Z_i\),直接连通。
目标:让所有岛屿连通,求最小总费用。
2. 关键思路:虚拟节点
机场和港口可以看成“网络”:
- 建机场 \(i\),相当于把岛屿 \(i\) 连到一个虚拟机场节点 \(0\),边权 \(X_i\)。
- 建港口 \(i\),相当于把岛屿 \(i\) 连到一个虚拟港口节点 \(n+1\),边权 \(Y_i\)。
这样,如果多个岛屿都建了机场,它们都连到 \(0\),自然就连通了。港口同理。
于是问题变成:在包含虚拟节点的图上,选一些边,使得所有真实岛屿(\(1\) 到 \(n\))连通,求最小总边权。这就是最小生成树。
3. 四种情况
虚拟节点 \(0\) 和 \(n+1\) 可以单独使用,也可以同时使用。我们枚举四种情况,分别跑 Kruskal,取最小值:
- 只用道路:边集 = 所有道路。
- 道路 + 机场:边集 = 所有道路 + 每个岛屿连向 \(0\) 的边 \((0, i, X_i)\)。
- 道路 + 港口:边集 = 所有道路 + 每个岛屿连向 \(n+1\) 的边 \((n+1, i, Y_i)\)。
- 道路 + 机场 + 港口:边集 = 所有道路 + 机场边 + 港口边。
为什么只有这四种?因为机场和港口是两种独立的网络,可以不用、只用一种、或两种都用。不可能出现只用机场但不用道路却能让所有岛屿连通的情况(除非所有岛屿都建机场),而这种情况已经包含在“道路 + 机场”中(此时道路边可能不被选)。所以四种情况覆盖了所有可能。
4. 代码详解
并查集
int fa[N], sz[N];
int get(int id) { return fa[id] == id ? id : fa[id] = get(fa[id]); }
fa父节点,sz连通块大小(只统计真实岛屿)。- 初始化时,
fa[0..n+1] = i,sz[1..n] = 1,虚拟节点sz[0] = sz[n+1] = 0。
Kruskal
int kruskal(vector<edge>& e) {
fa.assign(n+2, 0);
sz.assign(n+2, 0);
for(int i=0; i<=n+1; i++) fa[i] = i;
for(int i=1; i<=n; i++) sz[i] = 1;
sort(e.begin(), e.end(), cmd);
int cost = 0;
for(auto& ed : e) {
int ru = get(ed.u), rv = get(ed.v);
if(ru != rv) {
fa[ru] = rv;
sz[rv] += sz[ru];
cost += ed.w;
if(sz[rv] == n) return cost; // 所有岛屿连通
}
}
return inf;
}
- 按边权从小到大排序。
- 合并两个连通块,累加费用。
- 当某个连通块的大小等于 \(n\) 时,说明所有真实岛屿已连通,直接返回当前费用。
- 如果遍历完都没连通,返回
inf。
主函数
vector<edge> roads;
// 读入道路
int ans = inf;
// 情况1:只用道路
vector<edge> e = roads;
ans = min(ans, kruskal(e));
// 情况2:道路 + 机场
e = roads;
for(int i=1; i<=n; i++) e.push_back({0, i, x[i]});
ans = min(ans, kruskal(e));
// 情况3:道路 + 港口
e = roads;
for(int i=1; i<=n; i++) e.push_back({n+1, i, y[i]});
ans = min(ans, kruskal(e));
// 情况4:道路 + 机场 + 港口
e = roads;
for(int i=1; i<=n; i++) e.push_back({0, i, x[i]});
for(int i=1; i<=n; i++) e.push_back({n+1, i, y[i]});
ans = min(ans, kruskal(e));
cout << ans << "\n";
- 每种情况都重新构建边集,调用
kruskal。 - 取四种情况的最小值。
5. 复杂度
- 边数:道路 \(M\),机场 \(N\),港口 \(N\),所以每种情况边数最多 \(M + 2N\)。
- 排序:\(O((M+N) \log (M+N))\)。
- 四种情况总时间:\(O(4 \times (M+N) \log (M+N))\),可以接受。
- 空间:\(O(M+N)\)。
点击查看代码
#include <bits/stdc++.h>
#define int long long
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=2e5+5;
const int mod=998244353;
const int inf=1e18;
int n,m;
vector<int> x,y,fa,sz;
struct edge{int u,v,w;};
vector<edge> roads;
inline bool cmd(const edge&a,const edge&b){return a.w<b.w;}
inline int get(int id){return fa[id]==id?id:fa[id]=get(fa[id]);}
int kruskal(vector<edge>& e)
{
fa.assign(n+2,0);
sz.assign(n+2,0);
for(int i=0;i<=n+1;i++)fa[i]=i;
for(int i=1;i<=n;i++) sz[i]=1;
sort(e.be,e.en,cmd);
int cost=0;
for(auto&ed:e)
{
int ru=get(ed.u),rv=get(ed.v);
if(ru!=rv)
{
fa[ru]=rv;
sz[rv]+=sz[ru];
cost+=ed.w;
if(sz[rv]==n) return cost;
}
}
return inf;
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin>>n>>m;
x.resize(n+1);
y.resize(n+1);
for(int i=1;i<=n;i++) cin>>x[i];
for(int i=1;i<=n;i++) cin>>y[i];
for(int i=1;i<=m;i++)
{
int a,b,c;
cin>>a>>b>>c;
roads.push_back({a,b,c});
}
int ans=inf;
vector<edge> e=roads;
ans=min(ans,kruskal(e));
e=roads;
for(int i=1;i<=n;i++) e.push_back({0,i,x[i]});
ans=min(ans,kruskal(e));
e=roads;
for(int i=1;i<=n;i++) e.push_back({n+1,i,y[i]});
ans=min(ans,kruskal(e));
e=roads;
for(int i=1;i<=n;i++) e.push_back({0,i,x[i]});
for(int i=1;i<=n;i++) e.push_back({n+1,i,y[i]});
ans=min(ans,kruskal(e));
cout<<ans<<"\n";
return 0;
}
CF274D Lovely Matrix
题目描述
Lenny 有一个 \(n \times m\) 的正整数矩阵。他非常喜欢这个矩阵,因为矩阵的每一行都是非递减排序的。出于同样的原因,他把这样的整数矩阵称为“lovely”矩阵。
有一天,Lenny 在学校时,他的弟弟在家里玩弄了这个矩阵。他擦掉了矩阵中的一些元素,并且改变了部分列的顺序。当 Lenny 回到家时,他非常伤心。现在 Lenny 想重新恢复他的矩阵。
请你帮助他找到一种重新排列矩阵列的顺序,使得可以补全被擦掉的元素,从而重新获得一个“lovely”矩阵。注意,你可以用任意整数填补被擦掉的元素。
输入格式
输入的第一行为两个正整数 \(n\) 和 \(m\)(\(1 \leq n \cdot m \leq 10^{5}\))。接下来的 \(n\) 行,每行包含 \(m\) 个用空格分隔的整数,表示该矩阵。整数 \(-1\) 表示该位置上的元素被擦掉。其他整数(每个都在 \(0\) 到 \(10^{9}\) 之间,包括 \(0\) 和 \(10^{9}\))表示已经填好的元素。
输出格式
如果不存在可行的列重排方式,输出 \(-1\)。否则,输出 \(m\) 个整数 \(p_1, p_2, \ldots, p_m\),表示所求的列的排列。也就是说,恢复后的第一列为原矩阵的第 \(p_1\) 列,第二列为原矩阵的第 \(p_2\) 列,依此类推。
输入输出样例 #1
输入 #1
3 3
1 -1 -1
1 2 1
2 -1 1
输出 #1
3 1 2
输入输出样例 #2
输入 #2
2 3
1 2 2
2 5 4
输出 #2
1 3 2
输入输出样例 #3
输入 #3
2 3
1 2 3
3 2 1
输出 #3
-1
说明/提示
由 ChatGPT 5 翻译
这题是 拓扑排序 + 虚拟节点建图,用来确定列的合法排列。
1. 题意抽象
给定一个 \(n \times m\) 的矩阵,其中有些位置是 \(-1\)(可任意填充)。
原本矩阵的每一行都是非递减的,但现在列的顺序被打乱了。
我们需要重新排列列的顺序,使得每一行在重排后,可以通过填充 \(-1\) 变成一个非递减序列。
换句话说:对于每一行中已经给出的数字,它们在新的列顺序中必须保持非递减。
即:如果同一行中两个已知元素的值不同,那么值较小的那个所在的列必须排在值较大的列之前。
如果值相同,则它们之间没有顺序要求。
2. 核心思路:偏序关系 → 拓扑排序
每一行给出了一些列之间的偏序关系:
- 值小的列必须排在值大的列之前。
- 值相同的列可以任意排列。
这些关系必须同时满足,因此可以把列看作节点,关系看作有向边,求一个拓扑序。
如果存在拓扑序,就输出这个排列;如果存在环,说明无解,输出 \(-1\)。
3. 虚拟节点技巧
直接对每一行中所有“值小的列”向“值大的列”连边,边数可能爆炸(\(O(n m^2)\))。
所以使用虚拟节点来压缩关系。
对于每一行:
- 收集所有非 \(-1\) 的元素,得到
(值, 列号)。 - 按值从小到大排序。
- 将相同值的列分到一组。
- 为每个值组创建一个虚拟节点 \(v\)。
- 对于该组中的每一列 \(c\),连边 \(c \to v\)。
表示:这些列必须排在虚拟节点 \(v\) 之前。 - 如果存在前一个值组(对应虚拟节点
prev_v),则对于当前组中的每一列 \(c\),连边prev_v -> c。
表示:前一个虚拟节点必须排在当前组的列之前。
这样,对于一行中从小到大排列的值组 \(G_1, G_2, \dots, G_t\),会形成链:
其中 \(G_i\) 表示该组中的所有列。
拓扑排序后,所有 \(G_1\) 中的列会排在所有 \(G_2\) 中的列之前,以此类推,满足非递减要求。
4. 代码详解
vector<vector<int>> adj(m+1); // 1..m 是列节点,之后动态添加虚拟节点
vector<int> indeg(m+1, 0); // 入度
初始只有 \(m\) 个列节点,编号 \(1\) 到 \(m\)。
for(int i = 0; i < n; i++) {
vector<pair<int,int>> row;
for(int j = 0; j < m; j++) {
if(a[i][j] != -1) {
row.push_back({a[i][j], j+1}); // 列号从 1 开始
}
}
if(row.empty()) continue;
sort(row.begin(), row.end()); // 按值排序
int prev_v = -1;
int k = 0;
while(k < row.size()) {
int val = row[k].first;
vector<int> cur_cols;
while(k < row.size() && row[k].first == val) {
cur_cols.push_back(row[k].second);
k++;
}
// 创建虚拟节点
int v = adj.size();
adj.push_back(vector<int>());
indeg.push_back(0);
// 当前组的列 -> 虚拟节点 v
for(int c : cur_cols) {
adj[c].push_back(v);
indeg[v]++;
}
// 前一个虚拟节点 -> 当前组的列
if(prev_v != -1) {
for(int c : cur_cols) {
adj[prev_v].push_back(c);
indeg[c]++;
}
}
prev_v = v;
}
}
- 每行的每个值组创建一个虚拟节点。
- 边方向:列 \(\to\) 虚拟节点 \(\to\) 下一个值组的列。
- 这样可以保证值小的列全部排在值大的列之前。
queue<int> q;
for(int i = 1; i < adj.size(); i++)
if(indeg[i] == 0) q.push(i);
vector<int> ans;
while(!q.empty()) {
int u = q.front(); q.pop();
if(u <= m) ans.push_back(u); // 只记录列节点
for(int v : adj[u]) {
indeg[v]--;
if(indeg[v] == 0) q.push(v);
}
}
if(ans.size() != m) cout << -1 << "\n";
else for(int i = 0; i < m; i++) cout << ans[i] << " ";
- 拓扑排序,从入度为 0 的节点开始。
- 只把列节点(\(u \le m\))加入答案。
- 如果最终答案中的列数不等于 \(m\),说明有环,无解,输出 \(-1\)。
- 否则输出排列。
5. 为什么正确?
- 对于每一行,值小的列必须先于值大的列。虚拟节点链保证了这一点。
- 不同行之间的约束通过共享列节点自然合并。
- 拓扑排序给出的顺序满足所有偏序关系,即每一行都能变成非递减。
- 如果存在环,说明约束矛盾,无法满足,输出 \(-1\)。
6. 复杂度
- 设 \(S\) 为所有非 \(-1\) 元素的总数,\(S \le n \cdot m \le 10^5\)。
- 每一行排序:总 \(O(S \log S)\)。
- 虚拟节点总数不超过 \(S\),边数 \(O(S)\)。
- 拓扑排序 \(O(S + m)\)。
- 总时间 \(O(S \log S)\),空间 \(O(S + m)\)。
可以轻松通过 \(n \cdot m \le 10^5\)。
点击查看代码
#include <bits/stdc++.h>
#define int long long
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=2e5+5;
const int mod=998244353;
const int inf=1e18;
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int n,m;
cin>>n>>m;
vector<vector<int>>a(n,vector<int>(m));
for(int i=0;i<n;i++)
{
for(int j=0;j<m;j++)
{
cin>>a[i][j];
}
}
vector<vector<int>> adj(m+1);
vector<int> indeg(m+1,0);
for(int i=0;i<n;i++)
{
vector<pair<int,int>>row;
for(int j=0;j<m;j++)
{
if(a[i][j]!=-1)
{
row.push_back({a[i][j],j+1});
}
}
if(row.empty())continue;
sort(row.begin(),row.end());
int prev_v=-1;
int k=0;
while(k<(int)row.size())
{
int val=row[k].first;
vector<int> cur_cols;
while(k<(int)row.size()&&row[k].first==val)
{
cur_cols.push_back(row[k].second);
k++;
}
int v=adj.size();
adj.push_back(vector<int>());
indeg.push_back(0);
for(int c:cur_cols)
{
adj[c].push_back(v);
indeg[v]++;
}
if(prev_v!=-1)
{
for(int c:cur_cols)
{
adj[prev_v].push_back(c);
indeg[c]++;
}
}
prev_v=v;
}
}
queue<int>q;
for(int i=1;i<(int)adj.size();i++) if(indeg[i]==0) q.push(i);
vector<int> ans;
while(!q.empty())
{
int u=q.front();
q.pop();
if(u<=m) ans.push_back(u);
for(int v:adj[u])
{
indeg[v]--;
if(indeg[v]==0) q.push(v);
}
}
if((int)ans.size()!=m) cout<<-1<<"\n";
else for(int i=0;i<m;i++) cout<<ans[i]<<" ";
return 0;
}
本文来自博客园,作者:Aries2011,转载请注明原文链接:https://www.cnblogs.com/Aries2011/p/23128768

浙公网安备 33010602011771号