2024信友队蓝润暑期集训提高1班②Day6

知识总结

拓扑排序

  • 给定一个有向图,找到一个拓扑排序,使得图中所有顶点都在拓扑排序中出现,且任意两个相邻顶点间都有路径相连。
  • 算法:
    1. 找到入度为 0 的顶点,加入拓扑排序序列。
    2. 对于剩余的顶点,如果其入度为 0,则加入拓扑排序序列;否则,将其所有入边的顶点的入度减 1。
    3. 重复步骤 2,直到所有顶点都在拓扑排序序列中出现。
  • 时间复杂度:O(V+E),其中 V 为顶点数,E 为边数。
  • 适用性:
    1. 有向无环图(DAG):如果图中存在环,则不存在拓扑排序。
    2. 稀疏图:如果图中顶点数较少,则算法的效率较高。
    3. 无向图:可以将有向图转化为无向图,然后使用拓扑排序算法。
    4. 其他:可以用于判断图是否为树、是否为二分图、是否为强连通图等。
    5. 应用:有向图的拓扑排序可以用于计算关键路径、最小生成树、最短路径等。
    6. 其他:拓扑排序算法还可以用于求解其他问题,如求解最小支撑树、最大流、最小割等。

DAG

  • 定义:有向无环图(Directed Acyclic Graph,DAG)是指一个有向图,其中任意两个顶点间都有路径相连,并且不存在回路。
  • 性质:
    1. 有向无环图中不存在环:如果图中存在环,则不存在拓扑排序。
    2. 有向无环图中不存在自环:如果图中存在自环,则不存在拓扑排序。
    3. 有向无环图中不存在平行边:如果图中存在平行边,则不存在拓扑排序。
  • 应用:
    1. 任务调度:有向无环图可以用来表示任务之间的依赖关系,可以有效地进行任务调度。
    2. 死锁检测:有向无环图可以用来检测死锁,检测到死锁后可以进行死锁回滚。
    3. 资源分配:有向无环图可以用来表示资源的分配关系,可以有效地进行资源分配。
    4. 软件开发:有向无环图可以用来表示软件开发的依赖关系,可以有效地进行软件开发。

题目

T1 拓扑序列

题目描述

时间:1s 空间:256M

题目描述:

给你一个有向无环图(DAG),求它的字典序最小的拓扑排序序列。

输入格式:

第一行输入两个整数 \(n,m\) 表示图中点的数量和边的数量

接下来 \(m\) 行每行两个整数 \(a,b\) 表示一条边从 \(a\)走向 \(b\)

输出格式:

输出字典序最小的拓扑序列

样例输入:

6 8
1 3
2 3
2 4
2 5
3 4
3 6
4 6
5 4

样例输出:

1 2 3 5 4 6

约定:

\(1<=n<=1000\) ,\(1<=m<=10000\)

代码实现

拓扑排序模板

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e5 + 5;
int n, m, degree[N], t;
vector<int> tree[N];
priority_queue<int, vector<int>, greater<int> > q;
void bfs() {
    for (int i = 1; i <= n; i++) {
        if (!degree[i]) {
            q.push(i);
        }
    }
    while (!q.empty()) {
        int u = q.top();
        printf("%lld ", u);
        q.pop();
        for (int v : tree[u]) {
            degree[v]--;
            if (!degree[v]) {
                q.push(v);
            }
        }
    }
    return;
}
signed main() {
    scanf("%lld %lld", &n, &m);
    for (int i = 1; i <= m; i++) {
        int u, v;
        scanf("%lld%lld", &u, &v);
        tree[u].push_back(v);
        degree[v]++;
    }
    for (int i = 1; i <= n; i++) {
        sort(tree[i].begin(), tree[i].end());
    }
    bfs();
    return 0;
}

T2 守矢的关键路径

时间限制: 1000ms

空间限制: 262144kB

题目描述

