作物杂交

作物杂交

作物杂交是作物栽培中重要的一步。

已知有 $N$ 种作物 (编号 $1$ 至 $N$),第 $i$ 种作物从播种到成熟的时间为 $T_i$。

作物之间两两可以进行杂交,杂交时间取两种中时间较长的一方。

如作物 $A$ 种植时间为 $5$ 天,作物 $B$ 种植时间为 $7$ 天,则 $AB$ 杂交花费的时间为 $7$ 天。

作物杂交会产生固定的作物,新产生的作物仍然属于 $N$ 种作物中的一种。

初始时,拥有其中 $M$ 种作物的种子 (数量无限,可以支持多次杂交)。

同时可以进行多个杂交过程。

求问对于给定的目标种子,最少需要多少天能够得到。

如存在 $4$ 种作物 $ABCD$,各自的成熟时间为 $5$ 天、$7$ 天、$3$ 天、$8$ 天。

初始拥有 $AB$ 两种作物的种子,目标种子为 $D$,已知杂交情况为 $A \times B \to C$,$A \times C \to D$。

则最短的杂交过程为:

第 $1$ 天到第 $7$ 天 (作物 $B$ 的时间),$A \times B \to C$。

第 $8$ 天到第 $12$ 天 (作物 $A$ 的时间),$A \times C \to D$。

花费 $12$ 天得到作物 $D$ 的种子。

输入格式

输入的第 $1$ 行包含 $4$ 个整数 $N,M,K,T$,$N$ 表示作物种类总数 (编号 $1$ 至 $N$),$M$ 表示初始拥有的作物种子类型数量,$K$ 表示可以杂交的方案数,$T$ 表示目标种子的编号。

第 $2$ 行包含 $N$ 个整数,其中第 i 个整数表示第 $i$ 种作物的种植时间 $T_i$。

第 $3$ 行包含 $M$ 个整数,分别表示已拥有的种子类型 $K_j$,$K_j$ 两两不同。

第 $4$ 至 $K+3$ 行,每行包含 $3$ 个整数 $A,B,C$,表示第 $A$ 类作物和第 $B$ 类作物杂交可以获得第 $C$ 类作物的种子。

输出格式

输出一个整数,表示得到目标种子的最短杂交时间。

样例解释

$1 \leq N \leq 2000$,
$2 \leq M \leq N$,
$1 \leq K \leq {10}^5$,
$1 \leq T \leq N$,
$1 \leq T_i \leq 100$,
$1 \leq K_j \leq M$,

保证目标种子一定可以通过杂交得到。
不保证作物 $A$ 和 $B$ 杂交只能生成作物 $C$(也就是说,$A \times B \to C$ 和 $A \times B \to D$ 可能同时在输入中出现)
不保证作物 $C$ 只能由作物 $A$ 和 $B$ 杂交生成(也就是说,$A \times B \to D$ 和 $A \times C \to D$ 可能同时在输入中出现)。
不保证同一杂交公式不在输入中重复出现。

输入样例:

6 2 4 6
5 3 4 6 4 9
1 2
1 2 3
1 3 4
2 3 5
4 5 6

输出样例:

16

样例解释

第 $1$ 天至第 $5$ 天,将编号 $1$ 与编号 $2$ 的作物杂交,得到编号 $3$ 的作物种子。

第 $6$ 天至第 $10$ 天,将编号 $1$ 与编号 $3$ 的作物杂交,得到编号 $4$ 的作物种子。

第 $6$ 天至第 $9$ 天,将编号 $2$ 与编号 $3$ 的作物杂交,得到编号 $5$ 的作物种子。

第 $11$ 天至第 $16$ 天,将编号 $4$ 与编号 $5$ 的作物杂交,得到编号 $6$ 的作物种子。

总共花费 $16$ 天。

 

