刷题计划-图论

本文探讨了几道涉及图论和算法的竞赛题目,包括动态规划求解机器人行为方案数、矩阵快速幂解决路径计数问题、物流运输的最小费用规划、树形DP解决复杂道路修复问题,以及竞赛图中的特殊结构分析。通过这些实例,深入理解图论在实际问题中的应用和算法设计技巧。
摘要由CSDN通过智能技术生成

刷题计划1 ——洛谷题单-省选图论

P3758 [TJOI2017]可乐

题目描述:给一个无向无环图, n n n 个点 ( 1 − n ) (1 - n) (1n) m m m 条边, 总时间为 t t t,一个机器人从点 1 1 1 出发, 三种行为: 停在原地,去下一个相邻的城市,自爆。 问经过了 t t t 秒,机器人的行为方案数是多少 ? ? ?
数据范围
1 < t ≤ 1 0 6 1<t≤10^6 1<t106 1 ≤ N ≤ 30 1 \leq N \leq30 1N30
0 < M < 100 0<M<100 0<M<100
1 ≤ u 1 \leq u 1u, v ≤ N v \leq N vN

一开始看到这题感觉数据很水,想当然的写了个爆搜看下能过几个点,结果当然是Wa了 一个点没过。
真实思路:
我们先考虑 D p Dp Dp: 令 f [ i , j ] f[i, j] f[i,j] 表示 从点 i i i 走到 点 j j j 的方案数 ,那么 更新方式:
f [ i , j ] + = f [ i , k ] ∗ f [ k ] [ j ] f[i, j] += f[i, k] * f[k][j] f[i,j]+=f[i,k]f[k][j]
我们在设置一个点0 连接所有边来表示自爆, 增加自环表示停顿即可。

状态转移代码:

        for(int i = 0; i <= n; i ++)
            for(int j = 0; j <= n; j ++)
                for(int k = 0; k <= n; k ++)
                    f[i][j] = (f[i][j] + f[i][k] * f[k][j]) % 2017;
	

但是这样没办法处理走 t t t
然后我们发现,这个代码好像就是矩阵的乘法, 一个邻接矩阵 A A A A k A^k Ak 就可以表示从 i i i j j j 经过 k k k 步的路径方案总数。
然后写一个矩阵的 k k k次方的快速幂即可。
最后统计一下点 1 1 1 到所有点的方案数总和即可
代码;

#include <cstdlib>
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
typedef long long ll;
typedef pair<int, int> PII;
int n, m, t, ans;

typedef struct Mat {
    int g[35][35];

    Mat() {
        memset(g, 0, sizeof g);
    }

    Mat operator * (const Mat b) const { //矩阵乘法
        Mat c;
        for(int i = 0; i <= n; i ++)
            for(int j = 0; j <= n; j ++)
                for(int k = 0; k <= n; k ++)
                    c.g[i][j] = (c.g[i][j] + g[i][k] * b.g[k][j]) % 2017;
        return c;
    }
}M;
M I, E, ansm;

M pow(M a,int p) { //快速幂
    if(p == 0) return I;
    
    M temp = pow(a, p / 2);
    if(p % 2) return temp * temp * a;
    else return temp * temp;
}

int main() {
    ios::sync_with_stdio; cin.tie(0); cout.tie(0);
#ifndef ONLINE_JUDGE
    freopen("D:/Cpp/program/Test.in", "r", stdin);
    freopen("D:/Cpp/program/Test.out", "w", stdout);
#endif
    cin >> n >> m;
    for(int i = 1; i <= m; i ++) {
        int a, b;
        cin >> a >> b;
        E.g[a][b] = E.g[b][a] = 1;
    }

    for(int i = 0; i <= n; i ++) I.g[i][i] = E.g[i][i] = 1, E.g[i][0] = 1; //连接停下来的边和自爆的边

    cin >> t;
    ansm = pow(E, t);
    for(int i = 0; i <= n; i ++) ans += ansm.g[1][i];
    cout << ans % 2017 << '\n';
    return 0;
}

P1772 [ZJOI2006]物流运输

题目描述:
物流公司要把一批货物从码头 A A A 运到码头 B B B。由于货物量比较大,需要 n n n 天才能运完。货物运输过程中一般要转停好几个码头。
物流公司通常会设计一条固定的运输路线。有的时候某个码头会无法装卸货物。这时候就必须修改运输路线,让货物能够按时到达目的地。而修改线路会导致额外 k k k 的花费。 求 n n n天运输的最小花费。

数据范围: 1 ≤ n ≤ 100 , 1 ≤ m ≤ 20 。 1 \le n \le 100,1\le m \le 20。 1n1001m20

