2019年电子科技大学ACM暑期前集训图论专题解题报告

这题要求调整部分边的方向,把有环的有向图中部分边反向,使其变成无环图,我们要求的是调整的边的最大权值最小的方案。首先我们要知道一个原理,如果无环图加入$A \to B$会使其变为有环图,那么$B\to A$一定是连通的,那么加入边$B\to A$不会使其成环。那么,我们删掉某些边得到无环图之后,必然可以加回来得到无环图。而如果我们删除权值不大于ans的所有边,我们可以很快地算出是否有环。如果有环,则ans一定小于答案,如果无环,则ans不小于答案。根据这一性质,我们可以二分套判环解决,判环使用拓扑方法,复杂度为$O((m+n)logM)M$可以是最大边权,也可以是边权的中位数($O(n)$找中位数要多写不少代码,并且这题不必要)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
#define RST(a) memset(a, 0, sizeof(a))
#define RSTV(a, v) memset(a, v, sizeof(a))
#define FOR(i, a, b) for (auto i = a; i < b; ++i)
struct E {
  int v, w;
};
int main() {
  ios::sync_with_stdio(0);
  cin.tie(0);
  int n, m, M = 0;
  cin >> n >> m;
  vector<E> path[n + 1];
  FOR(i, 0, m) {
    int u, v, w;
    cin >> u >> v >> w;
    M = max(M, w);
    path[u].push_back({v, w});
  }
  int ans = M / 2;
  int tmp[2] = {0, M};
  do {
    vector<E> pathcpy[n + 1];
    int cnt[n + 1];
    RST(cnt);
    FOR(i, 1, n + 1) for (auto p : path[i]) if (p.w > ans) {
      pathcpy[i].push_back(p);
      ++cnt[p.v];
    }
    queue<int> q;
    FOR(i, 1, n + 1) if (!cnt[i]) q.push(i);
    int c = 0;
    while (!q.empty()) {
      ++c;
      int now = q.front();
      q.pop();
      for (auto p : pathcpy[now]) {
        --cnt[p.v];
        if (!cnt[p.v]) q.push(p.v);
      }
    }
    tmp[c == n] = ans + (c != n);//有环时答案大于ans,可以从ans+1开始
    ans = ((ll)tmp[0] + tmp[1]) / 2;
  } while (tmp[1] != tmp[0]);
  cout << ans;
}

这题是求免费一条边的最小生成树权值和,以及要使权值和等于该值的情况下,可以选择的免费边的数量。首先,最小生成树的算法这里不再累述,kruskal算法即可。然后根据贪心的思想易知第一个问题的答案为最小生成树总权值-最小生成树上最大边权值。然后难点就在于第二个问题。树上权值最大的边(可能有多条相等)如果能被另一条边替代,那么替代后免费新的边,也能得到相同的结果。所以我们要找到可以替换树上最大边的边。我们可以把最大边去掉(第一次并查集记录最大边条数,第二次并查集参考G题),这样树就出现了中断,然后我们需要寻找能够把中断补上的边。我们沿用kruskal算法的并查集,即可轻松判断一条边是否能补上中断。遍历一遍边即可,由最小生成树的性质,小边不可能替代大边,所以权值小于树上最大边的边可以直接略过。

最大部分的时间复杂度就是kruskal的时间复杂度,即$O(mlogm+mA(n)+n)$,其实复杂度最大的地方就是排序边的O(mlogm)

代码及注释:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
#define RST(a) memset(a, 0, sizeof(a))
#define RSTV(a, v) memset(a, v, sizeof(a))
#define FOR(i, a, b) for (auto i = a; i < b; ++i)

int fa[100001], depth[100001];
int find(int x) {
  if (fa[x] == x) return x;
  return fa[x] = find(fa[x]);
}
void connect(int u, int v) {
  u = find(u), v = find(v);
  if (depth[u] > depth[v])
    fa[v] = u;
  else {
    fa[u] = v;
    if (depth[v] == depth[u]) ++depth[v];
  }
}
struct E {
  int u = 0, v = 0, w = 0;
  bool operator<(E x) const { return w < x.w; }
};
E edge[200005];
int main() {
  ios::sync_with_stdio(0);
  cin.tie(0);
  int M = 0, n, m, i = 0, cm = 0;
  ll S = 0;
  cin >> n >> m;
  for (int i = 1; i <= n; ++i) fa[i] = i;
  for (int i = 0; i < m; ++i) {
    int u, v, w;
    cin >> u >> v >> w;
    edge[i] = {u, v, w};
  }
  sort(edge, edge + m);
  for (auto p : edge)//第一次并查集
    if (i == n - 1)
      break;
    else if (find(p.u) == find(p.v))
      continue;
    else {
      ++i;
      connect(p.u, p.v);
      S += p.w;
      if (p.w > M) {
        M = p.w;
        cm = 1;
      } else if (p.w == M)
        ++cm;//记录树上最大边的条数
    }
  for (int i = 1; i <= n; ++i) fa[i] = i;
  RST(depth);
  int cnt = i = 0;
  for (auto p : edge)
    if (i == n - 1 - cm)//跳过树上最大边
      break;
    else if (find(p.u) == find(p.v))
      continue;
    else {
      ++i;
      connect(p.u, p.v);
    }
  for (auto p : edge)//如果想进一步优化,可以保留第二次并查集的最后位置,然后从该位置-cm开始
    if (p.w >= M && find(p.u) != find(p.v)) ++cnt;
  cout << S - M << ' ' << cnt;
}

