1. Floyd
无/有向图,可带负边权。
板子:
public class Floyd {
public Integer[][] shortestPath(Integer[][] g){
int n = g.length;
Integer[][] ans = g.clone();
Integer[][] dp;
for(int k=0;k<n;k++){
dp=ans.clone();
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if(i==j){
continue;
}
if(dp[i][j]==null){
if(dp[i][j]==null || dp[i][k]==null || dp[k][j]==null){
ans[i][j] = null;
}
else{
ans[i][j] = dp[i][k]+dp[k][j];
}
continue;
}
ans[i][j] = Math.min(dp[i][j],dp[i][k]+dp[k][j]); // with direction
}
}
}
return ans;
}
}
java比较笨,没inf这种标识符。我就用包装类作为边权了,含null。因此特判有点多。但总体而言,状态转移方程都是一样的:
dp(k)(i,j) = min( dp(k-1)(i,k) + dp(k-1)(k,j), dp(k-1)(i,j)
dp(k)(i,j)表示从i到j的最短路,这条路径上除了i,j这两个节点,序号最大为k。
1.1. 周赛377 T3 LC 100156 转换字符串的最小成本Ⅰ
一个字符可以转为另一个字符,代表中间有一条边。很明显就转换为了最短路径和的问题。是一道Floyd的板子题。注意这个题是有向图,x能转换为y不代表y也能转换为x。
另外,小写字母限定了最多只有26个节点,也就是这题是个26^3的O(1)时间复杂度。另外边权比较小只有1e6,这样即便最长链也只有26*1e6,没爆int,dp放心用int,但是几条路径的和可能溢出,因此结果是long。
最后,题目提醒了,这是个多重边的图,因为最短路,很明显把重复的较大权重的边去掉就可以了。
import java.util.Arrays;
class Solution {
static int invalid = -1;
public long minimumCost(String source, String target, char[] original, char[] changed, int[] cost) {
int[][] edge = new int[26][26];
for (int i = 0; i < edge.length; i++) {
Arrays.fill(edge[i],invalid);
edge[i][i]=0;
}
int o1,o2;
for (int i = 0; i < original.length; i++) {
o1 = original[i]-'a';
o2 = changed[i]-'a';
if(o1==o2){
continue;
}
if(edge[o1][o2]!=invalid){
edge[o1][o2] = Math.min(edge[o1][o2],cost[i]);
}else{
edge[o1][o2] = cost[i];
}
}
int[][] dp = edge.clone();
int[][] temp;
for(int k=0;k<26;k++){
for(int i=0;i<26;i++){
for(int j=0;j<26;j++){
edge[i][j] = dp[i][j];
if(dp[i][k]!=invalid && dp[k][j]!=invalid){
if(edge[i][j]==invalid){
edge[i][j] = dp[i][k]+dp[k][j];
}else{
edge[i][j] = Math.min(edge[i][j],dp[i][k]+dp[k][j]);
}
}
}
}
temp = dp;
dp = edge;
edge = temp;
}
long ans = 0L;
for (int i = 0; i < source.length(); i++) {
int c = dp[source.charAt(i) - 'a'][target.charAt(i) - 'a'];
if(c == invalid){
return -1;
}
ans += c;
}
return ans;
}
}
这题不带负边权,我就把板子简化了,直接把-1当作不存在边。Floyd是可以把dp和edge矩阵交替用的,省空间。
1.2. LC 1976 到达目的地的方案数
这题我这么想的。首先可以先把0到n-1的最短路求出来。我们知道最短路的子路径也是最短路。所以我们可以建邻接表深搜,查看当前节点是否在一条最短路上:假设当前节点为node,子节点为child,已经走过的距离为dis,最短路矩阵为edges,那么相当于要判断dis+dis(node,child)是否等于edges[0][child]。如果是则可以深搜这个子节点。累积路径数量。
模运算还是那个样子,把能取模的地方全取模。
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
class Solution {
int mod = (int)1e9+7;
List<int[]>[] g;
int[] memo;
public int countPaths(int n, int[][] roads) {
g = new ArrayList[n];
Arrays.setAll(g,e->new ArrayList<>());
for (int[] road : roads) {
g[road[0]].add(new int[]{road[1],road[2]});
g[road[1]].add(new int[]{road[0],road[2]});
}
memo = new int[n];
Arrays.fill(memo,-1);
Long[][] edges = new Long[n][n];
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if(i==j){
edges[i][j] = 0L;
}else{
edges[i][j] = null;
}
}
}
for (int[] road : roads) {
edges[road[0]][road[1]] = edges[road[1]][road[0]] = (long) road[2];
}
Long[][] tmp;
for (int k = 0; k < n; k++) {
tmp = edges.clone();
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if(i==j){
continue;
}
if(tmp[i][j]==null){
if(tmp[i][k]==null||tmp[k][j]==null){
edges[i][j] = null;
}else{
edges[i][j] = tmp[i][k]+tmp[k][j];
}
}else{
edges[i][j] = tmp[i][j];
if(tmp[i][k]!=null && tmp[k][j]!=null){
edges[i][j] = Math.min(edges[i][j],tmp[i][k]+tmp[k][j]);
}
}
}
}
}
return dfs(0,-1,0L,edges);
}
private int dfs(int node,int fa,long dis,Long[][] edges){
if(node== edges.length-1){
return 1;
}
if(memo[node]!=-1){
return memo[node];
}
int ans = 0;
for (int[] child : g[node]) {
if(child[0]!=fa && child[1]+dis==edges[0][child[0]]){
ans = (ans % mod + dfs(child[0],node,dis+edges[node][child[0]],edges) % mod) %mod;
}
}
memo[node] = ans;
return ans;
}
}
我这做法太sb了。首先Floyd跑了个O(n³),然后记忆化搜索跑了个O(n),总体O(n³),拉爆了。
这题有更好的做法。DIJK同时DP。放到DJIK中吧。