思路:

  1. 由于这题的数据量很小, 我们可以选择预处理处出 f ( i , j ) f(i, j) f(i,j), 表示第 i i i 天到第 j j j 天的最小花费(1 ~ m的最短路)。
  2. 采用动态规划的方法: d p ( i ) dp(i) dp(i) 表示第 i i i 天的最小花费, 第 j j j ( j < i ) (j < i) (j<i) 天的时候更换了线路,那么状态转移方程:
    d p ( i ) = d p ( j ) + ( i − j ) ∗ f ( j + 1 ) ( i ) + k dp(i) = dp(j) + (i - j) * f(j + 1)(i) + k dp(i)=dp(j)+(ij)f(j+1)(i)+k m i n min min 那么答案就是 d p [ n ] dp[n] dp[n]
    代码:
#include <cstdlib>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <queue>
using namespace std;
typedef long long ll;
typedef pair<int, int> PII;
const int N = 110, M = 20100;
int n, m, k, s;
ll f[N][N];
int e[M], ne[M], w[M], h[N], idx;
int dist[N];
bool st[N], is_use[N][N];
ll dp[N];

void add(int a, int b, int c) {
    e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx ++;
}

int dijkstra() {
    memset(dist, 0x3f, sizeof dist);
    priority_queue<PII, vector<PII>, greater<PII>> hea;
    hea.push({0, 1});
    dist[1] = 0;

    while(hea.size()) {
        auto t = hea.top();
        hea.pop();
        int ver = t.second;
        if(st[ver]) continue;
        st[ver] = true;

        for(int i = h[ver]; ~i; i = ne[i]) {
            int j = e[i];
            
            if(st[j]) continue;
            if(dist[j] > dist[ver] + w[i]) {
                dist[j] = dist[ver] + w[i];
                hea.push({dist[j], j});
            }
        }
    }
    //cout << dist[5] << '\n';
    return dist[m];
}

int main() {
    ios::sync_with_stdio; cin.tie(0); cout.tie(0);
    cin >> n >> m >> k >> s;
    memset(h, -1, sizeof h);
    while(s --) {
        int a, b, c;
        cin >> a >> b >> c;
        add(a, b, c);
        add(b, a, c);
    }
    int d;
    cin >> d;
    while(d --) {
        int a, b, c;
        cin >> a >> b >> c;
        for(int i = b; i <= c; i ++) is_use[a][i] = true; //表示不能用
    }

    for(int i = 1; i <= n; i ++)
        for(int j = 1; j <= n; j ++) {
            memset(st, 0, sizeof st);
            for(int a = 1; a <= m; a ++)
                for(int b = i; b <= j; b ++)
                    if(is_use[a][b]) st[a] = true;
            
            f[i][j] = dijkstra();
        }

    //dp[i] 表示前i天的最小花费 
    memset(dp, 0x3f, sizeof dp);
    for(int i = 1; i <= n; i ++) {
        dp[i] = (ll)f[1][i] * i;
        for(int j = i - 1; j >= 0; j --) {
            dp[i] = min(dp[i], dp[j] + (ll)(i - j) * f[j + 1][i] + k);
        }
    }

    cout << dp[n] << '\n';
    return 0;
}

P4438 [HNOI/AHOI2018]道路

题目描述: 太麻烦了点击进去自己看。
思路:
记忆化搜索 + + + 树形 D p Dp Dp
f ( i , j , k ) f(i, j, k) f(i,j,k) 表示第 i i i 个点到根经过 j j j 个未被修复的公路, k k k 个未被修复的铁路,所得到的最小值。
状态转移:当更新到乡村的时候:
f [ x ] [ p ] [ q ] = m i n ( d f s ( s o n [ x ] [ 0 ] , p , q ) + d f s ( s o n [ x ] [ 1 ] , p , q + 1 ) , d f s ( s o n [ x ] [ 1 ] , p , q ) + d f s ( s o n [ x ] [ 0 ] , p + 1 , q ) ) f[x][p][q]=min(dfs(son[x][0],p,q)+dfs(son[x][1],p,q+1),dfs(son[x][1],p,q)+dfs(son[x][0],p+1,q)) f[x][p][q]=min(dfs(son[x][0],p,q)+dfs(son[x][1],p,q+1),dfs(son[x][1],p,q)+dfs(son[x][0],p+1,q))
答案就是 f ( 1 , p , q ) f(1, p, q) f(1,p,q) 根节点经过 p p p 条公路和 q q q 条铁路所得到的最小值
这里用 s o n ( x , 1 / 0 ) son(x, 1/0) son(x,1/0) 表示 x x x 的左儿子为公路所连接点, 右儿子为马路所连点。
( 1 (1 (1 ~ n − 1 ) n - 1) n1) 表示城市 ( n (n (n ~ 2 n − 1 ) 2n - 1) 2n1) 表示乡村。

代码:

#include <cstdlib>
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
typedef long long ll;
typedef pair<int, int> PII;
const int N = 2e4 + 10;

