【2024.8.8】Traveller解题报告——拓扑排序

洛谷 P1983 [NOIP2013 普及组] 车站分级

解题思路

这道题是一道非常经典的建模为拓扑排序解决的实际问题。可以想到,如果不同的事物之间有从高到低的优先级关系,那么可以用一条从低优先级点连向高优先级点(或反过来)的边来表示这一种相对关系。根据这个思路,我们很容易想到如何去建图:低优先级的站点连一条边到高优先级的站点。

可以发现,当一列火车从 s s s t t t行驶的过程中,那些停靠的站点的优先级肯定要大于未停靠的站点。发现这一条规律以后其实这道题已经解决了。对于每一个列车运行的信息,都从所有未停靠的站点分别连一条边到所有停靠的站点。然后在形成的DAG上跑拓扑排序找到最长的路径即为答案。时间复杂度应为 O ( n m ) O(nm) O(nm)

Attention:注意到本题的边数量较多,考虑使用邻接矩阵存图(用邻接表可能会像我一样t到飞起

AC代码

实现拓扑排序找到最长路径的方法有很多种,例如bfs一层一层删点和边,但我的代码中使用的是记忆化搜索的方式。

#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0)
#define pb push_back

#define Yes cout << "Yes\n"
#define No cout << "No\n"
#define YES cout << "YES\n"
#define NO cout << "NO\n"
#define ff first
#define ss second
#define fori(x,y) for(int i=x;i<=(int)(y);++i)
#define forj(x,y) for(int j=x;j<=(int)(y);++j)
#define fork(x,y) for(int k=x;k<=(int)(y);++k)

#define debug(x) cout << #x << " = " << x << endl

typedef long long ll;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;

ll MOD = 998244353;
ll qpow(ll a,ll p) {ll res=1; while(p) {if (p&1) {res=res*a%MOD;} a=a*a%MOD; p>>=1;} return res;}

const int N = 1e3+7;
int g[N][N];
int deg[N];

int n, q;
int dep[N];
int dfs(int u) {
    if (dep[u]) return dep[u];
    int res = 0;
    fori(1, n) if (g[u][i]) {
        res = max(res, dfs(i));
    }
    return (dep[u] = res+1);
}

bool is[N];
void solve() {
    cin >> n >> q;
    while (q--) {
        int m; cin >> m;
        fori(1, n) is[i] = false;
        int s, t;
        vector<int> vec;
        fori(1, m) {
            int x; cin >> x;
            if (i == 1) s = x;
            else if (i == m) t = x;
            is[x] = true;
            vec.push_back(x);
        }
        fori(s, t) if (!is[i]) {
            for (auto v: vec) {
                if (g[i][v]) continue;
                g[i][v] = 1;
                deg[v]++;
            }
        }
    }
    int ma = 1;
    fori(1, n) if (deg[i] == 0) ma = max(ma, dfs(i));
    cout << ma << endl;
}

signed main() {
	IOS;
	int t = 1;
	while (t--) {
		solve();
	}
	return 0;
}

洛谷 P1038 [NOIP2003 提高组] 神经网络

解题思路

这道题是非常经典的拓扑排序裸题,直接跑bfs即可。需要注意的是,要预先找到哪些点是输入层,哪些点是输出层。时间复杂度 O ( n m ) O(nm) O(nm)

AC代码

#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0)
#define pb push_back

#define Yes cout << "Yes\n"
#define No cout << "No\n"
#define YES cout << "YES\n"
#define NO cout << "NO\n"
#define ff first
#define ss second
#define fori(x,y) for(int i=x;i<=(int)(y);++i)
#define forj(x,y) for(int j=x;j<=(int)(y);++j)
#define fork(x,y) for(int k=x;k<=(int)(y);++k)

#define debug(x) cout << #x << " = " << x << endl

typedef long long ll;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;

ll MOD = 998244353;
ll qpow(ll a,ll p) {ll res=1; while(p) {if (p&1) {res=res*a%MOD;} a=a*a%MOD; p>>=1;} return res;}

const int N = 105;

vector<pll> g[N];
int deg[N];
ll U[N], C[N];
bool vis[N];
void solve() {
	int n, m;
    cin >> n >> m;
    fori(1, n) cin >> C[i] >> U[i];

    fori(1, m) {
        int u, v, w;
        cin >> u >> v >> w;
        g[u].push_back({v, w});
        deg[v]++;
    }

    queue<int> que;
    fori(1, n) {
        if (deg[i] == 0&&C[i]) que.push(i);
        else C[i] -= U[i];
    }
    while (que.size()) {
        auto u = que.front();
        que.pop();
        if (vis[u]) continue;
        vis[u] = true;
        for (auto v: g[u]) {
            C[v.first] += v.second*C[u];
            if (vis[v.first]) continue;
            if (C[v.first]>0) que.push(v.first);
        }
    }
    vector<int> ans;
    fori(1, n) if (g[i].size() == 0&&C[i]>0) ans.push_back(i);
    if (ans.size()) {
        for (auto u: ans) {
            cout << u << " " << C[u] << endl;
        }
    }
    else cout << "NULL" << endl;
}

signed main() {
	IOS;
	int t = 1;
	while (t--) {
		solve();
	}
	return 0;
}

洛谷 P4017 最大食物链计数

解题思路

这道题也是一道拓扑排序的裸题。我觉得对于一个初学者来说,这些题目对于建立一个对拓扑排序较为完整的认知体系是非常重要的。就算他在很多人眼中很简单,但有的时候做一些类似的题目可以帮助我们快速地学习或者是回忆知识点。

这道题目,我的思路是使用dfs+记忆化搜索,记录每个节点所生成的食物链数量,然后父节点的食物链数量=子节点的食物链数量之和。时间复杂度 O ( n m ) O(nm) O(nm)

AC代码

#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0)
#define pb push_back

#define Yes cout << "Yes\n"
#define No cout << "No\n"
#define YES cout << "YES\n"
#define NO cout << "NO\n"
#define ff first
#define ss second
#define fori(x,y) for(int i=x;i<=(int)(y);++i)
#define forj(x,y) for(int j=x;j<=(int)(y);++j)
#define fork(x,y) for(int k=x;k<=(int)(y);++k)

#define debug(x) cout << #x << " = " << x << endl

typedef long long ll;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;

ll MOD = 80112002;
ll qpow(ll a,ll p) {ll res=1; while(p) {if (p&1) {res=res*a%MOD;} a=a*a%MOD; p>>=1;} return res;}

const int N = 5e3+7;

vector<int> g[N];
int deg[N];
ll dp[N];

ll dfs(int u) {
    if (dp[u]) return dp[u];
    if (g[u].size() == 0) return 1;
    ll res = 0;
    for (auto v: g[u]) {
        res = (res+dfs(v))%MOD;
    }
    return (dp[u] = res%MOD);
}
void solve() {
	int n, m;
    cin >> n >> m;
    fori(1, m) {
        int u, v;
        cin >> u >> v;
        g[u].push_back(v);
        deg[v]++;
    }

    ll ans = 0;
    fori(1, n) if (deg[i] == 0) ans = (ans+dfs(i))%MOD;
    cout << ans << endl;
}

signed main() {
	IOS;
	int t = 1;
	while (t--) {
		solve();
	}
	return 0;
}

洛谷 P4934 礼物

解题思路

如果上面的题都非常简单,那么这道题就要上点强度了。注意到 a i & a j ≥ min ⁡ ( a i , a j ) a_i \& a_j \ge \min (a_i, a_j) ai&ajmin(ai,aj)成立的条件是 a i a_i ai的二进制表示是 a j a_j aj的子集(或反过来)。这道题的数据范围是 n ≤ 1 0 6 n \le 10^6 n106 0 ≤ k ≤ 20 0\le k \le 20 0k20 0 ≤ a i < 2 k 0 \le a_i < 2^k 0ai<2k,因此我们想到了枚举 k k k位二进制状态的每一位

然后我们知道:若 a i a_i ai的二进制表示是 a j a_j aj的子集,那么 a i a_i ai无法和 a j a_j aj放在同一个盒子中,这个时候我们就在它们之间连一条边。至于边的方向可以自己规定,但一定要朝同一个方向,例如我采用从小的集合到大的集合,如 3 → 7 3 \rightarrow 7 37。根据这种粗浅的方法,我们可以对每一个二进制数,都遍历一遍其子集,这样的话我们就可以得到一个约为 O ( 3 k ) O(3^k) O(3k)复杂度的建图方式,然后找出最长路径即可。

但实际上我们发现,每一个状态都只需要连接和自己只有一位不同的状态即可。因为就算枚举所有的子集,每个子集都向自己连一条边,但其实跟自己相差多位的状态还会通过其它更长的路径连接到自己,只有只相差一位的状态之间的路径固定只有一条边的长度。因此,我们就可以根据这个方法将时间复杂度优化至 O ( k 2 k ) O(k2^k) O(k2k),即可通过本题。

连完边后,我们会发现有一些节点本是不在给出的数组 a a a中的,这时候我们只需要给每个节点打上一个 v a l i d valid valid标记。在后续拓扑排序的过程中,只将valid=true的节点放入最后的分类中即可。

其实这道题可以用dp进行求解:首先按上面 v a l i d valid valid标记的方式记录每个数是否存在,然后采用如下转移方程:
d p i = max ⁡ ( d p j + [ v a l i d i ] ) , j ⊂ i 且 j 比 i 少一个 1 dp_i = \max(dp_j+[valid_{i}]), j \subset i且j比i少一个1 dpi=max(dpj+[validi]),jiji少一个1
最后 max ⁡ d p \max dp maxdp即为答案。时间复杂度也为 O ( k 2 k ) O(k2^k) O(k2k)

AC代码

这个是用 d p dp dp写的。

#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0)
#define pb push_back

#define Yes cout << "Yes\n"
#define No cout << "No\n"
#define YES cout << "YES\n"
#define NO cout << "NO\n"
#define ff first
#define ss second
#define fori(x,y) for(int i=x;i<=(int)(y);++i)
#define forj(x,y) for(int j=x;j<=(int)(y);++j)
#define fork(x,y) for(int k=x;k<=(int)(y);++k)

#define debug(x) cout << #x << " = " << x << endl

typedef long long ll;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;

ll MOD = 998244353;
ll qpow(ll a,ll p) {ll res=1; while(p) {if (p&1) {res=res*a%MOD;} a=a*a%MOD; p>>=1;} return res;}

const int N = 2e6+7;
bool arr[N];
int dp[N];
vector<int> res[25];
void solve() {
	int n, m;
    cin >> n >> m;
    fori(1, n) {
        int x; cin >> x;
        arr[x] = 1;
    }
    dp[0] = arr[0];
    if (arr[0]) res[1].push_back(0);
    int ans = arr[0];
    for (int mask=1;mask<(1<<m);++mask) {
        fori(0, m-1) {
            if (!(mask>>i&1)) continue;
            int res = mask^(1<<i);
            dp[mask] = max(dp[mask], dp[res]+arr[mask]);
        }
        if (arr[mask]) {
            ans = max(ans, dp[mask]);
            res[dp[mask]].push_back(mask);
        }
    }

    cout << 1 << endl;
    cout << ans << endl;
    fori(1, ans) {
        cout << res[i].size();
        for (auto j: res[i]) cout << " " << j;
        cout << endl;
    }
}

signed main() {
	IOS;
	int t = 1;
	while (t--) {
		solve();
	}
	return 0;
}

未完待续

  • 23
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值