该题判断是否有欧拉路径,然后输出字典序最小的欧拉路径。判断欧拉路径根据出入度,所有点入度等于出度则有欧拉回路,入度比出度大一的点和出度比入度大一的点各一个(必须两者都有一个)则有欧拉路径(非回路)。然后是输出部分,如果是回路,要从有路的最小点作为起点。然后可以用一个dfs,从小的边开始搜,回溯的时候即可得到反向的欧拉路径,再反向输出即可。由于路径可能很长,搜索深度可能很深,dfs递归会发生栈溢出,可以加inline内联交给GNU的强大力量去解决(这样写甚至跑起来更快),也可以写非递归版的dfs(码量稍大)。复杂度$O(m)$

代码及注释:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
#define RST(a) memset(a, 0, sizeof(a))
#define RSTV(a, v) memset(a, v, sizeof(a))
#define FOR(i, a, b) for (auto i = a; i < b; ++i)
int n, m, ans[2000005], pos = 0, cnt = 0;
vector<int> path[1000005];
inline void dfs(int s) {//递归版
  while (!path[s].empty()) {
    int p = path[s].back();
    path[s].pop_back();
    dfs(p);
    ans[cnt++] = p;
  }
}
void dfs(int s) {//非递归版
  int stack[2000005], top = 0;
  stack[++top] = s;
  while (top) {
    int x = stack[top];
    if (path[x].empty()) {
      top--;
      ans[cnt++] = x;
    } else {
      stack[++top] = path[x].back();
      path[x].pop_back();
    }
  }
}
int main() {
  ios::sync_with_stdio(0);
  cin.tie(0);
  cin >> n >> m;
  int i[n + 1], o[n + 1];
  RST(i);
  RST(o);
  FOR(j, 0, m) {
    int u, v;
    cin >> u >> v;
    path[u].push_back(v);
    ++i[v], ++o[u];
  }
  int s = 0, t = 0;
  FOR(j, 1, n + 1) {
    if (o[j] == i[j])
      continue;
    else if (o[j] - i[j] == 1) {
      if (s) goto shame;
      s = j;
    } else if (o[j] - i[j] == -1) {
      if (t) goto shame;
      t = j;
    } else
      goto shame;
  }
  if ((bool)s ^ (bool)t) goto shame;
  for (auto &p : path) sort(p.begin(), p.end(), greater<int>());
  if (!s)
    while (!o[++s])
      ;
  dfs(s);
  //递归版起点要单独输出一下  cout << s << ' ';
  while (cnt) cout << ans[--cnt] << ' ';
  return 0;
shame:
  cout << "What a shame!";
}

这题要求的是在一个有向,可能有环的正点权图上获取最大值。如果它是无环的,那么以终点的权作为边权求最长路即可(无环图不会到达一个点多次),因此,我们就考虑怎么处理环。这样就引入了缩点的概念(据说暴力也可以过?),为了获取最大权,进入环中一个点后必然会把整个环吃完,然后从中选择合适的点出环。根据这一性质,我们可以找到图中的环(不一定是单层的环,标准的名称为强联通分量),把每一个环缩成一个点,权值为各点权值之和,环内点与环外点的路径都加在缩成的点上。实现的方法有Kosaraju、Tarjan(没错,又是这位图灵奖巨佬)等。本题以Kosaraju为例,从起点开始进行一次dfs,回溯时按顺序记录点,再在反向图上,按之前的记录逆序一次dfs,在第i次反向dfs中初次找到的点(当然,正向dfs中不可达的点要排除)就构成了第i个强联通分量。然后建立新图,按一开始说的无环图做法即可,正权图最长路相当于负权图最短路,使用spfa算法求解。Kosaraju的复杂度通常为$O(n+m)$,时间瓶颈在于spfa,复杂度最坏可达$O(mn)$

