重复的DNA序列(MEDIUM)
-
如果仅利用哈希表等信息判重,会造成O(nL)O(nL)O(nL)的复杂度
-
没有充分利用仅有ATCG四种字符这种性质!
-
考虑状态压缩!利用位运算!分别编码ATCG为00,01,10,11!则每个字符串与一个20位的整数一一对应!
class Solution {
public List<String> findRepeatedDnaSequences(String s) {
Map<Integer,Boolean> a = new HashMap<>();
Map<Character,Integer> b = new HashMap<>();
b.put('A',0);b.put('T',1);b.put('C',2);b.put('G',3);
List<String> ans = new ArrayList<>();
int n = s.length();
if(n <= 10)
return ans;
int mask = (1 << 20) - 1;
//取前10个字符
int value = 0;
for(int i = 0;i<10;i++)
{
value = value | (b.get(s.charAt(i)) << (18 - 2*i));
}
a.put(value,false);
for(int i = 1;i<=n - 10;i++)
{
value = (( value << 2 ) | b.get(s.charAt(i + 9))) & mask;
if(!a.containsKey(value))
{
a.put(value,false);
}
else if(!a.get(value)) {
a.put(value, true);
ans.add(s.substring(i,i+10));
}
}
return ans;
}
}
数字范围按位与(MEDIUM)
- 开始的logn2log n^2logn2的代码,即观察left为1的位置左边第一个为0的位置再与left原先的位置相与是否小于等于right:
class Solution {
public int rangeBitwiseAnd(int left, int right) {
// long a;
int ans = 0;
for(int i = 0;i <= 30;i++)
{
if(((left >> i) & 1 )== 1)
{
int j;
for(j = i+1;j<=30 && ((left >> j) & 1 )== 1;j++);
if(j<=30)
{
if(((1 << j) | (left & (Integer.MAX_VALUE - ((1 << j) - 1)) ))>right)
ans |= (1 << i);
}
else ans |= (1 << i);
}
}
return ans;
}
}
- 优化:left和right的最长公共前缀是最终结果!从左到右考察left和right第一位不相同的位置,left为0,right为1,对left从该位往后全是0,该位为1再加上最长公共前缀构成的数在[left,right]范围内,而该位本身与right相与结果为0
- 求最长公共前缀:不断右移left,right直到两者相等
class Solution {
public int rangeBitwiseAnd(int left, int right) {
int i = 0;
while(left < right)
{
left = left >> 1;
right = right >> 1;
i++;
}
return left << i;
}
}
- Brian Kernighan 算法(不需要i计数)
- 主要原理:num和Num-1相与可去掉num最右侧的1
class Solution {
public int rangeBitwiseAnd(int left, int right) {
int i = 0;
while(left < right)
{
right &= (right - 1);
}
return right;
}
}
排列序列(HARD)
- next permutation的思路:
public String getPermutation(int n, int k) {
int[] a = new int[n + 1];
for(int i = 1;i<a.length;i++)
{
a[i ] = i ;
}
int count = 1;
while(count < k)
{
int j;
for(j = a.length - 1;j>=2 && a[j - 1] > a[j];j--);
int q;
for(q = a.length - 1;a[q] <= a[j - 1];q--);
int temp = a[q];
a[q] = a[j - 1];
a[j-1] = temp;
int low = j;
int high = a.length - 1;
while(low <= high)
{
int tmp = a[high];
a[high] = a[low];
a[low] = tmp;
low++;
high--;
}
count ++;
}
//count == k
StringBuilder s = new StringBuilder();
for(int i = 1;i<=a.length - 1;i++)
s.append((char)(a[i] + '0'));
return s.toString();
}
- 可能造成O(n!∗n)O(n! *n)O(n!∗n)的运算复杂度!
- 缩小问题规模+数学:
- 固定第一个数字,有(n-1)!个排列。根据k的大小确定第一个数字,如此递归下去。时间复杂度O(n2)O(n^2)O(n2)
public String getPermutation(int n, int k) {
List<Integer> a = new ArrayList<>();
for(int i = 1;i<=n;i++)
{
a.add(i);
}
StringBuilder s = new StringBuilder();
int acc = 0;
for(int i = 1;i<=n;i++) //n步选择
{
int n_fa = 1;
for(int j = 1;j<=(n - i);j++) {
n_fa *= j;
}
for(int j = 1;j<=n - i + 1;j++)
if(acc + n_fa * j >= k)
{
s.append(a.get(j - 1));
a.remove(j - 1);
acc += n_fa * (j - 1);
break;
}
}
return s.toString();
}
扰乱字符串-HARD
- 记忆化搜索
- 注意长度相等
- dp[i][j][k],字符串s1从第i个字符开始,s2从第j个字符开始,长度k个字符的字符串能否通过互相扰动得到。
没有优化之前(纯记忆化搜索,10ms)
class Solution {
public int dfs(int i,int j,int k,int[][][] dp,String s1,String s2)
{
if(dp[i][j][k]!=0)
return dp[i][j][k];
if(k == 1)
{
dp[i][j][k] = s1.charAt(i) == s2.charAt(j) ? 1 : -1;
return dp[i][j][k];
}
if(i + k - 1 >= s1.length() || j + k - 1 >= s2.length())
{
dp[i][j][k] = -1;
return -1;
}
int a = -1;
dp[i][j][k] = -1;
for(int len = 1 ; len <= k - 1;len++)
{
if(dfs(i,j,len,dp,s1,s2) == 1 && dfs(i + len,j + len,k - len,dp,s1,s2) == 1)
{
dp[i][j][k] = 1;
return 1;
}
if(dfs(i,j+k - len ,len,dp,s1,s2) == 1&& dfs(i +len,j,k - len,dp,s1,s2) == 1)
{
dp[i][j][k] = 1;
return 1;
}
}
return -1;
}
public boolean isScramble(String s1, String s2) {
int[][][] dp = new int[30][30][31]; //dp[i][j][k]表明从s_1开始,从s_2开始的字符串,长度为k的字符串是否能经过扰乱还原
// for(int i = 0;i<30;i++)
// for(int j = 0;j<30;j++)
// {
// if(s1.charAt(i) == s2.charAt(j))
// dp[i][j][k] = 1;
// else dp[i][j][k] = -1;
// }
int a = dfs(0,0,s1.length(),dp,s1,s2);
return a == 1;
}
public static void main(String[] args)
{
new Solution().isScramble("ab","aa");
}
}
周赛T4-移除所有载有违禁货物车厢所需的最少时间(HARD-前后缀DP)
前后缀分隔DP的MOTIVATION
- 直接前缀或后缀DP得到的困难。如果令pre[i]pre[i]pre[i]从最前直到处理到第i+1个车厢所需要的最小时间。对第i+1个车厢可能从右侧直接移除,于是preprepre不具有最优子结构。同理suf[i]suf[i]suf[i]也不具有。
- 但是所有可能的移除方法,总可将移除操作在s中分为两部分。前半部分移除仅运用1,3策略,后半部分仅运用2,3策略。(对任意解考虑最长的从左存在1策略的部分,则剩余的部分仅运用2,3策略)
- 因此最终只需要搜索分割点即可
Solution(空间复杂度O(n)O(n)O(n),遍历三次)
- 令pre[i]pre[i]pre[i]仅使用1,3策略处理从最前到s[i]需要的最小时间,若s[i]=0s[i] = 0s[i]=0,则pre[i]=pre[i−1]pre[i] = pre[i-1]pre[i]=pre[i−1];若s[i]=1s[i]=1s[i]=1则对第i+1个车厢可能直接删除,可能从s[i]最前到右删除,即pre[i]=min{pre[i−1]+2,i+1}pre[i] = min\{pre[i-1] + 2,i+1\}pre[i]=min{pre[i−1]+2,i+1}
- 由于仅采用1,3策略,不从后往前删除,pre[i−1]pre[i-1]pre[i−1]对应的方法完全不依赖于s[i]s[i]s[i]
- 同理令suf[i]suf[i]suf[i]仅使用2,3策略处理从最后到s[i]s[i]s[i]需要的最小时间,当s[i]=0,suf[i]=suf[i+1]s[i] = 0,suf[i] = suf[i+1]s[i]=0,suf[i]=suf[i+1],而s[i]=1s[i] = 1s[i]=1有:
suf[i]=min{suf[i+1]+2,s.length−i}suf[i] = \min\{ suf[i+1] + 2,s.length - i\}suf[i]=min{suf[i+1]+2,s.length−i} - 原问题:maxi{pre[i]+suf[i+1]}\max_i\{pre[i] + suf[i+1]\}maxi{pre[i]+suf[i+1]}
class Solution {
public int minimumTime(String s) {
int pre = s.charAt(0) == '0'?0 : 1;
int[] suf = new int[s.length()];
int len = s.length();
suf[len - 1] = s.charAt(len - 1) == 0?0 : 1;
for(int i = len - 2;i>=0;i--)
{
if(s.charAt(i) == '0')
suf[i] = suf[i + 1];
else suf[i] = Math.min(suf[i+1]+2,len - i);
}
int ans = suf[0];
for(int i = 0;i<len - 1;i++)
{
ans = Math.min(suf[i + 1]+pre,ans);
pre = s.charAt(i + 1) == '0'?pre:Math.min(pre + 2,i+2);
}
return Math.min(pre,ans);
}
}
Solution(空间复杂度O(1)O(1)O(1),必要条件减小搜索空间)
- 上述解法并未充分利用最优解的必要条件。
- 对最优解而言,是否存在一种等价的划分方式,划分点右侧全用策略2,左侧全用策略1,3呢?
- 对任意一个可行解,只需考虑最后一个从右边删除的不合法车厢,从该车厢(包括)向右的所有车厢都是通过策略2删除的。以该车厢作为划分点即可。
class Solution {
public int minimumTime(String s) {
int pre = s.charAt(0) == '0'?0 : 1;
int len = s.length();
int ans = len;
for(int i = 0;i<len - 1;i++)
{
ans = Math.min(pre + len - i - 1,ans);
pre = s.charAt(i + 1) == '0'?pre:Math.min(pre + 2,i+2);
}
return Math.min(pre,ans);
}
}
- 主要分析解满足什么样的形式,在这样的形式下搜索最优解。例如这道题的最优解一定满足:从左边一部分使用策略1删除,中间一部分使用策略3删除,后边使用策略2删除。中间部分和左边部分可以合并成pre变量。在这样的搜索空间下,确定分割点得到最优解。
双周赛T4-设置最小时间的代价(HARD)
- 和上一道DP类似,枚举分割点(实际上是划分解空间)+优先队列,和k递增那道题类似:考察删除后数组的性质(分析最优解所满足的必要条件),前n个元素一定是某个分割点左侧n个最大元素,后n个元素相反。
- 求最大n个元素(最小n个元素和),使用优先队列
class Solution {
public long minimumDifference(int[] nums) {
PriorityQueue<Integer> q_min = new PriorityQueue<>();
int n = nums.length / 3;
long[] right = new long[nums.length];
for(int i = nums.length - 1;i>=2 * n ;i--) {
right[n] += (long)nums[i];
q_min.add(nums[i]);
}
for(int i = 2 * n - 1;i>=n;i--)
{
if(nums[i] > q_min.peek())
{
int a = q_min.poll();
q_min.add(nums[i]);
right[3*n - i] = right[3*n - i - 1] - (long)a + (long)nums[i];
}
else right[3*n - i] = right[3*n - i - 1];
}
long left = 0;
PriorityQueue<Integer> q_max = new PriorityQueue<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return -Integer.compare(o1,o2);
}
});
for(int i = 0;i<n;i++)
{
q_max.add(nums[i]);
left += (long)nums[i];
}
long ans = Long.MAX_VALUE;
ans = Math.min(ans,left - right[2*n]);
for(int i = n ;i<=2*n - 1;i++)
{
if(nums[i] < q_max.peek())
{
int a = q_max.poll();
q_max.add(nums[i]);
left = left + (long)nums[i] - (long)a;
}
ans = Math.min(ans,left - right[3*n - i - 1]);
}
return ans;
}
}