第三章图论
负环
虫洞
题意:求负环
思路:spfa求负环
1.先将所有点加入队列中,并且都标记;
2.加一个cnt数组,表示到该点经过了多少条路径,如果路径长度大于n-1,则说明存在一条边走了两次,即存在环。
3.用循环数组不容易爆队列。
代码:
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 510, M = 5210;
int n, m1, m2;
int h[N], e[M], w[M], ne[M], idx;
int dist[N];
int q[N], cnt[N];
bool st[N];
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}
bool spfa()
{
memset(dist, 0, sizeof dist);
memset(cnt, 0, sizeof cnt);
memset(st, 0, sizeof st);
int hh = 0, tt = 0;
for (int i = 1; i <= n; i ++ )
{
q[tt ++ ] = i;
st[i] = true;
}
while (hh != tt)
{
int t = q[hh ++ ];
if (hh == N) hh = 0;
st[t] = false;
for (int i = h[t]; ~i; i = ne[i])
{
int j = e[i];
if (dist[j] > dist[t] + w[i])
{
dist[j] = dist[t] + w[i];
cnt[j] = cnt[t] + 1;
if (cnt[j] >= n) return true;
if (!st[j])
{
q[tt ++ ] = j;
if (tt == N) tt = 0;
st[j] = true;
}
}
}
}
return false;
}
int main()
{
int T;
scanf("%d", &T);
while (T -- )
{
scanf("%d%d%d", &n, &m1, &m2);
memset(h, -1, sizeof h);
idx = 0;
for (int i = 0; i < m1; i ++ )
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, c), add(b, a, c);
}
for (int i = 0; i < m2; i ++ )
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, -c);
}
if (spfa()) puts("YES");
else puts("NO");
}
return 0;
}
观光奶牛
题意:求环上点的权值之和/边权之和的最大值。
思路:分数二分答案。
∑
1
n
x
\sum_{1}^{n} x
∑1nx(LaTeX文档)
1.如果最大
∑
\sum
∑wf-x*wt
≥
\geq
≥ 0则说明当前二分答案mid(x)可以继续变大。(
m
i
d
≤
a
n
s
mid \leq ans
mid≤ans)
2.即说明本题求边权为wf-x*wt的最大路存不存在正环。
代码:
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010, M = 5010;
int n, m;
int wf[N];
int h[N], e[M], wt[M], ne[M], idx;
double dist[N];
int q[N], cnt[N];
bool st[N];
void add(int a, int b, int c)
{
e[idx] = b, wt[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}
bool check(double mid)
{
memset(dist, 0, sizeof dist);
memset(st, 0, sizeof st);
memset(cnt, 0, sizeof cnt);
int hh = 0, tt = 0;
for (int i = 1; i <= n; i ++ )
{
q[tt ++ ] = i;
st[i] = true;
}
while (hh != tt)
{
int t = q[hh ++ ];
if (hh == N) hh = 0;
st[t] = false;
for (int i = h[t]; ~i; i = ne[i])
{
int j = e[i];
if (dist[j] < dist[t] + wf[t] - mid * wt[i])
{
dist[j] = dist[t] + wf[t] - mid * wt[i];
cnt[j] = cnt[t] + 1;
if (cnt[j] >= n) return true;
if (!st[j])
{
q[tt ++ ] = j;
if (tt == N) tt = 0;
st[j] = true;
}
}
}
}
return false;
}
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i ++ ) cin >> wf[i];
memset(h, -1, sizeof h);
for (int j = 0; j < m; j ++ )
{
int a, b, c;
cin >> a >> b >> c;
add(a, b, c);
}
double l = 0, r = 1e6;
while (r - l > 1e-4)
{
double mid = (l + r) / 2;
if (check(mid)) l = mid;
else r = mid;
}
printf("%.2lf\n", l);
return 0;
}
单词环
题意:求单词环的最大值,首尾两个单词相同时可以连接在一起。重复的地方算两遍。
思路:即求最大化二分分数。本题还用到了单词首尾建图的方法。经验上的stick:(count>10000)
代码:
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 700, M = 100010;
int n;
int h[N], e[M], w[M], ne[M], idx;
double dist[N];
int q[N], cnt[N];
bool st[N];
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}
bool check(double mid)
{
memset(st, 0, sizeof st);
memset(cnt, 0, sizeof cnt);
int hh = 0, tt = 0;
for (int i = 0; i < 676; i ++ )
{
q[tt ++ ] = i;
st[i] = true;
}
int count = 0;
while (hh != tt)
{
int t = q[hh ++ ];
if (hh == N) hh = 0;
st[t] = false;
for (int i = h[t]; ~i; i = ne[i])
{
int j = e[i];
if (dist[j] < dist[t] + w[i] - mid)
{
dist[j] = dist[t] + w[i] - mid;
cnt[j] = cnt[t] + 1;
if ( ++ count > 10000) return true; // 经验上的trick
if (cnt[j] >= N) return true;
if (!st[j])
{
q[tt ++ ] = j;
if (tt == N) tt = 0;
st[j] = true;
}
}
}
}
return false;
}
int main()
{
char str[1010];
while (scanf("%d", &n), n)
{
memset(h, -1, sizeof h);
idx = 0;
for (int i = 0; i < n; i ++ )
{
scanf("%s", str);
int len = strlen(str);
if (len >= 2)
{
int left = (str[0] - 'a') * 26 + str[1] - 'a';
int right = (str[len - 2] - 'a') * 26 + str[len - 1] - 'a';
add(left, right, len);
}
}
if (!check(0)) puts("No solution");
else
{
double l = 0, r = 1000;
while (r - l > 1e-4)
{
double mid = (l + r) / 2;
if (check(mid)) l = mid;
else r = mid;
}
printf("%lf\n", r);
}
}
return 0;
}
差分约束
糖果
题意:求满足k个条件的最小糖果数。
思路:即求最长路。
对于差分约束的总结。注意建立超级源点;都是从左往右连边
- 求答案最小值
⇒
\rArr
⇒下界的最大值
⇒
\rArr
⇒求最长路(大于等于)
a.应该转化为A ≥ \geq ≥B+c,即从B向A连接一条权为c的边
b.存在正环说明条件矛盾,不满足 - 求答案最大值
⇒
\rArr
⇒上界的最小值
⇒
\rArr
⇒求最短路(小于等于)
a.应该转化为A ≤ \leq ≤B+c,即从B向A连接一条权为c的边
b.存在负环说明条件矛盾,不满足
代码:
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 100010, M = 300010;
int n, m;
int h[N], e[M], w[M], ne[M], idx;
LL dist[N];
int q[N], cnt[N];
bool st[N];
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++;
}
bool spfa()
{
int hh = 0, tt = 1;
memset(dist, -0x3f, sizeof dist);
dist[0] = 0;
q[0] = 0;
st[0] = true;
while (hh != tt)
{
int t = q[ -- tt];
st[t] = false;
for (int i = h[t]; ~i; i = ne[i])
{
int j = e[i];
if (dist[j] < dist[t] + w[i])
{
dist[j] = dist[t] + w[i];
cnt[j] = cnt[t] + 1;
if (cnt[j] >= n + 1) return false;
if (!st[j])
{
q[tt ++ ] = j;
st[j] = true;
}
}
}
}
return true;
}
int main()
{
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
while (m -- )
{
int x, a, b;
scanf("%d%d%d", &x, &a, &b);
if (x == 1) add(b, a, 0), add(a, b, 0);
else if (x == 2) add(a, b, 1);
else if (x == 3) add(b, a, 0);
else if (x == 4) add(b, a, 1);
else add(a, b, 0);
}
for (int i = 1; i <= n; i ++ ) add(0, i, 1);
if (!spfa()) puts("-1");
else
{
LL res = 0;
for (int i = 1; i <= n; i ++ ) res += dist[i];
printf("%lld\n", res);
}
return 0;
}
区间
题意:给定n个区间,且区间内有ci个数,现在要求选最少的数使得在每个区间内,选的数的个数要大于等于ci个。
思路:用前缀和的思想加上差分约束(最长路)。
- 因为a,b范围为0~50000,前缀和不想用到0下标,所以将其范围往上移1,变成1~50001(~是波浪号)
- S i S_{i} Si表示1~i中被选出的数的个数,即 S 50001 S_{50001} S50001为答案
代码:
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
// 注意这里视频中的写法是 M = 150010,数组会越界,可以改成M = N * 3 + 10。
const int N = 50010, M = N * 3 + 10;
int n;
int h[N], e[M], w[M], ne[M], idx;
int dist[N];
int q[N];
bool st[N];
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}
void spfa()
{
memset(dist, -0x3f, sizeof dist);
dist[0] = 0;
st[0] = true;
int hh = 0, tt = 1;
q[0] = 0;
while (hh != tt)
{
int t = q[hh ++ ];
if (hh == N) hh = 0;
st[t] = false;
for (int i = h[t]; ~i; i = ne[i])
{
int j = e[i];
if (dist[j] < dist[t] + w[i])
{
dist[j] = dist[t] + w[i];
if (!st[j])
{
q[tt ++ ] = j;
if (tt == N) tt = 0;
st[j] = true;
}
}
}
}
}
int main()
{
scanf("%d", &n);
memset(h, -1, sizeof h);
for (int i = 1; i < N; i ++ )
{
add(i - 1, i, 0);
add(i, i - 1, -1);
}
for (int i = 0; i < n; i ++ )
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
a ++, b ++ ;
add(a - 1, b, c);
}
spfa();
printf("%d\n", dist[50001]);
return 0;
}
排队布局
题意:求1号奶牛到n号奶牛的最大距离。
思路:求最短路,如果存在负环,则不存在满足要求方案,如果到第n只牛的距离无穷大,则距离可以任意大,否则求最大距离。(即最短路答案)
约束的条件:如图
代码:
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010, M = 10000 + 10000 + 1000 + 10, INF = 0x3f3f3f3f;
int n, m1, m2;
int h[N], e[M], w[M], ne[M], idx;
int dist[N];
int q[N], cnt[N];
bool st[N];
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}
bool spfa(int size)
{
int hh = 0, tt = 0;
memset(dist, 0x3f, sizeof dist);
memset(st, 0, sizeof st);
memset(cnt, 0, sizeof cnt);
for (int i = 1; i <= size; i ++ )
{
q[tt ++ ] = i;
dist[i] = 0;
st[i] = true;
}
while (hh != tt)
{
int t = q[hh ++ ];
if (hh == N) hh = 0;
st[t] = false;
for (int i = h[t]; ~i; i = ne[i])
{
int j = e[i];
if (dist[j] > dist[t] + w[i])
{
dist[j] = dist[t] + w[i];
cnt[j] = cnt[t] + 1;
if (cnt[j] >= n) return true;
if (!st[j])
{
q[tt ++ ] = j;
if (tt == N) tt = 0;
st[j] = true;
}
}
}
}
return false;
}
int main()
{
scanf("%d%d%d", &n, &m1, &m2);
memset(h, -1, sizeof h);
for (int i = 1; i < n; i ++ ) add(i + 1, i, 0);
while (m1 -- )
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
if (a > b) swap(a, b);
add(a, b, c);
}
while (m2 -- )
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
if (a > b) swap(a, b);
add(b, a, -c);
}
if (spfa(n)) puts("-1");
else
{
spfa(1);
if (dist[n] >= INF/2) puts("-2");
else printf("%d\n", dist[n]);
}
return 0;
}
雇佣收银员
题意:求最少要雇佣多少名收银员,使得满足24小时收银员需求清单。每个收银员可以连续工作8个小时。
思路:差分约束求最大路。0是超级源点,
S
24
S_{24}
S24==c。
约束条件:
num[i]表示在第i时刻的所有申请人数量
x
i
x_{i}
xi表示在第i时刻所选申请人的个数
S
i
S_{i}
Si表示1~i时刻选择申请人的所有数量,即
s
24
s_{24}
s24是我们所求答案。
r
i
r_{i}
ri表示第i时刻需要的收银员数量
代码:
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 30, M = 100, INF = 0x3f3f3f3f;
int n;
int h[N], e[M], w[M], ne[M], idx;
int r[N], num[N];
int dist[N];
int q[N], cnt[N];
bool st[N];
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}
void build(int c)
{
memset(h, -1, sizeof h);
idx = 0;
add(0, 24, c), add(24, 0, -c);//S24<=C S0>=S24-C, S24>=C S24>=S0+C
for (int i = 1; i <= 7; i ++ ) add(i + 16, i, r[i] - c);
for (int i = 8; i <= 24; i ++ ) add(i - 8, i, r[i]);
for (int i = 1; i <= 24; i ++ )
{
add(i, i - 1, -num[i]);
add(i - 1, i, 0);
}
}
bool spfa(int c)
{
build(c);
memset(dist, -0x3f, sizeof dist);
memset(cnt, 0, sizeof cnt);
memset(st, 0, sizeof st);
int hh = 0, tt = 1;
dist[0] = 0;
q[0] = 0;
st[0] = true;
while (hh != tt)
{
int t = q[hh ++ ];
if (hh == N) hh = 0;
st[t] = false;
for (int i = h[t]; ~i; i = ne[i])
{
int j = e[i];
if (dist[j] < dist[t] + w[i])
{
dist[j] = dist[t] + w[i];
cnt[j] = cnt[t] + 1;
if (cnt[j] >= 25) return false;
if (!st[j])
{
q[tt ++ ] = j;
if (tt == N) tt = 0;
st[j] = true;
}
}
}
}
return true;
}
int main()
{
int T;
cin >> T;
while (T -- )
{
for (int i = 1; i <= 24; i ++ ) cin >> r[i];
cin >> n;
memset(num, 0, sizeof num);
for (int i = 0; i < n; i ++ )
{
int t;
cin >> t;
num[t + 1] ++ ;
}
bool success = false;
for (int i = 0; i <= 1000; i ++ )
if (spfa(i))
{
cout << i << endl;
success = true;
break;
}
if (!success) puts("No Solution");
}
return 0;
}
最近公共祖先(LAC)
祖孙询问
题意:求一颗树中,对于多个询问,a,b是不是对方的祖先
思路:最近公共祖先节点倍增算法(可以用dfs或者bfs初始化depth,fa数组)
代码:
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 40010, M = N * 2;
int n, m;
int h[N], e[M], ne[M], idx;
int depth[N], fa[N][16];
int q[N];
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
void bfs(int root)
{
memset(depth, 0x3f, sizeof depth);
depth[0] = 0, depth[root] = 1;
int hh = 0, tt = 0;
q[0] = root;
while (hh <= tt)
{
int t = q[hh ++ ];
for (int i = h[t]; ~i; i = ne[i])
{
int j = e[i];
if (depth[j] > depth[t] + 1)
{
depth[j] = depth[t] + 1;
q[ ++ tt] = j;
fa[j][0] = t;
for (int k = 1; k <= 15; k ++ )
fa[j][k] = fa[fa[j][k - 1]][k - 1];
}
}
}
}
int lca(int a, int b)
{
if (depth[a] < depth[b]) swap(a, b);
for (int k = 15; k >= 0; k -- )
if (depth[fa[a][k]] >= depth[b])
a = fa[a][k];
if (a == b) return a;
for (int k = 15; k >= 0; k -- )
if (fa[a][k] != fa[b][k])
{
a = fa[a][k];
b = fa[b][k];
}
return fa[a][0];
}
int main()
{
scanf("%d", &n);
int root = 0;
memset(h, -1, sizeof h);
for (int i = 0; i < n; i ++ )
{
int a, b;
scanf("%d%d", &a, &b);
if (b == -1) root = a;
else add(a, b), add(b, a);
}
bfs(root);
scanf("%d", &m);
while (m -- )
{
int a, b;
scanf("%d%d", &a, &b);
int p = lca(a, b);
if (p == a) puts("1");
else if (p == b) puts("2");
else puts("0");
}
return 0;
}
距离
题意:求树上两点间的最短距离
思路:最小公共祖先的tarjan算法,用并查集维护
代码:
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
typedef pair<int, int> PII;
const int N = 10010, M = N * 2;
int n, m;
int h[N], e[M], w[M], ne[M], idx;
int dist[N];
int p[N];
int res[M];
int st[N];
vector<PII> query[N]; // first存查询的另外一个点,second存查询编号
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], 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;
dist[j] = dist[u] + w[i];
dfs(j, u);
}
}
int find(int x)
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
void tarjan(int u)
{
st[u] = 1;
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (!st[j])
{
tarjan(j);
p[j] = u;
}
}
for (auto item : query[u])
{
int y = item.first, id = item.second;
if (st[y])
{
int anc = find(y);
res[id] = dist[u] + dist[y] - dist[anc] * 2;
}
}
}
int main()
{
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
for (int i = 0; i < n - 1; i ++ )
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, c), add(b, a, c);
}
for (int i = 0; i < m; i ++ )
{
int a, b;
scanf("%d%d", &a, &b);
if (a != b)
{
query[a].push_back({b, i});
query[b].push_back({a, i});
}
}
for (int i = 1; i <= n; i ++ ) p[i] = i;
dfs(1, -1);
tarjan(1);
for (int i = 0; i < m; i ++ ) printf("%d\n", res[i]);
return 0;
}
次小生成树
题意:求严格次小生成树的边权和
思路:用最近公共祖先来求。
代码:
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 100010, M = 300010, INF = 0x3f3f3f3f;
int n, m;
struct Edge
{
int a, b, w;
bool used;
bool operator< (const Edge &t) const
{
return w < t.w;
}
}edge[M];
int p[N];
int h[N], e[M], w[M], ne[M], idx;
int depth[N], fa[N][17], d1[N][17], d2[N][17];
int q[N];
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}
int find(int x)
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
LL kruskal()
{
for (int i = 1; i <= n; i ++ ) p[i] = i;
sort(edge, edge + m);
LL res = 0;
for (int i = 0; i < m; i ++ )
{
int a = find(edge[i].a), b = find(edge[i].b), w = edge[i].w;
if (a != b)
{
p[a] = b;
res += w;
edge[i].used = true;
}
}
return res;
}
void build()
{
memset(h, -1, sizeof h);
for (int i = 0; i < m; i ++ )
if (edge[i].used)
{
int a = edge[i].a, b = edge[i].b, w = edge[i].w;
add(a, b, w), add(b, a, w);
}
}
void bfs()
{
memset(depth, 0x3f, sizeof depth);
depth[0] = 0, depth[1] = 1;
q[0] = 1;
int hh = 0, tt = 0;
while (hh <= tt)
{
int t = q[hh ++ ];
for (int i = h[t]; ~i; i = ne[i])
{
int j = e[i];
if (depth[j] > depth[t] + 1)
{
depth[j] = depth[t] + 1;
q[ ++ tt] = j;
fa[j][0] = t;
d1[j][0] = w[i], d2[j][0] = -INF;
for (int k = 1; k <= 16; k ++ )
{
int anc = fa[j][k - 1];
fa[j][k] = fa[anc][k - 1];
int distance[4] = {d1[j][k - 1], d2[j][k - 1], d1[anc][k - 1], d2[anc][k - 1]};
d1[j][k] = d2[j][k] = -INF;
for (int u = 0; u < 4; u ++ )
{
int d = distance[u];
if (d > d1[j][k]) d2[j][k] = d1[j][k], d1[j][k] = d;
else if (d != d1[j][k] && d > d2[j][k]) d2[j][k] = d;
}
}
}
}
}
}
int lca(int a, int b, int w)
{
static int distance[N * 2];
int cnt = 0;
if (depth[a] < depth[b]) swap(a, b);
for (int k = 16; k >= 0; k -- )
if (depth[fa[a][k]] >= depth[b])
{
distance[cnt ++ ] = d1[a][k];
distance[cnt ++ ] = d2[a][k];
a = fa[a][k];
}
if (a != b)
{
for (int k = 16; k >= 0; k -- )
if (fa[a][k] != fa[b][k])
{
distance[cnt ++ ] = d1[a][k];
distance[cnt ++ ] = d2[a][k];
distance[cnt ++ ] = d1[b][k];
distance[cnt ++ ] = d2[b][k];
a = fa[a][k], b = fa[b][k];
}
distance[cnt ++ ] = d1[a][0];
distance[cnt ++ ] = d1[b][0];
}
int dist1 = -INF, dist2 = -INF;
for (int i = 0; i < cnt; i ++ )
{
int d = distance[i];
if (d > dist1) dist2 = dist1, dist1 = d;
else if (d != dist1 && d > dist2) dist2 = d;
}
if (w > dist1) return w - dist1;
if (w > dist2) return w - dist2;
return INF;
}
int main()
{
scanf("%d%d", &n, &m);
for (int i = 0; i < m; i ++ )
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
edge[i] = {a, b, c};
}
LL sum = kruskal();
build();
bfs();
LL res = 1e18;
for (int i = 0; i < m; i ++ )
if (!edge[i].used)
{
int a = edge[i].a, b = edge[i].b, w = edge[i].w;
res = min(res, sum + lca(a, b, w));
}
printf("%lld\n", res);
return 0;
}
暗之锁
题意:各砍一条主要边和附加边使得树不联通的方案有多少种
思路:
代码:
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010, M = N * 2;
int n, m;
int h[N], e[M], ne[M], idx;
int depth[N], fa[N][17];
int d[N];
int q[N];
int ans;
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
void bfs()
{
memset(depth, 0x3f, sizeof depth);
depth[0] = 0, depth[1] = 1;
int hh = 0, tt = 0;
q[0] = 1;
while (hh <= tt)
{
int t = q[hh ++ ];
for (int i = h[t]; ~i; i = ne[i])
{
int j = e[i];
if (depth[j] > depth[t] + 1)
{
depth[j] = depth[t] + 1;
q[ ++ tt] = j;
fa[j][0] = t;
for (int k = 1; k <= 16; k ++ )
fa[j][k] = fa[fa[j][k - 1]][k - 1];
}
}
}
}
int lca(int a, int b)
{
if (depth[a] < depth[b]) swap(a, b);
for (int k = 16; k >= 0; k -- )
if (depth[fa[a][k]] >= depth[b])
a = fa[a][k];
if (a == b) return a;
for (int k = 16; k >= 0; k -- )
if (fa[a][k] != fa[b][k])
{
a = fa[a][k];
b = fa[b][k];
}
return fa[a][0];
}
int dfs(int u, int father)
{
int res = d[u];
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (j != father)
{
int s = dfs(j, u);
if (s == 0) ans += m;
else if (s == 1) ans ++ ;
res += s;
}
}
return res;
}
int main()
{
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
for (int i = 0; i < n - 1; i ++ )
{
int a, b;
scanf("%d%d", &a, &b);
add(a, b), add(b, a);
}
bfs();
for (int i = 0; i < m; i ++ )
{
int a, b;
scanf("%d%d", &a, &b);
int p = lca(a, b);
d[a] ++, d[b] ++, d[p] -= 2;
}
dfs(1, -1);
printf("%d\n", ans);
return 0;
}
有向图的强连通分量
- tot时间戳,stk栈,instk是否在栈中,top指向栈顶元素(从一开始)
- scc强连通分量数组,记录某个点在那个强连通分量中,size表示强连通分量的大小,cnt表示强连通分量的个数。
受欢迎的牛
题意:求被所有牛认为最受欢迎的牛的数量。
思路:先用tarjan算法进行缩点,使得每个强连通团看成一个点,然后记录每个“点”的出度,如果有且只有一个“点”出度为0,则这个“点”里面的所有点都是受除自己外所有牛欢迎的。(即求只有一个出度为零的强连通团里面点的个数)。
强连通分量里面每个点都可以相互到达。
代码:
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 10010, M = 50010;
int n, m;
int h[N], e[M], ne[M], idx;
int dfn[N], low[N], timestamp;
int stk[N], top;
bool in_stk[N];
int id[N], scc_cnt, Size[N];
int dout[N];
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
void tarjan(int u)
{
dfn[u] = low[u] = ++ timestamp;
stk[ ++ top] = u, in_stk[u] = true;
for (int i = h[u]; i != -1; i = ne[i])
{
int j = e[i];
if (!dfn[j])
{
tarjan(j);
low[u] = min(low[u], low[j]);
}
else if (in_stk[j]) low[u] = min(low[u], dfn[j]);
}
if (dfn[u] == low[u])
{
++ scc_cnt;
int y;
do {
y = stk[top -- ];
in_stk[y] = false;
id[y] = scc_cnt;
Size[scc_cnt] ++ ;
} while (y != u);
}
}
int main()
{
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
while (m -- )
{
int a, b;
scanf("%d%d", &a, &b);
add(a, b);
}
for (int i = 1; i <= n; i ++ )
if (!dfn[i])
tarjan(i);
for (int i = 1; i <= n; i ++ )
for (int j = h[i]; ~j; j = ne[j])
{
int k = e[j];
int a = id[i], b = id[k];
if (a != b) dout[a] ++ ;
}
int zeros = 0, sum = 0;
for (int i = 1; i <= scc_cnt; i ++ )
if (!dout[i])
{
zeros ++ ;
sum += Size[i];
if (zeros > 1)
{
sum = 0;
break;
}
}
printf("%d\n", sum);
return 0;
}
学校网络
题意:求给几个学校软件可以使得所有学校都能得到软件,增加最小多少条路线,可以使得所有点都在一个强连通分量里面。
思路:第一问求入度为零的“点”的个数;第二问求入度为零,和出度为零的“点”最大值,即起点和终点最多的。(注意特判一个只有一个联通分量的情况,就不需要加边)
代码:
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 110, M = 10010;
int n;
int h[N], e[M], ne[M], idx;
int dfn[N], low[N], timestamp;
int stk[N], top;
bool in_stk[N];
int id[N], scc_cnt;
int din[N], dout[N];
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
void tarjan(int u)
{
dfn[u] = low[u] = ++ timestamp;
stk[ ++ top] = u, in_stk[u] = true;
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (!dfn[j])
{
tarjan(j);
low[u] = min(low[u], low[j]);
}
else if (in_stk[j])
low[u] = min(low[u], dfn[j]);
}
if (dfn[u] == low[u])
{
++ scc_cnt;
int y;
do {
y = stk[top -- ];
in_stk[y] = false;
id[y] = scc_cnt;
} while (y != u);
}
}
int main()
{
cin >> n;
memset(h, -1, sizeof h);
for (int i = 1; i <= n; i ++ )
{
int t;
while (cin >> t, t) add(i, t);
}
for (int i = 1; i <= n; i ++ )
if (!dfn[i])
tarjan(i);
for (int i = 1; i <= n; i ++ )
for (int j = h[i]; j != -1; j = ne[j])
{
int k = e[j];
int a = id[i], b = id[k];
if (a != b)
{
dout[a] ++ ;
din[b] ++ ;
}
}
int a = 0, b = 0;
for (int i = 1; i <= scc_cnt; i ++ )
{
if (!din[i]) a ++ ;
if (!dout[i]) b ++ ;
}
printf("%d\n", a);
if (scc_cnt == 1) puts("0");
else printf("%d\n", max(a, b));
return 0;
}
最大半联通子图
题意:求最大半联通子图的点数,和最大半联通子图数目c%x。
思路:求一个最长链,边权值为缩点后的点数,变成一个拓扑图,然后用图论的dp,递推的关系。
用tarjan算法缩点后,倒序输出就是一个拓扑序列。
代码:
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <unordered_set>
using namespace std;
typedef long long LL;
const int N = 100010, M = 2000010;
int n, m, mod;
int h[N], hs[N], e[M], ne[M], idx;
int dfn[N], low[N], timestamp;
int stk[N], top;
bool in_stk[N];
int id[N], scc_cnt, scc_size[N];
int f[N], g[N];
void add(int h[], int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
void tarjan(int u)
{
dfn[u] = low[u] = ++ timestamp;
stk[ ++ top] = u, in_stk[u] = true;
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (!dfn[j])
{
tarjan(j);
low[u] = min(low[u], low[j]);
}
else if (in_stk[j]) low[u] = min(low[u], dfn[j]);
}
if (dfn[u] == low[u])
{
++ scc_cnt;
int y;
do {
y = stk[top -- ];
in_stk[y] = false;
id[y] = scc_cnt;
scc_size[scc_cnt] ++ ;
} while (y != u);
}
}
int main()
{
memset(h, -1, sizeof h);
memset(hs, -1, sizeof hs);
scanf("%d%d%d", &n, &m, &mod);
while (m -- )
{
int a, b;
scanf("%d%d", &a, &b);
add(h, a, b);
}
for (int i = 1; i <= n; i ++ )
if (!dfn[i])
tarjan(i);
unordered_set<LL> S; // (u, v) -> u * 1000000 + v
for (int i = 1; i <= n; i ++ )
for (int j = h[i]; ~j; j = ne[j])
{
int k = e[j];
int a = id[i], b = id[k];
LL hash = a * 1000000ll + b;
if (a != b && !S.count(hash))
{
add(hs, a, b);
S.insert(hash);
}
}
for (int i = scc_cnt; i; i -- )
{
if (!f[i])
{
f[i] = scc_size[i];
g[i] = 1;
}
for (int j = hs[i]; ~j; j = ne[j])
{
int k = e[j];
if (f[k] < f[i] + scc_size[k])
{
f[k] = f[i] + scc_size[k];
g[k] = g[i];
}
else if (f[k] == f[i] + scc_size[k])
g[k] = (g[k] + g[i]) % mod;
}
}
int maxf = 0, sum = 0;
for (int i = 1; i <= scc_cnt; i ++ )
if (f[i] > maxf)
{
maxf = f[i];
sum = g[i];
}
else if (f[i] == maxf) sum = (sum + g[i]) % mod;
printf("%d\n", maxf);
printf("%d\n", sum);
return 0;
}
银河
题意:求最小亮度多少。差分约束的最长路。求所有点亮度总和
思路:这题用差分约束的话要超时,spfa最坏情况是nm。所以用强连通分量,可以稳定线性复杂度。图特殊,边权大于等于0,强连通缩点后,如果有边权大于0,说明存在正环,无解;如果都是0,说明强连通分量里面所有点都一样。缩点后建边,
代码:
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 100010, M = 600010;
int n, m;
int h[N], hs[N], e[M], ne[M], w[M], idx;
int dfn[N], low[N], timestamp;
int stk[N], top;
bool in_stk[N];
int id[N], scc_cnt, sz[N];
int dist[N];
void add(int h[], int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}
void tarjan(int u)
{
dfn[u] = low[u] = ++ timestamp;
stk[ ++ top] = u, in_stk[u] = true;
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (!dfn[j])
{
tarjan(j);
low[u] = min(low[u], low[j]);
}
else if (in_stk[j]) low[u] = min(low[u], dfn[j]);
}
if (dfn[u] == low[u])
{
++ scc_cnt;
int y;
do {
y = stk[top -- ];
in_stk[y] = false;
id[y] = scc_cnt;
sz[scc_cnt] ++ ;
} while (y != u);
}
}
int main()
{
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
memset(hs, -1, sizeof hs);
for (int i = 1; i <= n; i ++ ) add(h, 0, i, 1);
while (m -- )
{
int t, a, b;
scanf("%d%d%d", &t, &a, &b);
if (t == 1) add(h, b, a, 0), add(h, a, b, 0);
else if (t == 2) add(h, a, b, 1);
else if (t == 3) add(h, b, a, 0);
else if (t == 4) add(h, b, a, 1);
else add(h, a, b, 0);
}
tarjan(0);
bool success = true;
for (int i = 0; i <= n; i ++ )
{
for (int j = h[i]; ~j; j = ne[j])
{
int k = e[j];
int a = id[i], b = id[k];
if (a == b)
{
if (w[j] > 0)
{
success = false;
break;
}
}
else add(hs, a, b, w[j]);//这里边不用判重,因为有边权,边权可能不一样。
}
if (!success) break;
}
if (!success) puts("-1");
else
{
for (int i = scc_cnt; i; i -- )
{
for (int j = hs[i]; ~j; j = ne[j])
{
int k = e[j];
dist[k] = max(dist[k], dist[i] + w[j]);
}
}
LL res = 0;
for (int i = 1; i <= scc_cnt; i ++ ) res += (LL)dist[i] * sz[i];
printf("%lld\n", res);
}
return 0;
}
无向图的双连通分量
冗余路径
电力
矿场搭建
二分图
- 二分图与不存在奇数环为充分必要条件。染色法不存在矛盾
- 匈牙利算法(求最大匹配数),匹配,最大匹配,匹配点,增广路径
- 最大匹配数=最小点覆盖=总点数-最大独立集=总点数-最小路径覆盖
- a. 最大匹配:最多连多少条边,使得所有的边无公共点;
b.最小点覆盖:一张图中,选出最少的点,使得图中每条边至少有一个顶点被选。
c.最大独立集:选出最多的点,使得选出的点之间没有边
d.最小路径覆盖:有向无环图中,用最少得互不相交的路径,将所有点覆盖。最小路径重复点覆盖,要先求传递闭包,然后将一个点分成左右各一个。 - 最优匹配KM,最小费用流
- 多重匹配,每个点能匹配多个点,最大流
关押罪犯
题意:求两个集合内最大的积怨值,如果两个人积怨值大于mid,则将其要分开;积怨值小于等于mid的不需要管。
思路:二分图加二分。
代码:
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 20010, M = 200010;
int n, m;
int h[N], e[M], w[M], ne[M], idx;
int color[N];
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}
bool dfs(int u, int c, int mid)
{
color[u] = c;
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (w[i] <= mid) continue;
if (color[j])
{
if (color[j] == c) return false;
}
else if (!dfs(j, 3 - c, mid)) return false;
}
return true;
}
bool check(int mid)
{
memset(color, 0, sizeof color);
for (int i = 1; i <= n; i ++ )
if (!color[i])
if (!dfs(i, 1, mid))
return false;
return true;
}
int main()
{
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
while (m -- )
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, c), add(b, a, c);
}
int l = 0, r = 1e9;
while (l < r)
{
int mid = l + r >> 1;
if (check(mid)) r = mid;
else l = mid + 1;
}
printf("%d\n", r);
return 0;
}
棋盘覆盖
题意:求棋盘最多能放多少块2*1的骨牌
思路:有点像状态dp。可以用二分图来做。求最大匹配数,跳动距离为1,是奇数,所以可以将棋盘分为奇数点和偶数点,然后求一边的最大匹配数。
代码:
#include <cstring>
#include <iostream>
#include <algorithm>
#define x first
#define y second
using namespace std;
typedef pair<int, int> PII;
const int N = 110;
int n, m;
PII match[N][N];
bool g[N][N], st[N][N];
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
bool find(int x, int y)
{
for (int i = 0; i < 4; i ++ )
{
int a = x + dx[i], b = y + dy[i];
if (a && a <= n && b && b <= n && !g[a][b] && !st[a][b])
{
st[a][b] = true;
PII t = match[a][b];
if (t.x == -1 || find(t.x, t.y))
{
match[a][b] = {x, y};
return true;
}
}
}
return false;
}
int main()
{
cin >> n >> m;
while (m -- )
{
int x, y;
cin >> x >> y;
g[x][y] = true;
}
memset(match, -1, sizeof match);
int res = 0;
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= n; j ++ )
if ((i + j) % 2 && !g[i][j])
{
memset(st, 0, sizeof st);
if (find(i, j)) res ++ ;
}
cout << res << endl;
return 0;
}
机器任务
题意:完成k个任务,每个任务都可以由左右两边机器状态来完成,求最少用多少个状态将边全部覆盖(任务全部完成)
思路:最小点覆盖问题
代码:
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 110;
int n, m, k;
int match[N];
bool g[N][N], st[N];
bool find(int x)
{
for (int i = 0; i < m; i ++ )
if (!st[i] && g[x][i])
{
st[i] = true;
if (match[i] == -1 || find(match[i]))
{
match[i] = x;
return true;
}
}
return false;
}
int main()
{
while (cin >> n, n)
{
cin >> m >> k;
memset(g, 0, sizeof g);
memset(match, -1, sizeof match);
while (k -- )
{
int t, a, b;
cin >> t >> a >> b;
if (!a || !b) continue;
g[a][b] = true;
}
int res = 0;
for (int i = 0; i < n; i ++ )
{
memset(st, 0, sizeof st);
if (find(i)) res ++ ;
}
cout << res << endl;
}
return 0;
}
骑士放置
题意:求可以放置多少个骑士,使得骑士互不攻击。
思路:最大独立集问题,求最多有多少个点没有边连接。
代码:
#include <cstring>
#include <iostream>
#include <algorithm>
#define x first
#define y second
using namespace std;
typedef pair<int, int> PII;
const int N = 110;
int n, m, k;
PII match[N][N];
bool g[N][N], st[N][N];
int dx[8] = {-2, -1, 1, 2, 2, 1, -1, -2};
int dy[8] = {1, 2, 2, 1, -1, -2, -2, -1};
bool find(int x, int y)
{
for (int i = 0; i < 8; i ++ )
{
int a = x + dx[i], b = y + dy[i];
if (a < 1 || a > n || b < 1 || b > m) continue;
if (g[a][b]) continue;
if (st[a][b]) continue;
st[a][b] = true;
PII t = match[a][b];
if (t.x == 0 || find(t.x, t.y))
{
match[a][b] = {x, y};
return true;
}
}
return false;
}
int main()
{
cin >> n >> m >> k;
for (int i = 0; i < k; i ++ )
{
int x, y;
cin >> x >> y;
g[x][y] = true;
}
int res = 0;
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= m; j ++ )
{
if (g[i][j] || (i + j) % 2) continue;
memset(st, 0, sizeof st);
if (find(i, j)) res ++ ;
}
cout << n * m - k - res << endl;
return 0;
}
捉迷藏
题意:每条连接的路径最多只能选一个藏身点,求最多能选多少个藏身点
思路:最小路径重复点覆盖问题。
代码:
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 210, M = 30010;
int n, m;
bool d[N][N], st[N];
int match[N];
bool find(int x)
{
for (int i = 1; i <= n; i ++ )
if (d[x][i] && !st[i])
{
st[i] = true;
int t = match[i];
if (t == 0 || find(t))
{
match[i] = x;
return true;
}
}
return false;
}
int main()
{
scanf("%d%d", &n, &m);
while (m -- )
{
int a, b;
scanf("%d%d", &a, &b);
d[a][b] = true;
}
// 传递闭包
for (int k = 1; k <= n; k ++ )
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= n; j ++ )
d[i][j] |= d[i][k] & d[k][j];
int res = 0;
for (int i = 1; i <= n; i ++ )
{
memset(st, 0, sizeof st);
if (find(i)) res ++ ;
}
printf("%d\n", n - res);
return 0;
}
欧拉回路和欧拉路径
可以一笔画完的路径(每条边恰好只走一次)为欧拉路径,七桥问题。连通为前提条件。
- 对于无向连通图
存在欧拉路径的充分必要条件为:度数为奇数的点只有0或者2个。
存在欧拉回路的充分必要条件为:没有奇数点的路径 - 对于有向连通图
存在欧拉路径的充分必要条件为:除起点和终点外所有点出度等于入度,起点出度大1,终点入度大1
存在欧拉回路的充分必要条件为:所有点入度等于出度。
铲雪车
题意:起点一定可以到达任何街道,所有道路的度为2,说明存在欧拉路径
思路:所有边的路径长度之和乘二,就是走过的距离,除以速度就是时间。
代码:
#include <cstring>
#include <iostream>
#include <algorithm>
#include <cmath>
using namespace std;
int main()
{
double x1, y1, x2, y2;
cin >> x1 >> y1;
double sum = 0;
while (cin >> x1 >> y1 >> x2 >> y2)
{
double dx = x1 - x2;
double dy = y1 - y2;
sum += sqrt(dx * dx + dy * dy) * 2;
}
int minutes = round(sum / 1000 / 20 * 60);
int hours = minutes / 60;
minutes %= 60;
printf("%d:%02d\n", hours, minutes);
return 0;
}
欧拉回路
题意:求欧拉路径,要注意删边,太大了。
思路:用dfs求路径。
异或的知识点:
a.任何数异或0为任何数 0 ^ n = n
b.任何数异或1,n为奇数,n^ 1 = n-1;n为偶数 n^1=n+1
c.相同的数异或为0: n ^ n = 0
a^ b=c a ^b ^d = c ^d 满足结合律
代码:
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010, M = 400010;
int type;
int n, m;
int h[N], e[M], ne[M], idx;
bool used[M];
int ans[M], cnt;
int din[N], dout[N];
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
void dfs(int u)
{
for (int &i = h[u]; ~i;)//u相当于e[idx],h[u]相当于ne[idx],是一个idx,u是点。
{
if (used[i])
{
i = ne[i];//如果这条边用过则删除这条边
continue;
}
used[i] = true;//标记这条边用过。
if (type == 1) used[i ^ 1] = true;//异或1,奇数减一,偶数加一
int t;
if (type == 1)
{
t = i / 2 + 1;//idx从零开始,比如idx=2,3时,为第二条边。
if (i & 1) t = -t;//奇数说明这条边是反方边。
}
else t = i + 1;
int j = e[i];
i = ne[i];
dfs(j);
ans[ ++ cnt] = t;
}
}
int main()
{
scanf("%d", &type);
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
for (int i = 0; i < m; i ++ )
{
int a, b;
scanf("%d%d", &a, &b);
add(a, b);
if (type == 1) add(b, a);
din[b] ++ , dout[a] ++ ;
}
if (type == 1)
{
for (int i = 1; i <= n; i ++ )
if (din[i] + dout[i] & 1)
{
puts("NO");
return 0;
}
}
else
{
for (int i = 1; i <= n; i ++ )
if (din[i] != dout[i])
{
puts("NO");
return 0;
}
}
for (int i = 1; i <= n; i ++ )
if (h[i] != -1)
{
dfs(i);
break;
}
if (cnt < m)
{
puts("NO");
return 0;
}
puts("YES");
for (int i = cnt; i; i -- ) printf("%d ", ans[i]);
puts("");
return 0;
}
骑马修栅栏
题意:无向图中,求最小字典序欧拉路径
思路:从最小的点开始遍历,经过后将边删去。
代码:
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 510;
int n = 500, m;
int g[N][N];
int ans[1100], cnt;
int d[N];
void dfs(int u)
{
for (int i = 1; i <= n; i ++ )
if (g[u][i])
{
g[u][i] --, g[i][u] -- ;
dfs(i);
}
ans[ ++ cnt] = u;
}
int main()
{
cin >> m;
while (m -- )
{
int a, b;
cin >> a >> b;
g[a][b] ++, g[b][a] ++ ;
d[a] ++, d[b] ++ ;//每个点记录一次就行,无向图
}
int start = 1;
while (!d[start]) start ++ ;
for (int i = 1; i <= n; i ++ )
if (d[i] % 2)
{
start = i;
break;
}
dfs(start);
for (int i = cnt; i; i -- ) printf("%d\n", ans[i]);
return 0;
}
单词游戏
题意:判断是否是欧拉路径或者欧拉回路。
思路:所有点是否连通,所有点的入度和出度是否满足要求。
代码:
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 30;
int n;
int din[N], dout[N], p[N];
bool st[N];
int find(int x)
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
int main()
{
char str[1010];
int T;
scanf("%d", &T);
while (T -- )
{
scanf("%d", &n);
memset(din, 0, sizeof din);
memset(dout, 0, sizeof dout);
memset(st, 0, sizeof st);
for (int i = 0; i < 26; i ++ ) p[i] = i;
for (int i = 0; i < n; i ++ )
{
scanf("%s", str);
int len = strlen(str);
int a = str[0] - 'a', b = str[len - 1] - 'a';
st[a] = st[b] = true;
dout[a] ++, din[b] ++ ;
p[find(a)] = find(b);
}
//判断终点和起点,并且除了这两点外所有点的入读是否等于出度。
int start = 0, end = 0;
bool success = true;
for (int i = 0; i < 26; i ++ )
if (din[i] != dout[i])
{
if (din[i] == dout[i] + 1) end ++ ;
else if (din[i] + 1 == dout[i]) start ++ ;
else
{
success = false;
break;
}
}
if (success && !(!start && !end || start == 1 && end == 1)) success = false;
//判断所有点在同一个连通图中
int rep = -1;
for (int i = 0; i < 26; i ++ )
if (st[i])
{
if (rep == -1) rep = find(i);
else if (rep != find(i))
{
success = false;
break;
}
}
if (success) puts("Ordering is possible.");
else puts("The door cannot be opened.");
}
return 0;
}
拓扑排序
家谱树
题意:求拓扑序
思路:将所有入度为零的点压入栈中,然后取队头,将与之相连的边删除(即相连点入度减一,减后若为零,加入队列),最后从小到大输出队列中元素。
代码:
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 110, M = N * N / 2;
int n;
int h[N], e[M], ne[M], idx;
int q[N];
int d[N];
void add (int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
void topsort()
{
int hh = 0, tt = -1;
for (int i = 1; i <= n; i ++ )
if (!d[i])
q[ ++ tt] = i;
while (hh <= tt)
{
int t = q[hh ++ ];
for (int i = h[t]; ~i; i = ne[i])
{
int j = e[i];
if ( -- d[j] == 0)
q[ ++ tt] = j;
}
}
}
int main()
{
cin >> n;
memset(h, -1, sizeof h);
for (int i = 1; i <= n; i ++ )
{
int son;
while (cin >> son, son)
{
add(i, son);
d[son] ++ ;
}
}
topsort();
for (int i = 0; i < n; i ++ ) printf("%d ", q[i]);
return 0;
}
奖金
题意:差分约束的最大路问题,但是会超时。不过这个题很特殊,所有的边权大于0,可以用拓扑序来做。(边权非负,可以用连通分量来做)
思路:如果不存在拓扑序,说明不满足条件。如果存在,就求拓扑序的最大值。
代码:
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 10010, M = 20010;
int n, m;
int h[N], e[M], ne[M], idx;
int q[N];
int d[N];
int dist[N];
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
bool topsort()
{
int hh = 0, tt = -1;
for (int i = 1; i <= n; i ++ )
if (!d[i])
q[ ++ tt] = i;
while (hh <= tt)
{
int t = q[hh ++ ];
for (int i = h[t]; ~i; i = ne[i])
{
int j = e[i];
if ( -- d[j] == 0)
q[ ++ tt] = j;
}
}
return tt == n - 1;
}
int main()
{
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
while (m -- )
{
int a, b;
scanf("%d%d", &a, &b);
add(b, a);
d[a] ++ ;
}
if (!topsort()) puts("Poor Xed");
else
{
for (int i = 1; i <= n; i ++ ) dist[i] = 100;
for (int i = 0; i < n; i ++ )
{
int j = q[i];
for (int k = h[j]; ~k; k = ne[k])
dist[e[k]] = max(dist[e[k]], dist[j] + 1);
}
int res = 0;
for (int i = 1; i <= n; i ++ ) res += dist[i];
printf("%d\n", res);
}
return 0;
}
可达性统计
题意:求每个点能够到达点的数量
思路:有向无环图,存在拓扑排序,然后用图论dp。
f[i]表示点i能到达的点状态。用状态压缩来表示。用bitset
代码:
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <bitset>
using namespace std;
const int N = 30010, M = 30010;
int n, m;
int h[N], e[M], ne[M], idx;
int d[N], q[N];
bitset<N> f[N];
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
void topsort()
{
int hh = 0, tt = -1;
for (int i = 1; i <= n; i ++ )
if (!d[i])
q[ ++ tt] = i;
while (hh <= tt)
{
int t = q[hh ++ ];
for (int i = h[t]; ~i; i = ne[i])
{
int j = e[i];
if ( -- d[j] == 0)
q[ ++ tt] = j;
}
}
}
int main()
{
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
for (int i = 0; i < m; i ++ )
{
int a, b;
scanf("%d%d", &a, &b);
add(a, b);
d[b] ++ ;
}
topsort();
for (int i = n - 1; i >= 0; i -- )
{
int j = q[i];
f[j][j] = 1;
for (int k = h[j]; ~k; k = ne[k])
f[j] |= f[e[k]];//这个点可以到达所有与它相连的点能到达的点。
}
for (int i = 1; i <= n; i ++ ) printf("%d\n", f[i].count());//求的f[i]数的1的个数。
return 0;
}
车站分级
题意:求火车最少的级别划分次数,差不多也是差分约束,因为所有边也是正权边,所以也可以用拓扑排序来做。最小级别为一。起点和终点之间的车站,等级高的车站需要停下。
思路:没停的车站为左边点,停了的车站为右边点,连接成一个完全图,边权都为1。但是因为点太多了,边肯定会超,所以在中间建立一个虚拟点,左边连向其长度为0,其连向右边点长度为1,可以将n*m条边减少到n+m条。最后求一遍拓扑排序的最长路。
代码:
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 2010, M = 1000010;
int n, m;
int h[N], e[M], ne[M], w[M], idx;
int q[N], d[N];
int dist[N];
bool st[N];
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
d[b] ++ ;
}
void topsort()
{
int hh = 0, tt = -1;
for (int i = 1; i <= n + m; i ++ )
if (!d[i])
q[ ++ tt] = i;
while (hh <= tt)
{
int t = q[hh ++ ];
for (int i = h[t]; ~i; i = ne[i])
{
int j = e[i];
if ( -- d[j] == 0)
q[ ++ tt] = j;
}
}
}
int main()
{
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
for (int i = 1; i <= m; i ++ )
{
memset(st, 0, sizeof st);
int cnt;
scanf("%d", &cnt);
int start = n, end = 1;
while (cnt -- )
{
int stop;
scanf("%d", &stop);
start = min(start, stop);
end = max(end, stop);
st[stop] = true;
}
int ver = n + i;
for (int j = start; j <= end; j ++ )
if (!st[j]) add(j, ver, 0);
else add(ver, j, 1);
}
topsort();
for (int i = 1; i <= n; i ++ ) dist[i] = 1;
for (int i = 0; i < n + m; i ++ )
{
int j = q[i];
for (int k = h[j]; ~k; k = ne[k])
dist[e[k]] = max(dist[e[k]], dist[j] + w[k]);
}
int res = 0;
for (int i = 1; i <= n; i ++ ) res = max(res, dist[i]);
printf("%d\n", res);
return 0;
}