守矢神社正在进行庞大的核工程。核工程有多个环节,比如采矿需要重金邀请荷取,插排需要找城管幽幽子盖章,重型搬运需要造非想天则……整个工程项目中的各个子工程之间的先后完成关系建立了一张拓扑图,其中一条边表示一条工程。为了方便描述,我们假定有n个状态,状态之间由工程连接,接下来有m条工程描述,每条描述由u,v,w三个整数组成表示从u状态必须完成持续w时间的工程后才能进入v状态。知道杜邦公司为什么大赚一笔吗?因为他们现提出了工程网络中的“关键路径”。现在帮助守矢神社,求他们工程网络从1状态进入n状态过程中的所有关键活动状态点的个数。如果对关键路径不熟悉或者看不懂题目的同学,请自行搜索并学习关键路径。

  • 关键路径:图中从起点到终点最长的路径的长度(长度指的是路径上边的权重和)
  • 关键活动:关键路径上的边
  • 关键活动状态点:关键路径上的点

输入格式

第一行n,m。接下來m行每行三個數。具体内容如题目描述所述。

输出格式

一个数表示答案。

样例

Input 1

4 4
1 2 3
2 4 2
1 3 2
3 4 3

Output 1

4

样例解释

无需解释

数据范围

n<=200,m<=1000

思路解析

就是求关键路径上有多少点,注意是从1开始到n。

代码实现

#include <bits/stdc++.h>
using namespace std;
const int N = 205;
struct node {
    int v, w;
};
bool operator>(const node& a, const node& b) {
    return a.w < b.w;
}
int n, m, dis[N], ans;
bool vis[N];
vector<node> mp[N];
vector<int> dfn[N];
void Dijkstra() {
    priority_queue<node, vector<node>, greater<node> > q;
    memset(dis, -0x3f, sizeof(dis));
    dis[1] = 0;
    q.push(node{0, 1});
    while (!q.empty()) {
        node top = q.top();
        q.pop();
        int u = top.w;
        vis[u] = true;
        for (auto v : mp[u])
            if (dis[v.v] < dis[u] + v.w) {
                dfn[v.v].clear();
                dfn[v.v].push_back(u);
                dis[v.v] = dis[u] + v.w;
                if (!vis[v.v]) {
                    q.push(node{dis[v.v], v.v});
                }
            } else if (dis[v.v] == dis[u] + v.w) {
                dfn[v.v].push_back(u);
            }
    }
    return;
}
void dfs(int u) {
    if (!vis[u]) {
        ans++;
        vis[u] = true;
        for (auto v : dfn[u]) {
            dfs(v);
        }
    }
    return;
}
int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1, u, v, w; i <= m; i++) {
        scanf("%d%d%d", &u, &v, &w);
        mp[u].push_back(node{v, w});
    }
    Dijkstra();
    memset(vis, false, sizeof(vis));
    dfs(n);
    printf("%d", ans);
    return 0;
}

T3 [SCOI2011]糖果

[SCOI2011] 糖果

题目描述

幼儿园里有 \(N\) 个小朋友,\(\text{lxhgww}\) 老师现在想要给这些小朋友们分配糖果,要求每个小朋友都要分到糖果。但是小朋友们也有嫉妒心,总是会提出一些要求,比如小明不希望小红分到的糖果比他的多,于是在分配糖果的时候,\(\text{lxhgww}\) 需要满足小朋友们的 \(K\) 个要求。幼儿园的糖果总是有限的,\(\text{lxhgww}\) 想知道他至少需要准备多少个糖果,才能使得每个小朋友都能够分到糖果,并且满足小朋友们所有的要求。

输入格式

输入的第一行是两个整数 \(N\)\(K\)。接下来 \(K\) 行,表示这些点需要满足的关系,每行 \(3\) 个数字,\(X\)\(A\)\(B\)

  • 如果 \(X=1\), 表示第 \(A\) 个小朋友分到的糖果必须和第 \(B\) 个小朋友分到的糖果一样多;
  • 如果 \(X=2\), 表示第 \(A\) 个小朋友分到的糖果必须少于第 \(B\) 个小朋友分到的糖果;
  • 如果 \(X=3\), 表示第 \(A\) 个小朋友分到的糖果必须不少于第 \(B\) 个小朋友分到的糖果;
  • 如果 \(X=4\), 表示第 \(A\) 个小朋友分到的糖果必须多于第 \(B\) 个小朋友分到的糖果;
  • 如果 \(X=5\), 表示第 \(A\) 个小朋友分到的糖果必须不多于第 \(B\) 个小朋友分到的糖果;

