3213. 最小代价构造字符串

给你一个字符串 target、一个字符串数组 words 以及一个整数数组 costs,这两个数组长度相同。

设想一个空字符串 s

你可以执行以下操作任意次数(包括 次):

  • 选择一个在范围 [0, words.length - 1] 的索引 i
  • words[i] 追加到 s
  • 该操作的成本是 costs[i]

返回使 s 等于 target最小 成本。如果不可能,返回 -1

示例 1:

输入: target = “abcdef”, words = [“abdef”,“abc”,“d”,“def”,“ef”], costs = [100,1,1,10,5]

输出: 7

解释:

  • 选择索引 1 并以成本 1 将 "abc" 追加到 s,得到 s = "abc"
  • 选择索引 2 并以成本 1 将 "d" 追加到 s,得到 s = "abcd"
  • 选择索引 4 并以成本 5 将 "ef" 追加到 s,得到 s = "abcdef"

示例 2:

输入: target = “aaaa”, words = [“z”,“zz”,“zzz”], costs = [1,10,100]

输出: -1

解释:

无法使 s 等于 target,因此返回 -1。

提示:

  • 1 <= target.length <= 5 * 10^4
  • 1 <= words.length == costs.length <= 5 * 10^4
  • 1 <= words[i].length <= target.length
  • 所有 words[i].length 的总和小于或等于 5 * 10^4
  • targetwords[i] 仅由小写英文字母组成。
  • 1 <= costs[i] <= 10^4

字典树 + 记忆化搜索dp

(周赛竟然忘记“记忆操作”???memo[idx] = (int)res;)

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
class Trie {
class TrieNode {
boolean end;
// int cnt; // 维护节点数量
TrieNode[] children = new TrieNode[26];
int cost = -1;
}
TrieNode root = new TrieNode();
void insert(String word, int cost) {
TrieNode cur = root;
for (char c : word.toCharArray()) {
int index = (int) c - 97;
if (cur.children[index] == null) {
cur.children[index] = new TrieNode();
}
cur = cur.children[index];
}
cur.end = true;
if (cur.cost != -1) cur.cost = Math.min(cur.cost, cost);
else cur.cost = cost;
}
boolean search(String word) {
TrieNode cur = root;
for (char c : word.toCharArray()) {
int index = (int) c - 97;
if (cur.children[index] == null) return false;
cur = cur.children[index];
}
return cur.end;
}
}
class Solution {
Trie tr = new Trie();
public int minimumCost(String target, String[] words, int[] costs) {
int n = words.length;
for (int i = 0; i < n; i++) tr.insert(words[i], costs[i]);
int memo[] = new int[target.length()];
Arrays.fill(memo, -1);
int res = dfs(0, target, memo);
return res == (int)1e9 ? -1 : res;
}
private int dfs(int idx, String target, int memo[]) {
if (idx == target.length()) return 0;
if (memo[idx] != -1) return memo[idx];
Trie.TrieNode cur = tr.root;
long res = (long)1e9;
for (int i = idx; i < target.length(); i++) {
int c = (int)target.charAt(i) - 97;
if (cur.children[c] != null) {
cur = cur.children[c];
if (cur.end) res = Math.min(res, dfs(i+1, target, memo) + cur.cost);
}
else break;
}
memo[idx] = (int)res;
return (int)res;
}
}