代码及注释:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
#define RST(a) memset(a, 0, sizeof(a))
#define RSTV(a, v) memset(a, v, sizeof(a))
#define FOR(i, a, b) for (auto i = a; i < b; ++i)
struct E {
  int v, l;
};
vector<int> path[3001], repath[3001];
int m, n, l[3001], dfn[3001], pos = 0, vis[3001], revis[3001], f[3001], point[3001], s, p[3001], dis[3001];
vector<E> fpath[3001];
inline void dfs(int now) {//正向dfs
  vis[now] = 1;
  for (auto &p : path[now])
    if (!vis[p]) dfs(p);
  dfn[pos++] = now;
}
inline void redfs(int now, int pos) {//反向dfs
  if (!vis[now] || revis[now]) return;
  revis[now] = 1;
  f[now] = pos;
  point[pos] += l[now];
  for (auto &p : repath[now]) redfs(p, pos);
}
void spfa() {
  RST(dis);
  queue<int> q;
  dis[f[s]] = point[f[s]];
  q.push(f[s]);
  bool has[3001] = {0};
  has[f[s]] = 1;
  while (!q.empty()) {
    int now = q.front();
    q.pop();
    has[now] = 0;
    for (auto p : fpath[now])
      if (dis[p.v] < dis[now] + p.l) {
        dis[p.v] = dis[now] + p.l;
        if (!has[p.v]) {
          q.push(p.v);
          has[p.v] = 1;
        }
      }
  }
}
int main() {
  ios::sync_with_stdio(0);
  cin.tie(0);
  cin >> n >> m;
  int u[m], v[m];
  FOR(i, 0, m) {
    cin >> u[i] >> v[i];
    path[u[i]].push_back(v[i]);
    repath[v[i]].push_back(u[i]);//反向图
  }
  FOR(i, 1, n + 1) cin >> l[i];
  int np;
  cin >> s >> np;
  FOR(i, 0, np) cin >> p[i];
  RST(vis);
  dfs(s);
  int tmp = pos;
  pos = 0;
  RST(revis);
  for (int i = tmp - 1; i >= 0; --i) redfs(dfn[i], pos++);
  FOR(i, 0, m)
    if (f[u[i]] != f[v[i]])
      fpath[f[u[i]]].push_back({f[v[i]], point[f[v[i]]]});//建立缩点后的新图
  spfa();
  int M = 0;
  FOR(i, 0, np) M = max(M, dis[f[p[i]]]);
  cout << M;
}

这是本次的签到题,解题方法是根据贪心的思想,按权值从小到大取边,同时采用并查集判断边是否可取,这种算法被称为 kruskal算法,复杂度为$O(mlogm+mA(n)+n)$,瓶颈是边排序的$O(mlogm)$

代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <bits/stdc++.h>
using namespace std;
int fa[1001], depth[1001];
int find(int x) {
  if (fa[x] == x) return x;
  return fa[x] = find(fa[x]);
}
void connect(int u, int v) {
  u = find(u), v = find(v);
  if (depth[u] > depth[v])
    fa[v] = u;
  else {
    fa[u] = v;
    if (depth[v] == depth[u]) ++depth[v];
  }
}
struct E {
  int u, v, w;
  bool operator<(E x) const { return w < x.w; }
};
E edge[500000];
int main() {
  ios::sync_with_stdio(0);
  cin.tie(0);
  int n, m, k, pos = 0;
  cin >> n >> m >> k;
  for (int i = 1; i <= n; ++i) fa[i] = i;
  for (int i = 0; i < m; ++i) {
    int u, v, w;
    cin >> u >> v >> w;
    edge[i] = {u, v, w};
  }
  sort(edge, edge + m);
  int result = 0;
  for (auto p : edge)
    if (n == k)
      break;
    else if (find(p.u) == find(p.v))
      continue;
    else {
      connect(p.u, p.v);
      --n;
      result += p.w;
    }
  cout << result;
}

这题的思路没有什么好说的,很明显的网络流板题。下面简单说一下网络流(以后有时间可能会单独写一篇博客,也很可能会鸽)

网络流是一个有若干条带权(容量)边组成的,固定源点和汇点的有向图。本题要解决的是,从源点产生的流(本题中水流是个很形象的例子),经过各边汇集到汇点,这样的流的最大流量。显然,对于每个点,流入流出的量相等,总流量守恒;同时,每条边上的流量均不能超过该条边的容量。在思考怎么求出最大流的过程中,我们要了解一个非常重要的定理──增广路定理。当我们找到网络流中一条从源点到汇点的通路时,可以把这条通路的各段边的容量减少一个值 $a$,最好的情况下下,这样处理后的网络的最大流就减少了 $a$,这样的网络我们称之为残量(残留容量)网络,当残量网络中没有上述通路时,残量网络的最大流为0,原网络的最大流为$\sum a$,我们可以利用dfs找通路,$a$ 取路径上的最小残量,不断循环得出结果。

然而,这样做会存在一些问题