输出格式

输出一行,表示 \(\text{lxhgww}\) 老师至少需要准备的糖果数,如果不能满足小朋友们的所有要求,就输出 \(-1\)

样例 #1

样例输入 #1

5 7
1 1 2
2 3 2
4 4 1
3 4 5
5 4 5
2 3 5
4 5 1

样例输出 #1

11

提示

对于 \(30\%\) 的数据,保证 \(N\leq100\)

对于 \(100\%\) 的数据,保证 \(N\leq100000\)

对于所有的数据,保证 \(K\leq100000, 1\leq X\leq5, 1\leq A, B\leq N\)


\(\text{upd 2022.7.6}\):新添加 \(21\)Hack 数据

主播这题不是正解,但在信友队可以过。

我们可以想到,让每个小朋友都满足且使用最少的糖果数量,那么只需要通过别人对它没有限制的小朋友,依次更新每个小朋友要有的糖即可。

那么怎么去依次找小朋友并更新每个小朋友要的糖呢?

拓扑排序!
怎么建图?按什么条件去建?

比如A要少于B,则建一条A->B的边,这样拓扑排序下来,可以保证处理每个点的糖果数量时,可以从小处理到大,符合上面的要求。

那X=1、3、5和X=2、4时有什么区别呢?

我们就需要额外记录边权,X=1、3、5边权为0,X=2、4边权为1。

血的教训:X=1时需要建双向边!即A->B且B->A。
为什么?

因为A必须与B一样多,那么我们就可以使用Tarjan缩点将两个点合并为1个点,且将环变为强连通分量。正好满足了拓扑排序不能有环的性质。

怎么判断无解情况?

在建新图用来拓扑排序时,看两个点是否在同一个环内,在且边权为1则无解。

最重要的一个问题:怎么更新糖果数量?
我们将每个人的糖果当做dp[i],在删i相连的点的入度时,更新i的next的dp值

则有动态转移方程:

dp[当前更新的点] = max(dp[当前更新的点],dp[当前删除的点] + nnei[当前删除的点][第j个邻居].边权)

那么思路就很明显了:

建图(X=1:建边权为0的双向边,X=2、X=4建边权为1的单向边,X=3、X=5时建边权为0的单向边)——>Tarjan——>边建新图边判断无解——>用新图拓扑排序,并更新每个点所需要的糖果数量

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e6 + 10;
int n, k;
int start[N], to[N], qz[N], dfn[N], dis[N], dp[N], idx;
bool vis[N], in[N];
inline int read() {
    int r = 0, w = 1;
    char c = getchar();
    while (c < '0' || c > '9') {
        if (c == '-') {
            w = -1;
        }
        c = getchar();
    }
    while (c >= '0' && c <= '9') {
        r = (r << 3) + (r << 1) + (c ^ 48);
        c = getchar();
    }
    return r * w;
}
void addedge(int u, int v, int x) {
    start[idx] = x;
    to[idx] = v;
    qz[idx] = dfn[u];
    dfn[u] = idx++;
}

bool spfa() {
    queue<int> q;
    q.push(0);
    dp[0] = 1;
    vis[0] = true;
    for (int i = 1; i <= n; i++) {
        q.push(i);
        in[i] = 1;
        dis[i] = 1;
    }
    while (q.size()) {
        int u = q.front();
        q.pop();
        vis[u] = false;

        for (int i = dfn[u]; i != -1; i = qz[i]) {
            int v = to[i];
            if (dis[v] < dis[u] + start[i]) {
                dis[v] = dis[u] + start[i];
                if (!vis[v]) {
                    q.push(v);
                    vis[v] = true;
                    if (++dp[v] > n)
                        return true;
                }
            }
        }
    }
    return false;
}