字符串哈希 + dp

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
class Solution {
public int minimumCost(String target, String[] words, int[] costs) {
char[] t = target.toCharArray();
int n = t.length;

// 多项式字符串哈希(方便计算子串哈希值)
// 哈希函数 hash(s) = s[0] * base^(n-1) + s[1] * base^(n-2) + ... + s[n-2] * base + s[n-1]
final int MOD = 1_070_777_777;
final int BASE = (int) 8e8 + new Random().nextInt((int) 1e8); // 随机 base,防止 hack
int[] powBase = new int[n + 1]; // powBase[i] = base^i
int[] preHash = new int[n + 1]; // 前缀哈希值 preHash[i] = hash(target[0] 到 target[i-1])
powBase[0] = 1;
for (int i = 0; i < n; i++) {
powBase[i + 1] = (int) ((long) powBase[i] * BASE % MOD);
preHash[i + 1] = (int) (((long) preHash[i] * BASE + t[i]) % MOD); // 秦九韶算法计算多项式哈希
}

Map<Integer, Map<Integer, Integer>> minCost = new TreeMap<>(); // 长度 -> 哈希值 -> 最小成本
for (int i = 0; i < words.length; i++) {
long h = 0;
for (char b : words[i].toCharArray()) {
h = (h * BASE + b) % MOD;
}
minCost.computeIfAbsent(words[i].length(), k -> new HashMap<>())
.merge((int) h, costs[i], Integer::min);
}

int[] f = new int[n + 1];
Arrays.fill(f, Integer.MAX_VALUE / 2);
f[0] = 0;
for (int i = 1; i <= n; i++) {
for (Map.Entry<Integer, Map<Integer, Integer>> e : minCost.entrySet()) {
int len = e.getKey();
if (len > i) {
break;
}
// 计算子串 target[i-sz] 到 target[i-1] 的哈希值(计算方法类似前缀和)
int subHash = (int) (((preHash[i] - (long) preHash[i - len] * powBase[len]) % MOD + MOD) % MOD);
f[i] = Math.min(f[i], f[i - len] + e.getValue().getOrDefault(subHash, Integer.MAX_VALUE / 2));
}
}
return f[n] == Integer.MAX_VALUE / 2 ? -1 : f[n];
}
}

双模字符串哈希 + dp

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
// 基于上面的「更快写法」
class Solution {
public int minimumCost(String target, String[] words, int[] costs) {
char[] t = target.toCharArray();
int n = t.length;
final int MOD1 = 1_070_777_777;
final int MOD2 = 1_000_000_007;
final int BASE1 = (int) 8e8 + new Random().nextInt((int) 1e8);
final int BASE2 = (int) 8e8 + new Random().nextInt((int) 1e8);
int[] powBase1 = new int[n + 1];
int[] powBase2 = new int[n + 1];
int[] preHash1 = new int[n + 1];
int[] preHash2 = new int[n + 1];
powBase1[0] = powBase2[0] = 1;
for (int i = 0; i < n; i++) {
powBase1[i + 1] = (int) ((long) powBase1[i] * BASE1 % MOD1);
powBase2[i + 1] = (int) ((long) powBase2[i] * BASE2 % MOD2);
preHash1[i + 1] = (int) (((long) preHash1[i] * BASE1 + t[i]) % MOD1);
preHash2[i + 1] = (int) (((long) preHash2[i] * BASE2 + t[i]) % MOD2);
}

Map<Long, Integer> minCost = new HashMap<>(); // 哈希值 -> 最小成本
for (int i = 0; i < words.length; i++) {
long h1 = 0;
long h2 = 0;
for (char b : words[i].toCharArray()) {
h1 = (h1 * BASE1 + b) % MOD1;
h2 = (h2 * BASE2 + b) % MOD2;
}
minCost.merge(h1 << 32 | h2, costs[i], Integer::min);
}

// 有 O(√L) 个不同的长度
Set<Integer> lengthSet = new HashSet<>();
for (String w : words) {
lengthSet.add(w.length());
}
int[] sortedLens = new int[lengthSet.size()];
int k = 0;
for (int len : lengthSet) {
sortedLens[k++] = len;
}
Arrays.sort(sortedLens);

int[] f = new int[n + 1];
Arrays.fill(f, Integer.MAX_VALUE / 2);
f[0] = 0;
for (int i = 1; i <= n; i++) {
for (int len : sortedLens) {
if (len > i) {
break;
}
// 计算子串 target[i-sz] 到 target[i-1] 的哈希值(计算方法类似前缀和)
long subHash1 = ((preHash1[i] - (long) preHash1[i - len] * powBase1[len]) % MOD1 + MOD1) % MOD1;
long subHash2 = ((preHash2[i] - (long) preHash2[i - len] * powBase2[len]) % MOD2 + MOD2) % MOD2;
long subHash = subHash1 << 32 | subHash2;
f[i] = Math.min(f[i], f[i - len] + minCost.getOrDefault(subHash, Integer.MAX_VALUE / 2));
}
}
return f[n] == Integer.MAX_VALUE / 2 ? -1 : f[n];
}
}