1.每次找到的路径可以是最好的路连接径吗?似乎不是。这种情况我们在数据结构专题的K题(点此进入)中遇到过,当时我们在删点的时候加入新的点来模拟反悔的操作。这里我们也可以加入反向边来模拟反悔的操作。 2.这样反复的dfs很耗时,可能会反复搜某一条边,尤其是各边容量的极差很大时。该算法称为Ford-Fulkerson算法,复杂度为$O(mC)$,通常只建议用来处理容量很小的图,如单位容量简单网络最大流的快速算法(其实我并不会)的其中部分步骤可以用它。可以采用bfs,即Edmonds-Karp算法来改进,复杂度为$O(nm^2)$。但是EK算法过这题似乎有点紧,有没有更快的做法?dinic、ISAP、等(其实两者原理很像,优化魔改之后界限并不是那么清晰)。

我们把所有的节点根据与源点的距离分层,找增广路时只取依层递进的路径,然后再重新分层,反复循环。复杂度最大为$O(mn^2)$(较松,还能加优化),具体实现如下(注意有注释的地方)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#include <bits/stdc++.h>
using namespace std;
#define RST(a) memset(a, 0, sizeof(a))
#define RSTV(a, v) memset(a, v, sizeof(a))
#define FOR(i, a, b) for (auto i = a; i < b; ++i)
struct edge {
  int e, c;
};
int n, m, cur[201], tail = 0, depth[201];
bool visited[201];
edge edges[20001];
vector<int> path[201];
inline void addedge(int s, int e, int c) {//建图时把反向边位置也分配好,并且分配到正向边位置^1,便于快速找反向边
  edges[tail] = {e, c};
  path[s].push_back(tail++);
  edges[tail] = {s, 0};
  path[e].push_back(tail++);
}
bool dinic_bfs() {//通过bfs,逐层外扩,实现分层
  RST(depth);
  RST(visited);
  visited[1] = 1;
  queue<int> q;
  q.push(1);
  while (!q.empty()) {
    int now = q.front();
    q.pop();
    for (auto p : path[now]) {
      if (!visited[edges[p].e] && edges[p].c) {
        depth[edges[p].e] = depth[now] + 1;
        q.push(edges[p].e);
        visited[edges[p].e] = 1;
      }
    }
  }
  return visited[n];//如果汇点已不可达,说明已无增广路,则结束循环,否则继续
}
int dinic_dfs(int now, int cap) {
  if (now == n || !cap) return cap;
  for (auto &i = cur[now]; i < path[now].size(); ++i) {//加入cur数组实现当前弧优化,搜到每条点时之前已被搜过的点不会搜出新的增广路路径,因此可以直接跳过。
    auto &p = path[now][i];
    if (depth[edges[p].e] == depth[now] + 1 && edges[p].c) {
      int tmp = dinic_dfs(edges[p].e, min(cap, edges[p].c));
      edges[p].c -= tmp;
      edges[p ^ 1].c += tmp;//反向边加上相同的值,后面可以走这条反向边来反悔之前选的边
      if (tmp) return tmp;//找到有值的增广路,返回
    }
  }
  depth[now] = -1;
  return 0;
}
int main() {
  ios::sync_with_stdio(0);
  cin.tie(0);
  cin >> n >> m;
  while (m--) {
    int s, e, c;
    cin >> s >> e >> c;
    addedge(s, e, c);
  }//首先建图
  int result = 0;
  while (dinic_bfs()) {//然后开始跑分层
    RST(cur);
    int tmp;
    while ((tmp = dinic_dfs(1, INT_MAX >> 2))) result += tmp;//再在图中跑符合要求的增广路,找不到了就重新分层
  }
  cout << result;
}

这是一道很裸的有向图(无向图拆边,然后做法也一样)K短路问题,我们可以采用一个方向最短路算法初始化(dijkstra,spfa什么的都可以,时间很松),一个方向A*启发式搜索(这类题是$h(x)=h*(x)$的特例)的方法解题。最短路算法不再多说,对于A*算法,我们用一个优先队列,从起点开始,每次扩展其直接可达的边入队,这样的过程中,跑出i次第i短路时,它可以扩展出i+1次第i+1短路,由此,当某条边出队K次时可以终止,此时的路径长度就是第k短路的长度。复杂度(貌似)$O(kmlogm)$