int n;             
struct Node {
    ll a, b, c;
}s[N];
ll f[N][45][45];
int son[N][5]; //0 为公路, 1 为铁路

ll dfs(int x, int p, int q) { //p条公路 q条铁路
    if(x >= n) return s[x - n + 1].c * (s[x - n + 1].a + p) * (s[x - n + 1].b + q);
    if(f[x][p][q] != f[n + 1][41][41]) return f[x][p][q]; //已经被更新过
    return f[x][p][q] = min(dfs(son[x][0], p, q) + dfs(son[x][1], p, q + 1), dfs(son[x][1], p, q) + dfs(son[x][0], p + 1, q));
}

int main() {
    ios::sync_with_stdio; cin.tie(0); cout.tie(0);
#ifndef ONLINE_JUDGE
    freopen("D:/Cpp/program/Test.in", "r", stdin);
    freopen("D:/Cpp/program/Test.out", "w", stdout);
#endif
    cin >> n;
    memset(f, 63, sizeof(f));
    for(int i = 1; i < n; i ++) {
        int x, y;
        cin >> x >> y;
        if(x < 0) x = -x + n - 1; //公路
        if(y < 0) y = -y + n - 1; //铁路

        son[i][0] = x;
        son[i][1] = y;
    }

    
    for(int i = 1; i <= n; i ++) {
        cin >> s[i].a >> s[i].b >> s[i].c;
    }
    cout << dfs(1, 0, 0) << '\n';
    return 0;
}

P1131 [ZJOI2007] 时态同步

题目描述:给定一颗树和一个树的根节点,求使得从根节点开始,到每个叶子节点的时间相同需要对每条边增加的边权值的最小值。
数据范围:

N ≤ 500000 N≤500000 N500000

思路:

  1. 对于每条边,我们应当先伸长靠近叶子节点的边,然后再伸长靠近根节点的边,这样得到的结果应当才是最小值。后伸长靠近根节点的边会使得根节点下的所有点到根节点的长度增加,这样最优。
  2. 所以我们先 d f s dfs dfs 到叶子节点,然后从下往上跟新 d i s t [ u ] dist[u] dist[u] (表示某一子树的根节点 u u u 到其所有叶子节点的距离中的最大值),更新方式: d i s t [ u ] = m a x ( d i s t [ j ] + w [ i ] ) dist[u] = max(dist[j] + w[i]) dist[u]=max(dist[j]+w[i]) 其中 j j j 是 u 的子节点。
  3. 然后再从叶子节点向根节点跟新答案 a n s ans ans, 更新方式: a n s + = d i s t [ u ] − ( d i s t [ j ] + w [ i ] ) ans += dist[u] - (dist[j] + w[i]) ans+=dist[u](dist[j]+w[i]),其中 w [ i ] w[i] w[i] 为 子节点 j j j 到子树根节点 u u u 的边的权值。这一步在统计该子树中需要增加的边的长度,只需与该子树最长叶子节点保持一致即可。
    在这里插入图片描述
    代码:
#include <cstdlib>
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
typedef long long ll;
typedef pair<int, int> PII;
const int N = 1e6 + 10, M = N * 2;
int n, s;
ll ans;
int e[M], ne[M], w[M], h[N], idx;
int dist[N];

void add(int a, int b, int c) {
    e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx ++;
}

void dfs(int u, int fa) {
    for(int i = h[u]; ~i; i = ne[i]) {
        int j = e[i];
        if(j == fa) continue;
        dfs(j, u);
        dist[u] = max(dist[u], dist[j] + w[i]); //更新子树到根节点的最大距离
        //cout << u << ":" << dist[u] << '\n';
    }

    for(int i = h[u]; ~i; i = ne[i]) {
        int j = e[i];
        //cout << fa << ": " << j << '\n';
        if(j == fa) continue;
        ans += (dist[u] - (dist[j] + w[i]));
        //cout << j << '\n';
    }
    //cout << u << ":" << dist[u] << '\n';
}

int main() {
    ios::sync_with_stdio(false); 
    cin.tie(0);
#ifndef ONLINE_JUDGE
    freopen("D:/Cpp/program/Test.in", "r", stdin);
    freopen("D:/Cpp/program/Test.out", "w", stdout);
#endif
    cin >> n >> s;
    memset(h, -1, sizeof h);
    for(int i = 1; i < n; i ++) {
        int a, b, c;
        cin >> a >> b >> c;
        add(a, b, c), add(b, a, c);
    }

    dfs(s, -1);

    cout << ans << '\n';
    return 0;
}

C. Game On Leaves

【思维题】
题目概述:
两个人玩游戏,每次可以拿走图中一个叶子节点,当有人拿到唯一的特殊节点时就获胜吗,问谁会赢。