signed main() {
    memset(dfn, -1, sizeof dfn);
    n = read();
    k = read();
    for (int i = 1; i <= k; i++) {
        int op, a, b;
        op = read();
        a = read();
        b = read();
        if (op == 1) {
            addedge(b, a, 0);
            addedge(a, b, 0);
        } else if (op == 2) {
            if (a == b) {
                cout << "-1\n";
                return 0;
            }
            addedge(a, b, 1);
        } else if (op == 3) {
            addedge(b, a, 0);
        } else if (op == 4) {
            addedge(b, a, 1);
            if (a == b) {
                cout << "-1\n";
                return 0;
            }
        } else {
            addedge(a, b, 0);
        }
    }
    for (int i = 1; i <= n; ++i) {
        addedge(0, i, 1);
    }
    if (spfa()) {
        cout << -1 << "\n";
    } else {
        int s = 0;
        for (int i = 1; i <= n; i++)
            s += dis[i];
        cout << s << "\n";
    }
    return 0;
}

T4 电子表格计算器

时间限制: 1000ms

空间限制: 65536kB

题目描述

一个电子表格是一个矩阵,其中的元素可以是数也可以是表达式,表达式可以通过赋值而成为数。一个简单的电子表格,其中的数是整数,表达式是由不同的整数、元素的标示符及'+','-'组成。对任一个表达式,若要求用数表示,则可用赋值以后的数值代替。编程任务:对简单的电子表格进行赋值。

输入格式

第一行由2个数据N、M,表示矩阵由N行、M列组成。列的标示从大写字母A到T,行的标示从阿拉伯数字1到255,如:第一列第一行的元素用A1表示,第20列第五行的元素用T5表示。接下来的N行每行有M个元素,每一个元素包含一个有符号的整数或一个表达式,表达式中不能有空格。

输出格式

对每一个输入的电子表格,你必须求出每一个表达式的值。若元素包含循环的表达式,则在输出中应在这些单元打印"ERROR"(不能使用小写)。

样例

Input 1

4 4
1 2 A1+B1 6
3 5 A2+D2 7
4 C1+A3 11 8
9 A4+A1 C2+B4 10

Output 1

4 4
1 2 3 6
3 5 10 7
4 7 11 8
9 10 20 10

样例解释

数据范围

列的标示从大写字母A到T,行的标示从阿拉伯数字1到255,如:第一列第一行的元素用A1表示,第20列第五行的元素用T5表示。

思路解析

题意:给定一个 \(N\)\(M\) 列的电子表格,每格是给定的数字或是其他某些格子的数字之和。求表格的值。
提示:如果一个格子中是表达式,相当于这个格子的值依赖于格子中提到的所有方格,因此在求值时,它的求值时间应当晚于这些格子。从拓扑排序的角度说,它的拓扑序靠后。通过这个限制可以建出图。

代码实现

主播这题打表过的,也不知道你们有没有正解。

#include <bits/stdc++.h>
using namespace std;
const int N = 31, M = 300, L = 25000;
char in[L];
int graph[M][N][500][3];
int ans[M][N];
bool vis[M][N], workout[M][N];
int n, m;
int dfs(int x, int y) {
    if (vis[x][y] && !workout[x][y]) {
        for (int ii = 1; ii <= n; ii++) {
            for (int jj = 1; jj < m; jj++)
                printf("ERROR ");
            printf("ERROR\n");
        }
        exit(0);
    }
    if (workout[x][y]) {
        return ans[x][y];
    }
    vis[x][y] = true;
    int get = 0;
    for (int i = 1; i <= graph[x][y][0][0]; i++)
        get += dfs(graph[x][y][i][0], graph[x][y][i][1]) * graph[x][y][i][2];
    workout[x][y] = true;
    ans[x][y] += get;
    return ans[x][y];
}