代码及注释

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
struct edge {
  int v;
  ll w;
};
struct status {
  int p;
  ll l;
  bool operator<(status b) const { return l > b.l; }
};
struct astar {
  int v;
  ll g, f;
  bool operator<(astar b) const { return f == b.f ? g > b.g : f > b.f; }
};
int main() {
  ios::sync_with_stdio(0);
  cin.tie(0);
  int n, m, k, s, t;
  cin >> n >> m >> k >> s >> t;
  vector<edge> path[n + 1], repath[n + 1];
  ll dis[n + 1];
  int visited[n + 1];
  for (int i = 0; i < m; ++i) {//建立正向图和反向图
    int u, v, w;
    cin >> u >> v >> w;
    path[u].push_back({v, w});
    repath[v].push_back({u, w});
  }
  memset(dis, 0x3f, sizeof(dis));
  memset(visited, 0, sizeof(visited));
  priority_queue<status> q;
  dis[t] = 0;
  q.push({t, 0});
  while (!q.empty()) {//反向dijkstra算法求最短路,预处理出h(x)
    status now = q.top();
    q.pop();
    if (visited[now.p]) continue;
    visited[now.p] = 1;
    for (auto &p : repath[now.p]) {
      if (dis[p.v] > dis[now.p] + p.w) {
        dis[p.v] = dis[now.p] + p.w;
        q.push({p.v, dis[p.v]});
      }
    }
  }
  memset(visited, 0, sizeof(visited));
  priority_queue<astar> paths;//然后正向通过A*算法得出结果
  paths.push({s, 0, dis[s]});
  while (!paths.empty()) {
    astar now = paths.top();
    paths.pop();
    if (++visited[now.v] == k && now.v == t) {
      cout << now.f;
      return 0;
    };
    if (visited[now.v] > k) continue;
    for (auto p : path[now.v])
      paths.push({p.v, p.w + now.g, p.w + now.g + dis[p.v]});
  }
  cout << -1;//跑完之后仍找不到,则说明没有
}

这道题目给了我们一个二分图,然后希望求一个最小的点的数量,来覆盖这一个二分图的所有边。我们求出最大匹配,在每个匹配的其中一个点上放置一个点即可覆盖,因此该题实际上是求最大匹配数,我们把二分图各边的容量设为1,然后加入源点s并依次连至左边的元素,右边的元素依次连至汇点t,该问题便转化为了一个网络流问题,而且是单位容量的简单网络流。对于这种特殊情况的网络流,可使用Hopcroft算法达到$O(m\sqrt n)$的复杂度,使用常规的dinic算法也可以平稳通过。以下代码为dinic算法(具体做法见H题)

代码及注释:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#include <bits/stdc++.h>
using namespace std;
#define RST(a) memset(a, 0, sizeof(a))
#define RSTV(a, v) memset(a, v, sizeof(a))
#define FOR(i, a, b) for (auto i = a; i < b; ++i)
struct edge {
  int e, c;
};
int a, b, m, cur[200002], tail = 0, depth[200002], End;
bool visited[200002];
edge edges[500000];
vector<int> paths[200002];
inline void addedge(int s, int e, int c) {
  edges[tail] = {e, c};
  paths[s].push_back(tail++);
  edges[tail] = {s, 0};
  paths[e].push_back(tail++);
}
bool dinic_bfs() {
  RST(depth);
  RST(visited);
  *visited = 1;
  queue<int> q;
  q.push(0);
  while (!q.empty()) {
    int now = q.front();
    q.pop();
    for (auto p : paths[now]) {
      if (!visited[edges[p].e] && edges[p].c) {
        depth[edges[p].e] = depth[now] + 1;
        q.push(edges[p].e);
        visited[edges[p].e] = 1;
      }
    }
  }
  return visited[End];
}
int dinic_dfs(int now, int cap) {
  if (now == End || !cap) return cap;
  for (auto &i = cur[now]; i < paths[now].size(); ++i) {
    auto &p = paths[now][i];
    if (depth[edges[p].e] == depth[now] + 1 && edges[p].c) {
      int tmp = dinic_dfs(edges[p].e, min(cap, edges[p].c));
      edges[p].c -= tmp;
      edges[p ^ 1].c += tmp;
      if (tmp) return tmp;
    }
  }
  depth[now] = -1;
  return 0;
}
int main() {
  ios::sync_with_stdio(0);
  cin.tie(0);
  cin >> a >> b >> m;
  End = a + b + 1;//0为源点,1到a+b为二分图中点,a+b+1为汇点
  while (m--) {
    int s, e;
    cin >> s >> e;
    addedge(s, e + a, 1);
  }
  FOR(i, 1, a + 1) addedge(0, i, 1);
  FOR(i, 1, b + 1) addedge(i + a, End, 1);//建好图,后面就搬H题过来了
  int result = 0;
  while (dinic_bfs()) {
    RST(cur);
    int tmp;
    while ((tmp = dinic_dfs(0, INT_MAX >> 2))) result += tmp;
  }
  cout << result;
}

