题目
LeetCode 元の質問
n 種類のステッカーがあります。各ステッカーには小文字の英単語が付いています。
集めたステッカーから個々の文字を切り取って並べ替えることで、指定された文字列ターゲットをスペルアウトしたいとします。各ステッカーは何度でも使用でき、各ステッカーの数に制限はありません。
ターゲットを綴るのに必要なステッカーの最小数を返します。タスクが不可能な場合は -1 を返します。
例: 文字ターゲット = "aaabc"、ステッカー ステッカー = {"aad"、"sk"、"eq"、"bc"}、ターゲットを詳しく説明するために必要なステッカーの最小数は 3 つで、そのうちのステッカーが必要です。 aad" 2 枚のカードを使用して aaa を消去し、1 枚の bc を使用して残りの文字列 "bc" を消去します。
暴力的再帰は
依然として暴力的再帰を解決する最初の方法であり、その後、暴力的再帰に基づく動的計画法に移ります。全体的な考え方は次のとおりです。
- すべてのステッカーを反復処理し、ステッカーとターゲット文字列の重複部分を削除し、ターゲット文字列の残りの部分を返します。
- 返された休符文字列とターゲットを比較します。長さが同じでない場合は、選択したステッカー文字列とターゲットに重複部分があることを意味します。ターゲット文字列の長さが 0 になるまで休符を渡し続けます。
コード
このコードは論理的には正しいのですが、LeetCodeで実行すると制限時間を超えてしまいますので、次に「枝刈り」による最適化を行います。
public static int minStickers(String[] stickers, String target) {
int ans = process(stickers, target);
return ans == Integer.MAX_VALUE ? -1 : ans;
}
//process返回最少贴纸数
public static int process(String[] stickers, String target) {
//如果当前target字符串剩余长度为0,说明所有字符已经被贴纸消除,return 0
if (target.length() == 0) {
return 0;
}
//用 Integer.MAX_VALUE来标记是否有最小贴纸数量,如果没有,则min的值不会变
int min = Integer.MAX_VALUE;
//遍历所有贴纸
for (String first : stickers) {
//rest是target字符串和贴纸刨去重合部分的剩余字符串
//注意:此处已经使用了一张贴纸
String rest = minus(target, first);
//如果长度不相等,说明这张贴纸有被使用
if (rest.length() != target.length()) {
//将剩余字符串继续向下递归。
min = Math.min(min, process(stickers, rest));
}
}
//因为之前遍历贴纸时,使用了一张,如果此时min != Integer.MIN_VALUE,说明target清零了,有贴纸使用,则要把上面使用的那张贴纸加回来
return min + (min == Integer.MAX_VALUE ? 0 : 1);
}
//去除target和贴纸重合字符部分的字符串 并返回剩余字符串
public static String minus(String s1, String s2) {
char[] chars1 = s1.toCharArray();
char[] chars2 = s2.toCharArray();
int[] counts = new int[26];
for (char c1 : chars1) {
counts[c1 - 'a']++;
}
for (char c2 : chars2) {
counts[c2 - 'a']--;
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 26; i++) {
if (counts[i] > 0) {
for (int j = 0; j < counts[i]; j++) {
sb.append((char) (i + 'a'));
}
}
}
return sb.toString();
}
枝刈りの最適化
枝刈りの最適化は、暴力的再帰における一般的な最適化方法です。
最も重要な点の 1 つは、いくつかの条件判断を通じて再帰的コードの呼び出しを減らすことです。
たとえば、激しい再帰では、すべてのステッカーが毎回走査され、非常に非効率的です。プルーニングの最適化は、無駄なステッカーの走査を減らすことです。2 番目の最適化は、ステッカー[] を int[][] に変換し、ターゲットの Int を変換することです[]で論理判定を満たしていれば直接減算するので、毎回char[]に変換してから激しい再帰で走査する方法より効率が格段に速いです。
最適化されたコード
public static int minStickers2(String[] stickers, String target) {
int N = stickers.length;
int[][] arrs = new int[N][26];
//主流程中,先将给定的固定贴纸初始化成int[][]
for (int i = 0; i < N; i++) {
char[] chars = stickers[i].toCharArray();
for (char cha : chars) {
arrs[i][cha - 'a']++;
}
}
int ans = process2(arrs, target);
return ans == Integer.MAX_VALUE ? -1 : ans;
}
public static int process2(int[][] stickers, String target) {
if (target.length() == 0) {
return 0;
}
int min = Integer.MAX_VALUE;
int N = stickers.length;
int[] tcount = new int[26];
char[] chars = target.toCharArray();
//将每一次传进来的target字符串转换成int[],方便后面直接相减
for (char cha : chars) {
tcount[cha - 'a']++;
}
for (int i = 0; i < N; i++) {
//获取每一张贴纸
int[] sticker = stickers[i];
//只有贴纸中,包含target中[0]位置字符串,才考虑往下进行
if (sticker[chars[0] - 'a'] > 0) {
StringBuilder sb = new StringBuilder();
for (int j = 0; j < 26; j++) {
//如果tcount[j] > 0,说明target中有这个字符
if (tcount[j] > 0) {
//取值,直接相减
int num = tcount[j] - sticker[j];
//将target中剩余字符append拼接
for (int m = 0; m < num; m++) {
sb.append((char) (j + 'a'));
}
}
}
String rest = sb.toString();
//继续向下传递
min = Math.min(min, process2(stickers, rest));
}
}
return min + (min == Integer.MAX_VALUE ? 0 : 1);
}
コードに基づく枝刈り最適化の原理は、実際には理解するのが難しくありません。ステッカーにターゲットの文字が含まれている場合、ターゲットの文字列を削除するプロセスで、最終的にターゲットの最初の文字が 1 つのステップで削除されます。 0の場合は、最初の文字列を0として削除する処理を進めますが、特定の結果には影響しません。
もちろん、このプロセスでは最悪の結果に遭遇する可能性もありますが、最悪のケースは、暴力的な再帰でのトラバースよりも優れています。
動的プログラミング
この動的プログラミング変換は、以前の変換とは異なります。以前の変換の全体的なプロセスは、暴力的プログラミング -> 愚かなキャッシュ -> 動的プログラミングです。ただし、変数パラメータが対象文字列であり、ステッカーによってはその都度残りの文字列が返される可能性が多すぎて境界がなく、強制的にキャッシュしてもキャッシュテーブルが壊れてしまう可能性があります。大きすぎると非常に複雑になるため、Map をキャッシュ テーブルとしてコードに直接追加します。
public static int minStickers3(String[] stickers, String target) {
int N = stickers.length;
int[][] arrs = new int[N][26];
for (int i = 0; i < N; i++) {
char[] chars = stickers[i].toCharArray();
for (char cha : chars) {
arrs[i][cha - 'a']++;
}
}
HashMap<String,Integer> dp = new HashMap<>();
int ans = process3(arrs, target,dp);
return ans == Integer.MAX_VALUE ? -1 : ans;
}
public static int process3(int[][] stickers, String target, HashMap<String,Integer> dp) {
if (dp.containsKey(target)){
return dp.get(target);
}
if (target.length() == 0) {
return 0;
}
int min = Integer.MAX_VALUE;
int N = stickers.length;
int[] tcount = new int[26];
char[] chars = target.toCharArray();
for (char cha : chars) {
tcount[cha - 'a']++;
}
for (int i = 0; i < N; i++) {
int[] sticker = stickers[i];
if (sticker[chars[0] - 'a'] > 0) {
StringBuilder sb = new StringBuilder();
for (int j = 0; j < 26; j++) {
if (tcount[j] > 0) {
int num = tcount[j] - sticker[j];
for (int m = 0; m < num; m++) {
sb.append((char) (j + 'a'));
}
}
}
String rest = sb.toString();
min = Math.min(min, process3(stickers, rest,dp));
}
}
int ans = min + (min == Integer.MAX_VALUE ? 0 : 1);
dp.put(target,ans);
return ans;
}