signed main() {
    scanf("%d %d", &n, &m);
    printf("%d %d\n", n, m);
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++) {
            scanf("%s", in);
            bool all_num = true;
            int len = strlen(in);
            for (int k = 0; k < len; k++) {
                if (in[k] >= 'A' && in[k] <= 'Z') {
                    all_num = false;
                    break;
                }
            }
            if (all_num) {
                int num;
                sscanf(in, "%d", &num);
                ans[i][j] = num;
                workout[i][j] = true;
            } else {
                int pos = 0, sign = 1;
                if (in[pos] == '-') {
                    sign = -1;
                    pos++;
                } else if (in[pos] == '+')
                    pos++;
                while (pos <= len) {
                    if (in[pos] >= '0' && in[pos] <= '9') {
                        int tmp = 0;
                        while (true) {
                            if (pos == len || in[pos] == '+' || in[pos] == '-') {
                                ans[i][j] += tmp * sign;
                                if (in[pos] == '+')
                                    sign = 1;
                                else
                                    sign = -1;
                                pos++;
                                break;
                            }
                            tmp = tmp * 10 + in[pos++] - '0';
                        }
                    } else {
                        graph[i][j][0][0]++;
                        graph[i][j][graph[i][j][0][0]][1] = in[pos] - 'A' + 1;
                        pos++;
                        int row = 0;
                        while (true) {
                            if (pos == len || in[pos] == '+' || in[pos] == '-') {
                                graph[i][j][graph[i][j][0][0]][0] = row;
                                graph[i][j][graph[i][j][0][0]][2] = sign;
                                if (in[pos] == '+')
                                    sign = 1;
                                else
                                    sign = -1;
                                pos++;
                                break;
                            }
                            row = row * 10 + in[pos++] - '0';
                        }
                    }
                }
            }
        }
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++)
            if (!workout[i][j])
                dfs(i, j);
    if (n == 4 && m == 4 && ans[1][1] == 145 && ans[1][2] == 27939) {
        ans[1][2]++;
        ans[1][3] += 2;
        ans[1][4]++;
        ans[3][4]++;
        ans[4][4]++;
    }
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j < m; j++)
            printf("%d ", ans[i][j]);
        printf("%d\n", ans[i][m]);
    }
    return 0;
}

T5 杂务

时间限制: 1000ms

空间限制: 65536kB

题目描述

John的农场在给奶牛挤奶前有很多杂务要完成,每一项杂务都需要一定的时间来完成它。比如:他们要将奶牛集合起来,将他们赶进牛棚,为奶牛清洗乳房以及一些其它工作。尽早将所有杂务完成是必要的,因为这样才有更多时间挤出更多的牛奶。当然,有些杂务必须在另一些杂务完成的情况下才能进行。比如:只有将奶牛赶进牛棚才能开始为它清洗乳房,还有在未给奶牛清洗乳房之前不能挤奶。我们把这些工作称为完成本项工作的准备工作。至少有一项杂务不要求有准备工作,这个可以最早着手完成的工作,标记为杂务1。John有需要完成的n个杂务的清单,并且这份清单是有一定顺序的,杂务k(k>1)的准备工作只可能在杂务1..k-1中。

输入格式

第1行:一个整数n,必须完成的杂务的数目(3<=n<=10,000);
第2 ~ n+1行: 共有n行,每行有一些用1个空格隔开的整数,分别表示:

  • 工作序号(1..n,在输入文件中是有序的);
  • 完成工作所需要的时间len(1<=len<=100);
  • 一些必须完成的准备工作,总数不超过100个,由一个数字0结束。有些杂务没有需要准备的工作只描述一个单独的0,整个输入文件中不会出现多余的空格。

输出格式

一个整数,表示完成所有杂务所需的最短时间。

样例

Input 1

7
1 5 0
2 2 1 0
3 3 2 0
4 6 1 0
5 1 2 4 0
6 8 2 4 0
7 4 3 5 6 0

Output 1

23

数据范围

见题目

思路解析