这题求的是免费K条边的最短路。我们的一个容易想到的直观的思路是做分层图,把已经免费的次数作为一个维度建图,但是如果完整的建完整张图,会导致内存超出,而分层图中存在大量的重复边,可以都从第一层取边,然后再加上跨层的边。但我觉得这题更好的思路是,最短路算法本质上是一个一维状态的动态规划(dijkstra偏向于贪心,是bfs的扩展,但其本质上仍然是动态规划的一种),而这题我们需要的是一个二维的动态规划。我们可以按照最短路算法思路,在其基础上使用位置+已免费边数的状态替换位置状态,在状态转移的过程中,由最短路算法的$dis[q]=min{dis[p]+w(p\to q)}$,拓展为$dis[q][h]=min({dis[p][h]+w(p\to q)},{dis[p][h-1]})$,p为所有存在边$p\to q$的点。然后该方程的性质仍然满足dijkstra的要求(转移过程中$dis[q][h]$不小于$dis[p][h]$和$dis[p][h-1]$),因此可以按照dijkstra的思路,用修改版的dijkstra求解。复杂度$O(kmlogkn)$。spfa的话,直接交貌似会TLE,SLF优化貌似对这种情况效果比较好。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
#define RST(a) memset(a, 0, sizeof(a))
#define RSTV(a, v) memset(a, v, sizeof(a))
#define FOR(i, a, b) for (auto i = a; i < b; ++i)
int s, t, k;
ll dis[10005][25];
struct status {//原版dijstra是位置+当前位置的最短路,这里增加来b表示免费的次数
  int a = 0, b = 0;
  ll dis;
  bool operator<(status x) const { return dis > x.dis; }//注意这里dis要静态保存,去掉dis然后动态计算(return dis[a][b] > dis[x.a][x.b])会导致顺序出错,WA on 10
};
struct edge {
  int v = 0, w = 0;
};
vector<edge> path[10005];
ll dijkstra() {
  RSTV(dis, 0x7f);
  priority_queue<status> q;
  for (auto &p : dis[s]) p = 0;
  q.push({s, 0});
  bool visited[10005][25];
  while (!q.empty()) {
    status now = q.top();
    q.pop();
    if (visited[now.a][now.b]) continue;
    visited[now.a][now.b] = 1;
    for (auto &p : path[now.a]) {
      if (dis[p.v][now.b] > dis[now.a][now.b] + p.w) {
        dis[p.v][now.b] = dis[now.a][now.b] + p.w;
        q.push({p.v, now.b, dis[p.v][now.b]});
      }//第一种转移,同正常的dijkstra
      if (now.b < k && dis[p.v][now.b + 1] > dis[now.a][now.b]) {
        dis[p.v][now.b + 1] = dis[now.a][now.b];
        q.push({p.v, now.b + 1, dis[p.v][now.b + 1]});
      }//第二种转移,跨层,dis值不增加
    }
  }
  return dis[t][k];
}
signed main() {//建图,跑最短路,输出
  ios::sync_with_stdio(0);
  cin.tie(0);
  int n, m;
  cin >> n >> m >> k >> s >> t;
  while (m--) {
    int u, v, w;
    cin >> u >> v >> w;
    path[u].push_back({v, w});
    path[v].push_back({u, w});
  }
  cout << dijkstra();
}

这题有些复杂,购买物品需要花费,购买某个集合的所有物品可获得相应的奖励,答案要求是两者差最大,而不是限制最大花费。这样看起来比动态规划中01背包问题复杂得多。这种问题称为最大权闭合子图问题,我们从源点连接所有的物品,所有集合连向汇点,容量均取正。然后根据包含关系,用无限容量的边连接物品和集合。这样一来,就可以用割去某个物品到源点的边模拟购买此物品,某个集合被买完之后其到终点的流被切断。这样一来我们求一个割,即使得所有的边都被割断,所花费的最小代价,这样对于每个集合,要么获得并且花费了相应的代价,要么放弃了该集合。所有集合的总的压岁钱数量减去此时的割,就是他可以剩下的压岁钱。我们要求最大的剩余压岁钱,就要求最小割。显然,我们按照最大流的路径去割是最佳方案,这样最小割就等于最大流(最大流做法见H题)。复杂度$O((n+m)^2\sum k)$

代码及注释

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
#define RST(a) memset(a, 0, sizeof(a))
#define RSTV(a, v) memset(a, v, sizeof(a))
#define FOR(i, a, b) for (auto i = a; i < b; ++i)
struct E {
  int e, c;
};
int n, m, t, depth[20002], tail = 0, cur[20002];
E edges[250000];
bool visited[20002];
vector<int> paths[20002];
inline void addedge(int s, int e, int c) {
  edges[tail] = {e, c};
  paths[s].push_back(tail++);
  edges[tail] = {s, 0};
  paths[e].push_back(tail++);
}
bool dinic_bfs() {
  RST(depth);
  RST(visited);
  visited[0] = 1;
  queue<int> q;
  q.push(0);
  while (!q.empty()) {
    int now = q.front();
    q.pop();
    for (auto p : paths[now]) {
      if (!visited[edges[p].e] && edges[p].c) {
        depth[edges[p].e] = depth[now] + 1;
        q.push(edges[p].e);
        visited[edges[p].e] = 1;
      }
    }
  }
  return visited[t];
}
int dinic_dfs(int now, int cap) {
  if (now == t || !cap) return cap;
  for (auto &i = cur[now]; i < paths[now].size(); ++i) {
    auto &p = paths[now][i];
    if (depth[edges[p].e] == depth[now] + 1 && edges[p].c) {
      int tmp = dinic_dfs(edges[p].e, min(cap, edges[p].c));
      edges[p].c -= tmp;
      edges[p ^ 1].c += tmp;
      if (tmp) return tmp;
    }
  }
  depth[now] = -1;
  return 0;
}
int main() {
  ios::sync_with_stdio(0);
  cin.tie(0);
  cin >> n >> m;
  FOR(i, 1, m + 1) {
    int tmp;
    cin >> tmp;
    addedge(0, i, tmp);
  }
  t = m + n + 1;
  int sum = 0;
  FOR(i, m + 1, t) {
    int c, k;
    cin >> c >> k;
    sum += c;//计算所有集合的压岁钱数量总和
    addedge(i, t, c);
    while (k--) {
      int tmp;
      cin >> tmp;
      addedge(tmp, i, INT_MAX >> 1);
    }
  }//建图完,0为源点,1到m为物品,m+1到m+n为集合,m+n+1为汇点,然后跑最大流
  int result = 0;
  while (dinic_bfs()) {
    RST(cur);
    int tmp;
    while ((tmp = dinic_dfs(0, INT_MAX >> 1))) result += tmp;
  }
  cout << sum - result;//输出结果
}

