AIGC标识 拆点,边拆点,虚点相关题目

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\),所以可以把状态多开一维:

\[dp[u][j] = \text{从起点 } s \text{ 到城市 } u \text{,已经免费乘坐了 } j \text{ 次的最小花费} \]

其中:

  • \(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\) 号点。每次,你可以做两件事情中的一件:

  1. 移动。移到相邻的一个点上,注意这条边一定是可以通行的。
  2. 按开关。此时,全部路的边权取反。即:\(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\) 的路径最小权值,路径权值定义为:

\[\sum w_e - \max w_e + \min w_e \]

可以改写为:

\[\sum w_e - \max w_e + \min w_e = \sum w_e - (\max w_e - \min w_e) \]

等价于:在路径中选一条边免费(减去它的权值),再选一条边双倍花费(加上它的权值)。但必须保证免费的是最大边,双倍的是最小边。由于最短路会自然选择最优顺序,我们可以用分层图建模。

状态设计

每个点拆成 \(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)\) 出发:

  1. 同层移动:不改变操作状态,正常走边。

    \[(v, s) \leftarrow (u, s) + w \]

  2. 从层 \(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 \]

  3. 从层 \(1\) 出发(已免费):

    • 再选择当前边双倍:进入层 \(3\),花费 \(2w\)。

      \[(v, 3) \leftarrow (u, 1) + 2w \]

  4. 从层 \(2\) 出发(已双倍):

    • 再选择当前边免费:进入层 \(3\),花费 \(0\)。

      \[(v, 3) \leftarrow (u, 2) + 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\),复杂度为:

\[O((n+m)\log n) \]

答案

对于每个 \(i \in [2, n]\),到达节点 \(i\) 且两种操作都使用过的状态就是最优的,即输出:

\[\text{dist}[i + 3n] \]

如果不可达,距离为初始的 \(\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\) 条边

因此,走完一条最优路径最多需要:

\[(n-1)\times 50 \le 49 \times 50 = 2450 \]

取一个稍大的上限:

const int MAXS = 2500;

所有超过 \(2500\) 的银币都可以直接截断到 \(2500\),因为多余的银币不会让答案更优。

2. 状态设计

设:

\[dist[u][j] = \text{到达城市 } u \text{,身上有 } j \text{ 个银币时的最小时间} \]

其中:

  • \(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\),可以兑换:

\[j' = \min(MAXS, j + c_u) \]

\[dist[u][j'] = \min(dist[u][j'], dist[u][j] + d_u) \]

代码:

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\):

\[j' = j - x \]

\[dist[v][j'] = \min(dist[v][j'], dist[u][j] + t) \]

代码:

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]\),答案是:

\[\min_{0 \le j \le MAXS} dist[i][j] \]

因为到达城市时,身上有多少银币不重要,取时间最小的即可。

代码:

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\) 种症状。

总状态数最多:

\[2^n \le 2^{10} = 1024 \]

初始症状是一个二进制状态 st,目标是状态 0(没有任何症状)。

有 \(m\) 种药,每种药:

  • 花费 \(d_i\) 天
  • 能消除某些症状,用二进制 rem[i] 表示,某位为 \(1\) 表示能消除该症状
  • 会产生某些副作用,用二进制 add[i] 表示,某位为 \(1\) 表示会出现该症状

服用一种药后,症状变化为:

\[\text{new\_mask} = (\text{mask} \ \& \ \sim \text{rem}[i]) \ | \ \text{add}[i] \]

意思是:

  • mask & ~rem[i]:把能消除的症状位清零
  • | add[i]:把副作用症状位置为 \(1\)

2. 算法:Dijkstra

把每个症状状态看作图上的一个点,初始状态 st 是起点,状态 0 是终点。

每种药相当于一条边:

  • 从当前状态 mask 转移到 new_mask
  • 边权是服药天数 d[i]

因为所有边权 \(d_i \ge 1\),是正权图,所以可以用 Dijkstra 求从 st 到 0 的最短路径。

状态数最多 \(1024\),每个状态最多尝试 \(m\) 种药,总边数约:

\[1024 \times 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\) 条边”。

节点编号:

\[u + j \times n \]

表示第 \(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\) 号点,但可能在任意一层,所以最终答案是:

\[\min_{0 \le j \le K} dist[N + jn] \]

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\) 的最晚出发时刻。

显然:

\[f[N] = +\infty \]

因为已经在 \(N\),不需要再出发。

对于一条从 \(A\) 到 \(B\) 的列车,如果在 \(B\) 能够赶上的最晚出发时刻是 \(f[B]\),那么从 \(A\) 出发乘坐这趟车,必须满足:

\[\text{到达 } B \text{ 的时刻} \le f[B] \]

即:

\[t + c \le f[B] \]

其中 \(t\) 是发车时刻。所以我们希望找到满足这个条件的最大 \(t\),然后更新 \(f[A] = \max(f[A], t)\)。

3. 如何找最大发车时刻

发车时刻是等差数列:

\[t = l + i d, \quad 0 \le i < k \]

条件 \(t + c \le f[B]\) 等价于:

\[l + i d + c \le f[B] \]

即:

\[i \le \frac{f[B] - c - l}{d} \]

所以满足条件的最大整数 \(i\) 为:

\[i = \min\left(k-1, \left\lfloor \frac{f[B] - c - l}{d} \right\rfloor\right) \]

如果这个 \(i < 0\),说明连最早一班车都赶不上,直接跳过。

然后发车时刻:

\[t = l + i d \]

用 \(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\)

题目条件等价于:

\[\forall u \in T, \forall v \in S,\quad \text{level}(u) < \text{level}(v) \]

因此可以建一条有向边 \(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,取最小值:

  1. 只用道路:边集 = 所有道路。
  2. 道路 + 机场:边集 = 所有道路 + 每个岛屿连向 \(0\) 的边 \((0, i, X_i)\)。
  3. 道路 + 港口:边集 = 所有道路 + 每个岛屿连向 \(n+1\) 的边 \((n+1, i, Y_i)\)。
  4. 道路 + 机场 + 港口:边集 = 所有道路 + 机场边 + 港口边。

为什么只有这四种?因为机场和港口是两种独立的网络,可以不用、只用一种、或两种都用。不可能出现只用机场但不用道路却能让所有岛屿连通的情况(除非所有岛屿都建机场),而这种情况已经包含在“道路 + 机场”中(此时道路边可能不被选)。所以四种情况覆盖了所有可能。

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. 收集所有非 \(-1\) 的元素,得到 (值, 列号)。
  2. 按值从小到大排序。
  3. 将相同值的列分到一组。
  4. 为每个值组创建一个虚拟节点 \(v\)。
  5. 对于该组中的每一列 \(c\),连边 \(c \to v\)。
    表示:这些列必须排在虚拟节点 \(v\) 之前。
  6. 如果存在前一个值组(对应虚拟节点 prev_v),则对于当前组中的每一列 \(c\),连边 prev_v -> c。
    表示:前一个虚拟节点必须排在当前组的列之前。

这样,对于一行中从小到大排列的值组 \(G_1, G_2, \dots, G_t\),会形成链:

\[G_1 \to v_1 \to G_2 \to v_2 \to \dots \to G_t \to v_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;
}
posted @ 2026-09-26 21:51  Aries2011  阅读(27)  评论(0)    收藏  举报