作物杂交
作物杂交
作物杂交是作物栽培中重要的一步。
已知有 $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/
本文来自博客园,作者:onlyblues,转载请注明原文链接:https://www.cnblogs.com/onlyblues/p/17191565.html