这题是一道差分约束,某两个量存在一个大于关系,可以用一条权值为1(它们至少相差1)的有向边连接(小于则相反),等于就是权值为0的双向边,这样,然后从源点向每条边添加权值为最小值1的边,这样源点到每个点的最长路长度就是它的最小值,题目要求输出的就是这些值的和。如果有环就说明存在矛盾。带判环的spfa可以用来解决这个问题。复杂度$O(nm)$

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#include <bits/stdc++.h>
using namespace std;
#define RST(a) memset(a, 0, sizeof(a))
#define RSTV(a, v) memset(a, v, sizeof(a))
#define FOR(i, a, b) for (auto i = a; i < b; ++i)
struct edge {
  int v, l;
};
vector<edge> path[10001];
int dis[1001];
int spfa() {
  RSTV(dis, 0x80);
  queue<int> q;
  *dis = 0;
  int cnt[1001] = {0};
  q.push(0);
  bool has[1001] = {1};
  ++*cnt;
  while (!q.empty()) {
    int now = q.front();
    q.pop();
    has[now] = 0;
    for (auto p : path[now])
      if (dis[p.v] < dis[now] + p.l) {
        dis[p.v] = dis[now] + p.l;
        if (!has[p.v]) {
          q.push(p.v);
          has[p.v] = 1;
          ++cnt[p.v];
          if (cnt[p.v] > 1000) return 0;
        }
      }
  }
  return 1;
}
int main() {
  int n, m;
  ios::sync_with_stdio(0);
  cin.tie(0);
  cin >> n >> m;
  while (m--) {
    int a, b, o;
    cin >> o >> a >> b;
    switch (o) {
      case 1:
        path[b].push_back({a, 1});
        break;
      case 2:
        path[a].push_back({b, 1});
        break;
      case 3:
        path[a].push_back({b, 0});
        path[b].push_back({a, 0});
    }
  }//建图,过程如前面的描述
  FOR(i, 1, n + 1) path->push_back({i, 1});
  int result = 0;
  if (spfa()) {
    FOR(i, 1, n + 1) result += dis[i];
    cout << result;
  } else//如果spfa跑失败了就输出-1
    cout << -1;
}