解题思路

  好题,通过这道题可以知道 Bellman-Ford 算法是如何通过动态规划得到的。同时学习到与平时不同的建图方式。

  首先 Bellman-Ford 算法本质就是个dp,定义状态$f(i,j)$表示从源点出发所有在$i$步以内(经过不超过$i+1$个点,即经过不超过$i$条边)可以到达点$j$的路径的集合,属性就是路径长度的最小值。根据节点$j$的所有出边(可以到达的点)来进行状态转移。状态转移方程如下图:

  最后从源点经过不超过$n-1$步到点$x$的最短路径就是$f(n-1,x)$。为什么是在$n-1$步内?这是因为如果从源点经过超过$n-1$步到达点$x$,意味着在这条路径中存在至少$n+1$个点,又因为最多只有$n$个不同的点,因此根据抽屉原理路径中必然至少存在两个相同的点,这意味着这路径中存在环。如果这个环是负环,那么可以一直走这个环,从源点到点$x$的最短路径就是负无穷了,即不存在最短路。否则如果是正环,那么我们可以把这个环去掉,路径长度会变小,就矛盾了。

  Bellman-Ford 算法可以把第一维去掉,就变成我们经常用的$\text{dist}[j]$了,表示从源点出发到点$j$的最短路径。Bellman-Ford 算法的时间复杂度为$O(n \cdot m)$,其中$n$是点的数量,$m$是边的数量。

  在本题中同样可以借用 Bellman-Ford 算法的思路来定义状态。定义状态$f(i,j)$表示所有在$i$步以内可以生成作物$j$的方法的集合,属性就是所需时间的最小值。状态转移方程和上面的很类似,假设有$x \times y \to j$,那么状态转移方程就是$$f(i,j) = \max \{ f(i-1,x), \ f(i-1,y) \} + \max \{ w_x, \ w_y \}$$

  这个是从其他状态转移到$f(i,j)$,那么如何从$f(i,j)$转移到其他状态呢?这就涉及到如何建图了。

  建图的方式很巧妙,每条边除了存权值外还可以存其他的东西。假设有关系$A \times B \to C$,那么我们建两条边,一条是$A \to C$,边的信息还要存一个$B$,表示$A$和$B$生成$C$。另外一条是$B \to C$,边的信息还要存一个$A$,表示$B$和$A$生成$C$。可以理解为每次枚举到$j = A$,我们需要把所有与$A$配对的点取出来更新可以生成的作物。这样状态转移方程就可以写成$$f(i+1, u_k) = \min \left\{ f(i+1,u_k), \ \max \{ f(i,j), \ f(i,p_k) \} + \max \{ w_j, \ w_{p_k} \} \right\}$$

  其中有$j \times p_k \to u_k$。

  在这题如果直接暴力循环$n-1$的话时间复杂度为$O(n \times m)$,会超时。可以加个优化,如果对于某个$i$发现所有的状态都没用被更新过那么就可以退出了。同时还要把第一维去掉,否则还是会TLE。

  AC代码如下:

 1 #include <bits/stdc++.h>
 2 using namespace std;
 3 
 4 const int N = 2010, M = 2e5 + 10;
 5 
 6 int w[N];
 7 int head[N], e[M], p[M], ne[M], idx;
 8 int f[N];
 9 
10 void add(int v, int w, int x) {
11     e[idx] = w, p[idx] = x, ne[idx] = head[v], head[v] = idx++;
12 }
13 
14 int main() {
15     int n, m, k, t;
16     scanf("%d %d %d %d", &n, &m, &k, &t);
17     for (int i = 1; i <= n; i++) {
18         scanf("%d", w + i);
19     }
20     memset(f, 0x3f, sizeof(f));
21     while (m--) {
22         int x;
23         scanf("%d", &x);
24         f[x] = 0;
25     }
26     memset(head, -1, sizeof(head));
27     while (k--) {
28         int v, w, x;
29         scanf("%d %d %d", &v, &w, &x);
30         add(v, x, w), add(w, x, v);
31     }
32     for (int i = 0; i < n - 1; i++) {
33         bool flag = false;
34         for (int j = 1; j <= n; j++) {
35             for (int k = head[j]; k != -1; k = ne[k]) {
36                 int t = max(f[j], f[p[k]]) + max(w[j], w[p[k]]);
37                 if (f[e[k]] > t) {
38                     f[e[k]] = t;
39                     flag = true;
40                 }
41             }
42         }
43         if (!flag) break;   // 没有状态被更新过
44     }
45     printf("%d", f[t]);
46     
47     return 0;
48 }

  还可以写 SPFA 来优化 Bellman-Ford 算法,AC代码如下:

 1 #include <bits/stdc++.h>
 2 using namespace std;
 3 
 4 const int N = 2010, M = 2e5 + 10;
 5 
 6 int w[N];
 7 int head[N], e[M], p[M], ne[M], idx;
 8 int dist[N];
 9 bool vis[N];