思路:

  1. 把图中的边分成两个部分,第一部分为直接与特殊节点相连的边,第二部分为其他边。
  2. 对于第一部分,如果为奇数,则先手必赢,反之必输(画个图看看)。
  3. 对于第二部分,如果边的数量为奇数,则会改变第一部分的先手与后手,为偶数则不会。
  4. 如果只有一条边或者没有边与特殊节点相连,那么直接先手赢。
  5. 否则将以上两个情况同时考虑即可。

代码:

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int, int> PII;

int main() {
    ios::sync_with_stdio(false); 
    cin.tie(0); cout.tie(0);
#ifndef ONLINE_JUDGE
    freopen("D:/Cpp/program/Test.in", "r", stdin);
    freopen("D:/Cpp/program/Test.out", "w", stdout);
#endif
    int T;
    cin >> T;
    while(T --) {
        int n, x;
        cin >> n >> x;
        int cnt = 0;
        for(int i = 1; i < n; i ++) {
            int a, b;
            cin >> a >> b;
            if(a == x || b == x) cnt ++;
        }

        if(cnt == 1 || cnt == 0) cout << "Ayush" << '\n';
        else if((n - cnt - 1) % 2){
            if(cnt % 2) cout << "Ashish" << '\n';
            else cout << "Ayush" << '\n';
        } else {
            if(cnt % 2) cout << "Ayush" << '\n';
            else cout << "Ashish" << '\n';
        }
    }
    return 0;
}

C. Cycle

题目概述:
一个竞赛图中找大小为 3 3 3 的环,输出任意一个,如果没有就输出 − 1 -1 1

竞赛图:若有向简单图 G G G 满足任意不同两点间都有恰好一条边(单向),则称 G G G 为 竞赛图 ( T o u r n a m e n t g r a p h ) (Tournament graph) (Tournamentgraph)

思路:
我们考虑如下情况的图:
在这里插入图片描述

  1. 如果直接使用 T a r j a n Tarjan Tarjan 计算环的大小,那么这个环的大小会被记为 5 5 5 但是图中是有一个大小为 3 3 3 的环 z , a , x z, a, x z,a,x, 因此我们考虑另外一种做法。
  2. 因为竞赛图保证任意两点之间有一条有向边,如果上述情况出现时, 在删掉 x x x—> z z z 后, 不论 y , z y, z y,z 之间的边方向如何,都会有一个新的大小为 3 3 3 的环。
  3. 当我们删掉这样的边后,我们使得每个点的出度都为 1 1 1, 这样,我们只需呀枚举 点 i i i, 点 i i i 直接到达的点 t o [ i ] to[i] to[i] 和点 j j j 三者之间能否构成环即可。
    代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int, int> PII;
const int N = 5010, M = N * N;
int n;
char g[N][N];
int to[N]; //to[i] 存储 i 到达的点

int main() {
   ios::sync_with_stdio(false); 
   cin.tie(0); cout.tie(0);
#ifndef ONLINE_JUDGE
   freopen("D:/Cpp/program/Test.in", "r", stdin);
   freopen("D:/Cpp/program/Test.out", "w", stdout);
#endif
    cin >> n;
    for(int i = 1; i <= n; i ++) 
        for(int j = 1; j <= n; j ++)
            cin >> g[i][j];

    for(int i = 1; i <= n; i ++)
        for(int j = 1; j <= n; j ++)
            if(g[i][j] == '1' && (!to[i] || g[j][to[i]] == '1')) to[i] = j;

    for(int i = 1; i <= n; i ++)
        for(int j = 1; j <= n; j ++)
            if(g[j][i] == '1' && g[to[i]][j] == '1') {
                cout << i << ' ' << to[i] << ' ' << j << '\n';
                return 0;
            }

    cout << -1 << '\n';
    return 0;
}

CF437C The Child and Toy

题目概述:
n n n 个点 m m m 条边,每删一个点需要花费与该点直接相连的点 i i i v [ i ] v[i] v[i],问删完整个图的最小花费。

本题考虑删点过于麻烦,因为要把图删完,所以删边也是一样的效果。
对于删掉的每条边,最小花费是边的两个节点的最小 v [ i ] v[i] v[i],将每条边的最小花费加起来就是最后的答案。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int, int> PII;
const int N = 1010, M = N * 2;
int n, m;
int a[N], ans;

int main() {
   ios::sync_with_stdio(false); 
   cin.tie(0); cout.tie(0);
#ifndef ONLINE_JUDGE
   freopen("D:/Cpp/program/Test.in", "r", stdin);
   freopen("D:/Cpp/program/Test.out", "w", stdout);
#endif
    cin >> n >> m;
    for(int i = 1; i <= n; i ++) cin >> a[i];

    while(m --) {
        int x, y;
        cin >> x >> y;
        ans += min(a[x], a[y]);
    }

    cout << ans << '\n';
    return 0;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值