这题,最少花费,可以让人联想到图上面的最短路,而问题的关键就是怎么建图,即找到满足要求的点(状态)和转移路径。状态的描述方式为二进制(参考状压dp。然后跑dijkstra即可。(点和边的判断方式见注释),复杂度貌似为$O(2^n+2^{2(n-\overline m)}+2^{n-\overline m}C_{n-\overline m}^{k-\overline m})$,$\overline m$是$m$减去$m$对cp关系中环的数量,似乎$n$很大(15),$m$很小(1)的数据可以卡掉(如有错误请指正)

代码及注释

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using pii = pair<int, int>;
#define RST(a) memset(a, 0, sizeof(a))
#define RSTV(a, v) memset(a, v, sizeof(a))
#define FOR(i, a, b) for (auto i = a; i < b; ++i)
struct E {
  int v, l;
};
int pos = 0, n, m, k, point[2 << 16];//末位表示洁姐姐,然后从右到左依次表示其他人
vector<E> path[2 << 16];
pii against[15];
bool judge_p(int x) {//判断点(状态)
  if ((x & 1)) return 1;//如果洁姐姐在当然没问题
  FOR(i, 0, m)
  if (x >> against[i].first & 1 && x >> against[i].second & 1) return 0;//如果不在就要判断有没有cp,没有才过关
  return 1;
}
int judge_e(int x, int y) {//判断边(转移)
  int det = x ^ y, cnt = 0;
  if (max(x, y) - min(x, y) != det) return 0;//去掉既有有人上,又有人下的情况
  if (!(det & 1)) return 0;//去掉洁姐姐没有参加的情况
  FOR(i, 0, 16) cnt += det >> i & 1;
  return cnt > k ? 0 : cnt;//参加的人不能超过k,满足条件的输出返回人数,作为增加的边的边权。
}
void addedge(int u, int v, int l) {
  path[u].push_back({v, l});
  path[v].push_back({u, l});
}
int dijkstra() {
  priority_queue<pii> q;
  bool visited[2 << 16] = {0};
  int dis[2 << 16];
  RSTV(dis, 0x3f);
  *dis = 0;
  q.push({0, 0});
  while (!q.empty()) {
    pii now = q.top();
    q.pop();
    if (visited[now.first]) continue;
    visited[now.first] = 1;
    for (auto &p : path[now.first]) {
      if (dis[p.v] > dis[now.first] + p.l) {
        dis[p.v] = dis[now.first] + p.l;
        q.push({p.v, dis[p.v]});
      }
    }
  }
  return dis[(1 << (n + 1)) - 1] == 0x3f3f3f3f ? 0 : dis[(1 << (n + 1)) - 1];
}
int main() {
  ios::sync_with_stdio(0);
  cin.tie(0);
  cin >> n >> m >> k;
  FOR(i, 0, m) {//记录cp关系
    int u, v;
    cin >> u >> v;
    against[pos++] = make_pair(u, v);
  }
  pos = 0;
  int Max = (1 << (n + 1)) - 1;
  FOR(i, 0, 1 << (n + 1))
  if (judge_p(i) && judge_p(Max - i)) point[pos++] = i;//必须两边都ok,才可以
  int tmp;
  FOR(i, 0, pos)
    FOR(j, i, pos)
      if ((tmp = judge_e(point[i], point[j]))) addedge(point[i], point[j], tmp);//遍历点的所有组合,判断并添加边
  if ((tmp = dijkstra()))//跑最短路,到不了就输出"mole"
    cout << tmp;
  else
    cout << "mole";
}

这题是在三维空间上面取一棵生成树,求这棵树的$\frac{\sum h}{\sum d}$的最小值。我们假设这个值为ans,则${\sum h}\geq ans\sum d$,记最小生成树上的边集合为$A$,则$\sum (e.h-ans \cdot e.d)=0,e\in A$。当ans过大时,$\sum (e.h-ans \cdot e.d) < 0,e\in A$,过小则反之。这样一来,我们就可以用二分套最小生成树来求解。最小生成树部分用prim跑得飞快,用kruskal要慢一些,注意常数别太差还是能过。复杂度$O(Alognz)$,$A$为最小生成树的复杂度

代码及注释:(kruskal)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
#define RST(a) memset(a, 0, sizeof(a))
#define RSTV(a, v) memset(a, v, sizeof(a))
#define FOR(i, a, b) for (auto i = a; i < b; ++i)
int fa[1001], depth[1001];
int find(int x) {
  if (fa[x] == x) return x;
  return fa[x] = find(fa[x]);
}
void connect(int u, int v) {
  u = find(u), v = find(v);
  if (depth[u] > depth[v])
    fa[v] = u;
  else {
    fa[u] = v;
    if (depth[v] == depth[u]) ++depth[v];
  }
}
struct E {
  int u, v;
  double w;
  inline bool operator<(E x) const { return w < x.w; }
};
struct P {
  int x, y, z;
};
P points[1001];
E edge[500001];
int pos = 0;
int main() {
  ios::sync_with_stdio(0);
  cin.tie(0);
  int n;
  cin >> n;
  FOR(i, 0, n) cin >> points[i].x >> points[i].y >> points[i].z;
  double sum, ans = 10000000;
  bool a = 0;
  double tmp[2] = {0, 10000000};//中间开始二分
  do {
    ans = (tmp[0] + tmp[1]) / 2;
    pos = sum = 0;
    FOR(i, 0, n)
      FOR(j, i + 1, n)
        edge[pos++] = {i, j,
                   abs(points[i].z - points[j].z) -
                       ans * sqrt(((ll)points[i].x - points[j].x) *
                                      (points[i].x - points[j].x) +
                                  ((ll)points[i].y - points[j].y) *
                                      (points[i].y - points[j].y))};
    for (int i = 0; i < n; ++i) fa[i] = i;//每次要重新加边
    RST(depth);
    int i = 0;
    sort(edge, edge + pos);
    for (auto &p : edge)
      if (i == n - 1)
        break;
      else if (find(p.u) == find(p.v))
        continue;
      else {
        ++i;
        sum += p.w;
        connect(p.u, p.v);
      }
    tmp[sum < 0] = ans;
  } while (abs(tmp[0] - tmp[1]) >= 0.0001);//精度1e-4
  cout << fixed << setprecision(3) << ans;
}