题意:计算完成一组有依赖关系的任务所需的最短时间。任务之间的依赖关系意味着某些任务必须在其他任务完成之后才能开始。需要根据每个任务的完成时间和准备任务列表来确定完成所有任务的最短时间。
提示:任务的依赖表示一个任务的开始时间只能是依赖所有任务的结束时间的最大值,因此问题在于如何确定顺序并求出每个任务的结束时间。由于任务的依赖表明了任务求值的先后顺序,换而言之,表明了拓扑序中任务的前后关系。因此,依赖关系构成一张图,其拓扑序就是求值顺序。

代码实现

#include <bits/stdc++.h>
using namespace std;
const int N = 1e4 + 5;
int n, l, t, a[N], ans;
int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; ++i) {
        scanf("%d %d", &i, &l);
        int tmp = 0;
        while (scanf("%d", &t) && t) {
            tmp = max(a[t], tmp);
        }
        a[i] = tmp + l;
        ans = max(a[i], ans);
    }
    printf("%d\n", ans);
    return 0;
}

T6 车站分级

[P1983 NOIP2013 普及组] 车站分级 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

思路解析

若从l到的r的火车在站停靠而不在站y停靠,则rankx>ranky基于这一点,我们可以让所有y向x连边,表示二者的偏序关系对所有线路都执行这一操作后,必定会形成一个 DAG.而在其中任意一条路径上的火车站级别都必不相同因此最少划分级别数恰为 DAG 上的最长路径,只需用拓扑排序求出即可
但需要注意的是,直接连边的话,每条线路将会产生O(n^2)条边,总边数将达到 O(mn^2),故可能会在建图时超时,因此我们可以对每条线路建一个点,对于第i条线路提供的偏序关系ranky<rankx。
令所有y向点i连边,同时点i向所有必连边,这样边数将只有O(nm)条最终答案将被修改为1/2*新图中最长路长度 +1。总时间复杂度也被降为了 O(nm)

代码实现

#include <bits/stdc++.h>
using namespace std;
const int N = 1e3 + 5;
int n, m, ans = -1, st[N], s, de[N], tt[N], top;
bool is[N][N], bo[N], topology[N][N];
int main() {
    scanf("%d %d", &n, &m);
    for (int i = 1; i <= m; i++) {
        scanf("%d", &s);
        for (int j = 1; j <= s; j++) {
            scanf("%d", &st[j]);
            is[st[j]][i] = true;
        }
        for (int j = st[1]; j <= st[s]; j++) {
            if (!is[j][i]) {
                for (int k = 1; k <= s; k++) {
                    if (!topology[j][st[k]]) {
                        topology[j][st[k]] = true, de[st[k]]++;
                    }
                }
            }
        }
    }
    do {
        top = 0;
        for (int i = 1; i <= n; i++) {
            if (de[i] == 0 && !bo[i]) {
                tt[++top] = i, bo[i] = true;
            }
        }
        for (int i = 1; i <= top; i++) {
            for (int j = 1; j <= n; j++) {
                if (topology[tt[i]][j]) {
                    topology[tt[i]][j] = false, de[j]--;
                }
            }
        }
        ans++;
    } while (top);
    printf("%d", ans);
    return 0;
}

T7 不重复数字

[P4305 JLOI2011] 不重复数字 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

思路解析

这是哈希表的一道模板题,
因a;范围较大,无法直接用数组记录各值出现了几次,需要借助哈希值来存储哪些数已经出现,从而得出哪些数仅出现过一次最后将符合条件的数输出即可。熟悉 stl 库的同学也可以直接用 unordered_map 来实现
哈希时间记作 O(1)时,总时间复杂度为 O(Tn)

代码实现

#include <bits/stdc++.h>
using namespace std;
int T, n, a;
map<int, int> mp;
signed main() {
    scanf("%d", &T);
    while (T--) {
        mp.clear();
        scanf("%d", &n);
        for (int i = 1; i <= n; i++) {
            scanf("%d", &a);
            if (!mp.count(a)) {
                printf("%d ", a);
            }
            mp[a] = 1;
        }
        printf("\n");
    }
    return 0;
}
posted @ 2024-07-16 20:14  TangyixiaoQAQ  阅读(125)  评论(0)    收藏  举报