10 
11 void add(int v, int w, int x) {
12     e[idx] = w, p[idx] = x, ne[idx] = head[v], head[v] = idx++;
13 }
14 
15 int main() {
16     int n, m, k, t;
17     scanf("%d %d %d %d", &n, &m, &k, &t);
18     for (int i = 1; i <= n; i++) {
19         scanf("%d", w + i);
20     }
21     queue<int> q;
22     memset(dist, 0x3f, sizeof(dist));
23     while (m--) {
24         int x;
25         scanf("%d", &x);
26         q.push(x);
27         dist[x] = 0;
28         vis[x] = true;
29     }
30     memset(head, -1, sizeof(head));
31     while (k--) {
32         int v, w, x;
33         scanf("%d %d %d", &v, &w, &x);
34         add(v, x, w), add(w, x, v);
35     }
36     while (!q.empty()) {
37         int t = q.front();
38         q.pop();
39         vis[t] = false;
40         for (int i = head[t]; i != -1; i = ne[i]) {
41             int d = max(dist[t], dist[p[i]]) + max(w[t], w[p[i]]);
42             if (dist[e[i]] > d) {
43                 dist[e[i]] = d;
44                 if (!vis[e[i]]) vis[e[i]] = true, q.push(e[i]);
45             }
46         }
47     }
48     printf("%d", dist[t]);
49     
50     return 0;
51 }

  这题还可以用 Dijkstra 来写。这是因为求最短路,并且所有的边权均是非负数。思想一样是从动态规划出发,然后发现是个有环图,所以跑个 Dijkstra 就可以了。

  建图的方式与上面的一样,定义$\text{dist}[i]$表示从源点到点$i$的最短距离,发现状态转移是存在环的,因此问题就变成了在有环图求最短路的问题,由于边权非负所以可以用跑 Dijkstra。

  AC代码如下:

 1 #include <bits/stdc++.h>
 2 using namespace std;
 3 
 4 typedef pair<int, int> PII;
 5 
 6 const int N = 2010, M = 2e5 + 10;
 7 
 8 int w[N];
 9 int head[N], e[M], p[M], ne[M], idx;
10 int dist[N];
11 bool vis[N];
12 
13 void add(int v, int w, int x) {
14     e[idx] = w, p[idx] = x, ne[idx] = head[v], head[v] = idx++;
15 }
16 
17 int main() {
18     int n, m, k, t;
19     scanf("%d %d %d %d", &n, &m, &k, &t);
20     for (int i = 1; i <= n; i++) {
21         scanf("%d", w + i);
22     }
23     priority_queue<PII, vector<PII>, greater<PII>> pq;
24     memset(dist, 0x3f, sizeof(dist));
25     while (m--) {
26         int x;
27         scanf("%d", &x);
28         pq.push({0, x});
29         dist[x] = 0;
30     }
31     memset(head, -1, sizeof(head));
32     while (k--) {
33         int v, w, x;
34         scanf("%d %d %d", &v, &w, &x);
35         add(v, x, w), add(w, x, v);
36     }
37     while (!pq.empty()) {
38         int t = pq.top().second;
39         pq.pop();
40         if (vis[t]) continue;
41         vis[t] = true;
42         for (int i = head[t]; i != -1; i = ne[i]) {
43             int d = max(dist[t], dist[p[i]]) + max(w[t], w[p[i]]);
44             if (dist[e[i]] > d) {
45                 dist[e[i]] = d;
46                 pq.push({dist[e[i]], e[i]});
47             }
48         }
49     }
50     printf("%d", dist[t]);
51     
52     return 0;
53 }

 

参考资料

  AcWing 3305. 作物杂交(蓝桥杯集训·每日一题):https://www.acwing.com/video/4647/

posted @ 2023-03-08 12:02  onlyblues  阅读(156)  评论(0编辑  收藏  举报
Web Analytics