题目来源:力扣
给定一个字符串数组 arr,字符串 s 是将 arr 某一子序列字符串连接所得的字符串,如果 s 中的每一个字符都只出现过一次,那么它就是一个可行解。
请返回所有可行解 s 中最长长度。-
======================================
示例 1:
输入:arr = [“un”,“iq”,“ue”]
输出:4
解释:所有可能的串联组合是 “”,“un”,“iq”,“ue”,“uniq” 和 “ique”,最大长度为 4。
=====================================
刚看到这道题目的时候,我使用了与黄金矿工同样的回溯算法设计解决方案:
假设当前字符串被选中,如果当前字符串中的字符与路径上其他字符串的字符相同,则该字符串不是可行解,返回0,如果该字符串可行,则标记该字符串,并对剩余所有字符串进行搜索,返回最长的拼接字符串长度.在递归退出时,取消对当前字符串的标记.
我们可以使用将字符串加入集合与移除集合的方式完成字符串的标记与取消标记.同时,为了检查已有字符串是否为可行解,我们需要在标记字符串的同时,记录该字符串中的字符个数.我们可以使用整数数组纪录.
该方法在测试题目给出的集合案例时,通过了,但当字符串较大时,则失败.
当我进一步分析该问题后,发现我这样设计的回溯算法其实是对数组中的所有字符串进行了全排列,对每一排列{a, b, c, ...}
, 从前向后添加字符,保证每次添加的字符串中字符与已有字符不重复,添加的字符总数即为该排列所有的最长拼接字符串.然后返回最长的拼接字符串.仔细分析会发现,字符串的次序在最优解中并无任何作用.而排列则考虑了字符串的次序,全排列的时间复杂度为O(N!).这是非常大的时间复杂度!由于我们并不关心字符串的次序,因此我们真正需要的是组合而非排列.
对于组合问题,每一字符串要么出现在最优解中,要么不出现在最优解中.因此其时间复杂度为 O ( 2 N ) O(2^N) O(2N),这要远低于 O ( N ! ) O(N!) O(N!).
我们可以顺序遍历每一字符串,如果当前字符串可以加入(不与已有字符重合),则我们可以分别计算当前字符串被加入与不被加入的拼接字符串长度,并返回最大值.如果当前字符串不能被加入,则我们直接返回其不被加入的拼接字符串长度.
class Solution {
Set<String> strs = new HashSet<>();
int[] chars = new int[26]; //纪录当前每个字符出现的次数
private boolean canContain(String s){
for(int i = 0; i < s.length(); i++){
chars[s.charAt(i)-'a']++;
}
boolean unique = true;
for(int n: chars){
if(n > 1){
unique = false;
break;
}
}
for(int i = 0; i < s.length(); i++){
chars[s.charAt(i)-'a']--;
}
return unique;
}
private void addStringToCharSet(String s){
for(int i = 0; i < s.length(); i++){
chars[s.charAt(i)-'a']++;
}
}
private void removeStringFromCharSet(String s){
for(int i = 0; i < s.length(); i++){
chars[s.charAt(i)-'a']--;
}
}
private int dfs(List<String> arr, String s){
//如果当前s不可行,返回0
//如果s中任一字符与已有字符重复
for(int i = 0; i < s.length(); i++)
if(chars.contains(s.charAt(i)))
return 0;
//进入标记
strs.add(s);
addStringToCharSet(s);
int length = 0;
for(String a: arr){
if(!strs.contains(a)){
System.out.printf("%s: %s\n", s, a);
length = Math.max(length, dfs(arr, a));
}
}
//退出取消标记
strs.remove(s);
removeStringFromCharSet(s);
return length + s.length();
}
public int maxLength(List<String> arr) {
int maxLength = 0;
for(String s: arr)
maxLength = Math.max(maxLength, dfs(arr, s));
return maxLength;
}
}
class Solution {
//所有结果有2^n种
//遍历所有路径,求出可行路径中的最多字符数
int[] chars = new int[26]; //纪录当前每个字符出现的次数
private boolean canContain(String s){
for(int i = 0; i < s.length(); i++){
chars[s.charAt(i)-'a']++;
}
boolean unique = true;
for(int n: chars){
if(n > 1){
unique = false;
break;
}
}
for(int i = 0; i < s.length(); i++){
chars[s.charAt(i)-'a']--;
}
return unique;
}
private void addStringToCharSet(String s){
for(int i = 0; i < s.length(); i++){
chars[s.charAt(i)-'a']++;
}
}
private void removeStringFromCharSet(String s){
for(int i = 0; i < s.length(); i++){
chars[s.charAt(i)-'a']--;
}
}
private int dfs(List<String> arr, int i){
if(i >= arr.size())
return 0;
String s = arr.get(i);
int maxLength = 0;
if(canContain(s)){
addStringToCharSet(s);
maxLength = s.length() + dfs(arr, i+1);
removeStringFromCharSet(s);
}
//计算不加入当前字符串时的最大长度
maxLength = Math.max(maxLength, dfs(arr, i+1));
return maxLength;
}
public int maxLength(List<String> arr) {
return dfs(arr, 0);
}
}