不同的子序列(HARD)
- 记忆化搜索或者后缀DP即可
记忆化搜索
class Solution {
public int dfs(int[][] dp,int i,int j,String s,String t,int len_s,int len_t)
{
if(i >= s.length())
return 1;
if(j >= t.length())
return 0;
if(dp[i][j] != -1)
return dp[i][j];
if(len_s - i > len_t - j)
{
dp[i][j] = 0;
return 0;
}
int b = dfs(dp,i,j+1,s,t,len_s,len_t);
if(s.charAt(i) == t.charAt(j))
{
dp[i][j] = dfs(dp,i+1,j+1,s,t,len_s,len_t) + b;
return dp[i][j];
}
dp[i][j] = b;
return b;
}
public int numDistinct(String s, String t) {
int len_s = s.length();
int len_t = t.length();
int[][] dp = new int[len_t][len_s];
for(int i = 0;i<dp.length;i++)
Arrays.fill(dp[i],-1);
return dfs(dp,0,0,t,s,len_t,len_s);
}
public static void main(String[] args)
{
int a = new Solution().numDistinct("rabbbit","rabbit");
System.out.println(a);
}
}
后缀DP
public int numDistinct(String s, String t) {
int len_s = s.length();
int len_t = t.length();
int[] pre = new int[s.length()];
for(int i = pre.length - 1;i>=0;i--)
{
if(i == pre.length - 1)
pre[i] = s.charAt(i) == t.charAt(len_t - 1)?1:0;
else pre[i] = pre[i + 1] + (s.charAt(i) == t.charAt(len_t - 1)?1:0);
}
int[] dp = new int[s.length()];
for(int i = len_t - 2;i>=0;i--)
{
for(int j = len_s - 1 ;j>=0;j--)
{
if(j == len_s - 1)
dp[j] = 0;
else
{
dp[j] = (t.charAt(i) == s.charAt(j)?pre[j + 1]:0) + dp[j + 1];
}
}
System.arraycopy(dp,0,pre,0,pre.length);
}
return pre[0];
}
文本左右对齐(HARD)
- 模拟即可
class Solution {
public List<String> fullJustify(String[] words, int maxWidth) {
int i = 0;
List<String> a = new ArrayList<>();
while(i < words.length)
{
int total_count = words[i].length();
int w_len = words[i].length();
int j = i + 1;
while(j < words.length && total_count <= maxWidth)
{
total_count += words[j].length();
total_count += 1;
w_len += words[j].length();
j++;
}
StringBuilder s = new StringBuilder();
if(j >= words.length && total_count <= maxWidth) //最后一行
{
for(int k = i;k<j;k++)
{
s.append(words[k]);
if(k != j - 1)
s.append(' ');
}
s.append(" ".repeat(maxWidth - total_count));
a.add(s.toString());
}
else //没有最后一行
{
j--;
w_len -= words[j].length(); //单词数目
j--;
if(i == j)
{
a.add(words[j] + " ".repeat(maxWidth - words[j].length()));
}
else
{
int e_s = (maxWidth - w_len) / (j - i); //每个空格数目
int extra = (maxWidth - w_len) - (e_s * (j - i)); //左侧额外添加空格的数目
for(int k = i;k<=j - 1;k++)
{
s.append(words[k]);
s.append(k - i + 1 <= extra ? " ".repeat(e_s + 1):" ".repeat(e_s));
}
s.append(words[j]);
a.add(s.toString());
}
}
i = j + 1;
}
return a;
}
}
复习-中缀表达式转换为逆波兰表达式
- 遇到操作数直接输出
- 遇到左括号入栈
- 遇到右括号弹栈直到遇到左括号(左括号也跟着弹栈)
- 遇到运算符弹栈直到栈空或者栈顶运算符的优先级高于栈中其他操作符的优先级
- 遍历完成后将栈中所有元素弹空
基本计算器(HARD)仅含加减,数字,括号的情况(思路是参考官方的,不使用逆波兰表达式方法)
- 由于仅存在加法,减法,括号,考虑括号展开法则。
- 对每对括号里的部分设置"符号状态"。使用栈保存当前的符号状态。
- 对于一对括号掌管的部分而言,遇到左括号就在栈顶设置一个当前"符号状态"。
- 对没进入新的括号的部分,如果遇到+,则使用当前栈顶的符号状态赋予当前操作数,否则使用当前符号状态的相反数赋予当前操作数。
- 遇到右括号就将栈顶的符号弹出表明该对括号掌管的符号状态已经结束。
- 整个表达式可以看作是0 + (表达式)的形式。故初始的符号状态就是正。
class Solution {
public int calculate(String s) {
int sign = 1;
Stack<Integer> op = new Stack<>();
int i = 0;
op.push(1);
long num = 0;
long ret =0 ;
while(i < s.length())
{
char ch = s.charAt(i);
if(ch == '+')
{
sign = op.peek(); //使用当前的符号状态
}
else if(ch == '-')
{
sign = -op.peek(); //符号状态与栈顶符号状态即当前符号状态相反
}
else if(ch == '(') //保存当前符号状态
{
op.push(sign);
}
else if(ch == ')')
{
op.pop(); //当前括号掌管的sign结束
}
else if(ch != ' ')
{
while(i<s.length() && '0'<=s.charAt(i)&&s.charAt(i)<='9')
{
num = num * 10 + (s.charAt(i) - '0');
i++;
}
ret += sign * num;
num = 0;
continue;
}
i++;
}
return (int)ret;
}
}
分发糖果(HARD)
拓扑排序(效率低,没有充分利用相邻关系)
- 建图:每人作为一个结点,如果结点A必须比结点B必须得到的糖果多则从结点B到结点A连一条边。该图为有向无环图(但不一定连通)。
- 对原图中每个入度为0的结点以起始点拓扑排序,求出结果。
class Solution {
//topological sort
public int candy(int[] ratings) {
int[] dis = new int[ratings.length];
Arrays.fill(dis,-1);
//邻接表
Map<Integer,List<Integer>> a = new HashMap<>();
//入度
int[] ind = new int[ratings.length];
//建图
for(int i = 0;i<ratings.length;i++)
{
List<Integer> b = new ArrayList<>();
if(i + 1 < ratings.length && ratings[i + 1] > ratings[i]) {
b.add(i + 1);
ind[i+1]++;
}
if(i - 1 >= 0 && ratings[i - 1] > ratings[i]) {
b.add(i - 1);
ind[i-1]++;
}
a.put(i,b);
}
for(int i = 0;i<ratings.length;i++)
{
if(dis[i] == -1 && ind[i] == 0)
{
dis[i] = 1;
Queue<Integer> q = new LinkedList<>();
q.add(i);
while(!q.isEmpty())
{
int v = q.poll();
for(int k : a.get(v))
{
dis[k] = Math.max(dis[v]+1,dis[k]);
ind[k]--;
if(ind[k] == 0)
{
q.add(k);
}
}
}
}
}
int ans = 0;
for(int i = 0;i<dis.length;i++)
ans += dis[i];
return ans;
}
}
左到右,右到左两次遍历( O ( n ) 空 间 O(n)空间 O(n)空间)
- 按结点编号排成链式,仅相邻两点存在边
- 先从左向右遍历从左到右的边,得到的解就是最优解的下界
- 再从右向左遍历从右到左的边,再次得到一个最优解的下界
- 将每个结点两个最优解下界取最大值就是最优解。只需证明得到的解是可行解即可。对第i个结点分类讨论:
- 如 r a t e [ i − 1 ] > r a t e [ i ] , r a t e [ i ] < r a t e [ i + 1 ] rate[i - 1] > rate[i],rate[i] < rate[i+1] rate[i−1]>rate[i],rate[i]<rate[i+1],则candy[i]=1,从左到右遍历使得最终 c a n d y [ i + 1 ] > = 1 + 1 = 2 , c a n d y [ i − 1 ] candy[i+1] >=1+1=2,candy[i-1] candy[i+1]>=1+1=2,candy[i−1]同理。
- 如 r a t e [ i ] > r a t e [ i − 1 ] , r a t e [ i ] > r a t e [ i + 1 ] rate[i] > rate[i-1],rate[i]>rate[i+1] rate[i]>rate[i−1],rate[i]>rate[i+1],则i+1仅在从右到左才有可能大于1.而从右到左遍历又使得 c a n d y [ i ] > c a n d y [ i + 1 ] candy[i] > candy[i+1] candy[i]>candy[i+1]。i结点同理
- 如 r a t e [ i − 1 ] < r a t e [ i ] < r a t e [ i + 1 ] rate[i-1]<rate[i] < rate[i+1] rate[i−1]<rate[i]<rate[i+1], r a t e [ i − 1 ] rate[i-1] rate[i−1]仅在从左到右才可能大于1,而从左到右 c a n d y [ i ] > c a n d y [ i − 1 ] candy[i] > candy[i-1] candy[i]>candy[i−1]。对 c a n d y [ i + 1 ] candy[i+1] candy[i+1]无论是从左到右,从右到左的结果均大于 c a n d y [ i ] candy[i] candy[i]。
- 其余情况均对称
class Solution {
public int candy(int[] ratings) {
int[] left = new int[ratings.length];
int[] right = new int[ratings.length];
left[0] = 1;
for(int i = 1;i<ratings.length;i++)
{
if(ratings[i] > ratings[i-1])
left[i] = left[i - 1] + 1;
else left[i] = 1;
}
right[ratings.length - 1]= 1;
int ans = Math.max(right[ratings.length - 1],left[ratings.length - 1]);
for(int i = ratings.length - 2;i>=0;i--)
{
if(ratings[i] > ratings[i + 1])
right[i] = right[i+1] + 1;
else right[i] = 1;
ans += Math.max(right[i],left[i]);
}
return ans;
}
}
常数空间做法
- 如何贪心的尽可能小得分糖果。
- 结点1分1个开始,后面一个结点得rate大于前一个结点,后面结点个数为前一个结点个数加1.否则,考察最长递减序列的情况即可(当当前结点的数量减为0时,应该将该递减序列的哪些部分糖果加1)。
class Solution {
public int candy(int[] ratings) {
int pre = 1;
int lg_dec = 1; //当前最长递减子序列长度
int ans = 1;
int dif = 0;
for(int i = 1;i<ratings.length;i++)
{
if(ratings[i] == ratings[i - 1])
{
lg_dec = 1;
pre = 1;
ans += 1;
dif = 0;
}
else if(ratings[i] < ratings[i - 1])
{
if(lg_dec == 1)
{
ans+=(dif == 0?2:1);
if(dif == 0)
dif = 1;
}
else
{
ans += (dif == 1?lg_dec:lg_dec - 1) + 1;
if(dif != 1)
dif--;
}
pre = 1;
lg_dec++;
}
else
{
pre++;
ans += pre;
lg_dec = 1;
dif = pre - 1;
}
}
return ans;
}
}
代码优化
- Inc始终记录可能进入递减字串的第一个数值大小
class Solution {
public int candy(int[] ratings) {
int pre = 1;
int ans = 1;
int inc = 1;
int dif = 0;
for(int i = 1;i<ratings.length;i++)
{
if(ratings[i] == ratings[i - 1])
{
pre = 1;
ans += 1;
inc = 1;
dif = 0;
}
else if(ratings[i] < ratings[i - 1])
{
dif++;
if(dif == inc)
dif++;
pre = 1;
ans+=dif;
}
else
{
pre++;
ans += pre;
inc = pre;
dif = 0;
}
}
return ans;
}
}
复习:KMP算法
- KMP算法模板
class Solution {
//haystack中寻找needle出现的第一个位子
public int strStr(String haystack, String needle) {
//空串
if(needle.equals(""))
return 0;
//next数组
int[] next = new int[needle.length()];
int j,k;
j=0;k=-1;
next[0]=-1;//第一个字符前无字符串,给值-1
while (j<needle.length()-1)
{
if (k==-1 || needle.charAt(j)==needle.charAt(k))
{
j++;k++;
next[j]=k;
}
else
{
k=next[k];
}
}
// 模式匹配
j = k = 0;
while(j < haystack.length() && k < needle.length())
{
if(k == -1 || haystack.charAt(j) == needle.charAt(k))
{
k++;
j++;
}
else
k = next[k];
}
if(k >= needle.length())
{
return j - needle.length();
}
else return -1;
}
}
分隔回文串(HARD)
- 直接dp求解回文串会超时( O ( n 2 ) O(n^2) O(n2)),但我们是求解最长前缀回文串,dp求解了任意字串是否回文串
KMP算法的MOTIVATION
- 最长前缀回文串,逆序后该串与自身相同,且处于原始串s的前缀。因此将s逆序放在后面,利用KMPnext数组的性质求解最长真前缀真后缀串
- 但为避免产生真前缀串或真后缀串同时包含原始串和逆序串的字符,拼接中央插入字符#
构造思路
- 将字符串s逆序得到 s , s^, s,,构造字符串 s # s , a s\#s^,a s#s,a,#是字符#,a是任意字符。问题转换为求最后字符a的next数组值(即最长真前缀串和最长真后缀串),此值减1
public String shortestPalindrome(String s) {
StringBuilder s1 = new StringBuilder();
s1.append(s); s1.append('#');
for(int i = s.length() - 1;i>=0;i--)
s1.append(s.charAt(i));
s1.append('a');
s = s1.toString();
int[] next = new int[s.length()];
int j,k;
j=0;k=-1;
next[0]=-1;//第一个字符前无字符串,给值-1
while (j<s.length()-1)
{
if (k==-1 || s.charAt(j)==s.charAt(k))
{
j++;k++;
next[j]=k;
}
else
{
k=next[k];
}
}
int len = (s.length() - 2) / 2;
return s.substring(len + 1,len + 1 + len - next[next.length - 1]) + s.substring(0,len);
}
ROLLING哈希映射
- motivation:前缀:考虑滑动窗口
- 枚举每个可能前缀位置。原始字符串前缀的hashing值,逆转字符串对应后缀的hashing值。认为hashing相等字符串就相等,选取合适的function使得不碰撞。
- function的选取:将字符串看成base进制数。大于字符集大小(种类数)作为base
f u n c t i o n [ s ] = ( s [ 0 ] ∗ b a s e + s [ 1 ] ∗ b a s e 2 + ⋯ + s [ n − 1 ] ∗ b a s e n − 1 ) % m o d function[s] = (s[0] * base + s[1] * base^2 + \cdots + s[n-1] * base^{n-1} ) \% mod function[s]=(s[0]∗base+s[1]∗base2+⋯+s[n−1]∗basen−1)%mod
mod选取字符串平方级别的质数。碰撞概率小。(只能是通过测试用例)
class Solution {
public String shortestPalindrome(String s) {
long base = 131, mod = 1000000007;
long hash1 = 0,hash2 = 0;
int max_len = 0;
long a = 1;
StringBuilder ans = new StringBuilder(s);
ans.reverse();
for(int j = 0,k = s.length() - 1;j<s.length();j++,k--)
{
hash1 = ((base * hash1) % mod + s.charAt(j) % mod) % mod;
hash2 = (((ans.charAt(k) % mod) * a) % mod + hash2) % mod;
if(hash1 == hash2)
max_len = j + 1;
a = (a * (base % mod)) % mod;
}
ans = new StringBuilder();
for(int j = s.length() - 1;j>=max_len;j--)
ans.append(s.charAt(j));
ans.append(s);
return ans.toString();
}
}
给表达式添加运算符(HARD)
方案1(击败12%)
- 考虑第一个运算符后的子问题即可
class Solution {
//求解字符串s从i开始的所有解
// m_acc代表当前得到的乘积累积结果
// fan表示添加的运算符是否取反
// target是目标值
// a表示当前得到的结果
public void dfs(long m_acc,int i,String s,List<String> ans,boolean fan,long target,Stack<String> a,char lastch)
{
long acc = 0;
//第一个运算符出现的位置
int j;
for( j = i + 1;j<=s.length()-1;j++)
{
acc = acc * 10 + (s.charAt(j - 1) - '0');
if(lastch == '*')
{
a.push(acc + "*");
dfs(m_acc * acc,j,s,ans,fan,target,a,'*');//这里的运算符也是乘号
a.pop();
a.push(acc + (fan?"+":"-")); //这里的运算符是加号
dfs(1,j,s,ans,fan,target - m_acc * acc,a,'+');
a.pop();
a.push(acc + (fan ? "-" : "+")); //这里的运算符是减号
dfs(1,j,s,ans,!fan,m_acc * acc - target,a,'-');
a.pop();
}
else
{
a.push(acc + "*");
dfs(acc,j,s,ans,fan,target,a,'*');//这里的运算符是乘号
a.pop();
a.push(acc + (fan?"+":"-")); //这里的运算符是加号
dfs(1,j,s,ans,fan,target - acc,a,'+');
a.pop();
a.push(acc + (fan?"-":"+")); //这里的运算符是减号
dfs(1,j,s,ans,!fan,acc - target,a,'-');
a.pop();
}
if(j == i + 1 && s.charAt(j - 1) == '0')
return;
}
acc = acc * 10 + (s.charAt(j - 1) - '0');
if((lastch == '*' && m_acc * acc == target) || (lastch!='*' && acc == target))
{
StringBuilder q = new StringBuilder();
for(int k = 0;k<a.size();k++)
q.append(a.get(k));
q.append(acc);
ans.add(q.toString());
}
}
public List<String> addOperators(String num, int target) {
List<String> ans = new ArrayList<>();
Stack<String> a = new Stack<>();
dfs(1,0,num,ans,true,target,a,'+');
return ans;
}
public static void main(String[] args)
{
List<String> ans = new Solution().addOperators("105",5);
for(String s:ans)
{
System.out.println(s);
}
}
}
优化-考虑不用上述stack转而用index记录最后一个值(官方)
class Solution {
int n;
String num;
int target;
List<String> ans;
public List<String> addOperators(String num, int target) {
this.n = num.length();
this.num = num;
this.target = target;
this.ans = new ArrayList<String>();
StringBuffer expr = new StringBuffer();
backtrack(expr, 0, 0, 0);
return ans;
}
public void backtrack(StringBuffer expr, int i, long res, long mul) {
if (i == n) {
if (res == target) {
ans.add(expr.toString());
}
return;
}
int signIndex = expr.length();
if (i > 0) {
expr.append(0); // 占位,下面填充符号
}
long val = 0;
// 枚举截取的数字长度(取多少位),注意数字可以是单个 0 但不能有前导零
for (int j = i; j < n && (j == i || num.charAt(i) != '0'); ++j) {
val = val * 10 + num.charAt(j) - '0';
expr.append(num.charAt(j));
if (i == 0) { // 表达式开头不能添加符号
backtrack(expr, j + 1, val, val);
} else { // 枚举符号
expr.setCharAt(signIndex, '+');
backtrack(expr, j + 1, res + val, val);
expr.setCharAt(signIndex, '-');
backtrack(expr, j + 1, res - val, -val);
expr.setCharAt(signIndex, '*');
backtrack(expr, j + 1, res - mul + mul * val, mul * val);
}
}
expr.setLength(signIndex);
}
}
周赛T4-数组的最大与和(HARD)
状态压缩DP(代码冗长 不太好)
- 最优子结构显然; d p [ i ] [ j ] dp[i][j] dp[i][j]表示直到第i个元素,状态为j获得的最大与和
- 状态j的定义:高numslots比特记录是否有第二个物品在对应箱子,低位比特记录是否有第一个物品在对应箱子
- 预先计算在有i+1个元素时,所有状态j的可能值
class Solution {
public int maximumANDSum(int[] nums, int numSlots) {
int[][]dp = new int[nums.length][1 << (2 * numSlots)];
List<Integer>[] a = new List[2 * numSlots + 1];
for(int i = 0;i<a.length;i++)
a[i] = new ArrayList<Integer>(); //已经放了多少个数
for(int i = 0;i<(1 << (2 * numSlots));i++)
{
boolean flag = true;
int count_1 = 0;
for(int j = 0;j<numSlots;j++)
{
if (((i >> j) & 1) == 0 && (((i >> (j + numSlots)) & 1 )!= 0))
{
flag = false;
break;
}
else
{
count_1 += (((i >> j) & 1) + ((i >> (j + numSlots)) & 1 ));
}
}
if(flag)
{
a[count_1].add(i);
}
}
int ans = Integer.MIN_VALUE;
for(int i : a[1])
{
for(int k = 0;k<numSlots;k++)
{
if(((i >> k) & 1) == 1)
dp[0][i] = Math.max(dp[0][i],dp[0][i] & (k + 1));
}
dp[0][i] = nums[0] & 1;
}
for(int i = 1;i<nums.length;i++)
{
for(int j : a[i + 1]) //尝试在桶子中除去一个
{
int v = Integer.MIN_VALUE;
for(int k = 0;k<numSlots;k++)
{
if(((j >> (k + numSlots) )& 1) == 1)
v = Math.max(v, (nums[i] & (k+1))+ dp[i-1][j - (1 << (k + numSlots))]);
else if(((j >> k) & 1) == 1)
v = Math.max(v,(nums[i] & (k+1)) + dp[i-1][j - (1 << k)]);
}
dp[i][j] = v;
}
}
for(int j : a[nums.length])
ans = Math.max(ans,dp[nums.length - 1][j]);
return ans;
}
}
代码优化
- 注意使用2bits位可以将其看作2n个篮子。只需让相邻的篮子编号一致即可。从低到高第j位位子为1则篮子编号 j / 2 + 1 j/2 + 1 j/2+1
- 省略了状态记录
三进制压缩
- 利用三进制数字代替二进制压缩。从低到高代表对应编号篮子中有几个。0,1,2三种状态
- f [ i , m a s k ] f[i,mask] f[i,mask]表示处理到第i个,状态mask的最优解。
class Solution:
def maximumANDSum(self, nums: List[int], numSlots: int) -> int:
@lru_cache(None)
def f(i, mask):
if i < 0:
return 0
t, w, res = mask, 1, 0
for k in range(1, numSlots + 1):
if t % 3:
res = max(res, f(i-1, mask-w) + (k & nums[i]))
t, w = t // 3, w * 3
return res
return f(len(nums) - 1, 3**numSlots-1)
匹配与最大流
- 将num[i]分配到第j个篮子 有点像匹配问题
- 最小费用最大流思路