Leetcode - 周赛443
目录
- 一、3502. 到达每个位置的最小费用
- 二、3503. 子字符串连接后的最长回文串 I
- 三、3504. 子字符串连接后的最长回文串 II
- 四、3505. 使 K 个子数组内元素相等的最少操作数
一、3502. 到达每个位置的最小费用
题目链接
本题是一道脑筋急转弯,实际就是计算前缀最小值,画个图理解一下:
代码如下:
class Solution {
public int[] minCosts(int[] cost) {
int n = cost.length;
int[] ans = new int[n];
ans[0] = cost[0];
for(int i = 1; i < n; i++){
ans[i] = Math.min(ans[i-1], cost[i]);
}
return ans;
}
}
二、3503. 子字符串连接后的最长回文串 I
题目链接
本题数据范围较小,直接暴力,代码如下:
class Solution {
public int longestPalindrome(String s, String t) {
int n = s.length();
int m = t.length();
int ans = 1;
for(int i = 0; i < n; i++){
for(int j = i; j < n; j++){
if(check(s.substring(i,j+1))){
ans = Math.max(ans, j-i+1);
}
for(int x = 0; x < m; x++){
for(int y = x; y < m; y++){
if(check(s.substring(i,j+1) + t.substring(x,y+1))){
ans = Math.max(ans, j-i+1+y-x+1);
}
if(check(t.substring(x,y+1))){
ans = Math.max(ans, y-x+1);
}
}
}
}
}
return ans;
}
boolean check(String s){
int l = 0, r = s.length() - 1;
while(l < r){
if(s.charAt(l) != s.charAt(r))
return false;
l++;
r--;
}
return true;
}
}
三、3504. 子字符串连接后的最长回文串 II
题目链接
本题要构造一个最长的回文串,可以将它分成三个部分 —— ABA,它有两种情况:
- 情况一:
s = "???AB??", t = "???A??"
,A1 与 A2 互相回文,B 自身是一个回文串 - 情况二:
s = "???A???", t = "??BA??"
,A1 与 A2 互相回文,B 自身是一个回文串 - 注:A,B 长度都可以为 0
举一个例子 s = "???abcac??", t = "???ba??"
,这里 A1 = “ab”,B = “cac”,A2 = “ba”,可以将它拆分成两个部分:
- A1 与 A2,这里换一个视角,将 A2 翻转过来,A1 就等于 A2,所以可以将
字符串t
反转一下,这部分实际上就变成了一个求s 与 resT
的最长公共子串问题,可以使用 dp 来解决。- 定义 f [ i ] [ j ] f[i][j] f[i][j]:以 i i i 结尾的字符串 s s s 与 以 j j j 结尾的字符串 r e s T resT resT 的最长公共子串
- s [ i ] s[i] s[i] != r e s T [ j ] resT[j] resT[j], f [ i ] [ j ] = 0 f[i][j] = 0 f[i][j]=0
- s [ i ] s[i] s[i] == r e s T [ j ] resT[j] resT[j], f [ i ] [ j ] = f [ i − 1 ] [ j − 1 ] + 1 f[i][j] = f[i-1][j-1]+1 f[i][j]=f[i−1][j−1]+1
- B 这部分可以使用中心扩展法来枚举,由于回文串的长度可奇可偶,对于一个长度为 n n n 的字符串,需要枚举的中心点就有 n + ( n − 1 ) n + (n - 1) n+(n−1) 个(奇数中心有 n n n 个,偶数中心有 n − 1 n-1 n−1 个),可以从 0 枚举到 2 ∗ n − 2 2 * n - 2 2∗n−2,此时可以直接得到 l = i / 2 , r = ( i + 1 ) / 2 l=i/2, r=(i+1)/2 l=i/2,r=(i+1)/2,然后向两边扩展就行。
- 此时的答案等于: r − l + 1 + 2 ∗ m a x ( f [ l − 1 ] ) r-l+1+2*max(f[l-1]) r−l+1+2∗max(f[l−1])
代码如下:
class Solution {
int dfs(String S, String T){
char[] s = S.toCharArray();
char[] t = T.toCharArray();
int n = s.length, m = t.length;
int[][] f = new int[n+1][m+1];
int[] mx = new int[n+1]; // 统计以 i-1 结尾的字符串S 与 字符串T 的最长公共子串长度
int ans = 0;
for(int i = 0; i < n; i++){
for(int j = 0; j < m; j++){
if(s[i] == t[j]){
f[i+1][j+1] = f[i][j] + 1;
mx[i+1] = Math.max(f[i+1][j+1], mx[i+1]);
}
}
ans = Math.max(ans, 2 * mx[i+1]); // B 为 0 的情况
}
for(int i = 0; i < 2 * n - 1; i++){ // 中心扩展
int l = i / 2;
int r = (i + 1) / 2;
while(l >= 0 && r < n && s[l] == s[r]){
l--;
r++;
}
if(l+1 <= r-1) // 判断 s[l+1, r-1] 不为空
ans = Math.max(ans, r - l - 1 + 2 * mx[l+1]);
}
return ans;
}
public int longestPalindrome(String s, String t) {
String resT = new StringBuilder(t).reverse().toString();
// 分别计算 AB与A,A与BA 两种情况
return Math.max(dfs(s, resT), dfs(resT, s));
}
}
四、3505. 使 K 个子数组内元素相等的最少操作数
题目链接
本题是一道综合题:
- 长度为
x
x
x 的子数组,说明要使用
滑动窗口
- 求子数组中所有元素相等的最小操作次数,
肯定是变成中位数操作次数最小,求中位数
- 求包含
k
k
k 个长度 恰好 为 x 的不重叠子数组(每个子数组中的所有元素都相等)所需要的最少操作数,这是
dp
先来解决如何计算滑窗中位数,使用对顶堆
解决,准备两个堆,一个大根堆 left
,一个小根堆right
:
- 由特殊到一般,如果给定一个无序数组,如何求它的中位数(不排序做法)
- 中位数只会出现在有序数组的中间位置,所以可以使用
left
来统计数组的前半段,right
来统计数组的后半段,保证left.size() >= right.size()
,此时中位数一定就是left的堆顶元素(1/2个)
。明确了大概的方向,接下来就是如何动态维护元素入堆 - 对于元素
x
,如何判断它是进入left
还是right
,分情况讨论:- 如果
left.size() <= right.size()
,那么一定要把x
放入left
,但是这里会出现一个问题,x 可能比rgiht.peek()还要大,如果直接把 x 放入left,那么有序性就被打破
,所以最好的做法是先将 x 放入right
,然后把right.poll() 放入 left 中
- 如果
left.size() > right.size()
,那么一定要把x
放入right
,但是这里也会出现一个问题,x 可能比left.peek()还要小,如果直接把 x 放入right,那么有序性就被打破
,所以最好的做法是先将x放入left
,然后把left.poll() 放入 right 中
- 如果
- 中位数只会出现在有序数组的中间位置,所以可以使用
- 上述做法已经把入堆的操作讲完了,接下来就是在滑窗过程中如何出元素,假设该元素为
out
- 先判断
out
在那个堆当中 - 如果
out <= left.peek()
,说明它在left
,需要将它从left
移除。但是仅仅这样还不行,它还会出现一个问题如果移除之后 left.size() < rgiht.size()
,那么就不符合我们上述的定义了,还需要将right.poll() 放入 left
- 如果
out > left.peek()
,说明它在right
,需要将它从right
移除。同理这样也不行,它会出现一个问题如果移除之后 left.size() > rgiht.size() + 1
,那么也不符合我们上述的定义,需要将left.poll() 放入 right
- 最后,
left的堆顶元素(1/2个)
就是该滑窗的中位数
- 先判断
滑窗中位数有了,接下来就是计算——将窗口中元素全部变为中位数所需的操作次数,画个图好理解:
得到
r
e
s
res
res 数组(res[i]:将 [i,i+k-1] 所有数变成中位数的操作次数)之后,剩下的就是枚举选哪
k
k
k 个不重叠的子数组,直接使用 dp 来做,定义 f[i][j]:在前 j 个数中,选择 i 个长度为 x 的不重叠子数组所需的最小操作次数
- 对于
f[i][j]
,即以下标为j-1
结尾的子数组有选或不选两种情况: - 不选,它直接从
f[i][j-1]
转移过来,f[i][j] = f[i][j-1]
- 选,题目要求它不能重叠,所以选的子数组下标是
[j-k,j-1]
,需要从f[i-1][j-k](这里j-k表示的是前 j-k 个数,也就是 [0,j-k-1] 选 i-1 个数的最小操作次数)
转移过来,f[i][j] = f[i-1][j-k] + res[j-k]
代码如下:
class Solution {
public long minOperations(int[] nums, int x, int k) {
long[] res = medianSlidingWindow(nums, x);
//[i, i + x - 1]
int n = nums.length;
long[][] f = new long[k+1][n+1];
// f[i][j] = min(f[i][j-1], f[i-1][j-x] + res[j-x])
for(int i = 1; i <= k; i++){
f[i][i*x-1] = Long.MAX_VALUE;
for(int j = i * x; j <= n - (k - i) * x; j++){
f[i][j] = Math.min(f[i][j-1], f[i-1][j-x] + res[j-x]);
}
}
return f[k][n];
}
public long[] medianSlidingWindow(int[] nums, int k) {
int n = nums.length;
long[] ans = new long[n-k+1];
LazyHeap left = new LazyHeap((x, y) -> Integer.compare(y, x));
LazyHeap right = new LazyHeap((x, y) -> Integer.compare(x, y));
for(int l = 0, r = 0; r < n; r++){
int in = nums[r];
// 入堆操作
if(left.size() == right.size()){
left.push(right.pushPop(in));
}else{
right.push(left.pushPop(in));
}
if(r < k - 1) continue;
int x = left.top();
// 计算变成中位数所需操作次数
ans[l] = (long) x * (k % 2) + right.sum() - left.sum();
int out = nums[l];
// 出堆操作
if(out <= left.peek()){
left.remove(out);
if(left.size() < right.size()){
left.push(right.pop());
}
}else{
right.remove(out);
if(left.size() > right.size() + 1){
right.push(left.pop());
}
}
l++;
}
return ans;
}
}
// 手写的懒删除堆(否则会超时)
class LazyHeap extends PriorityQueue<Integer> {
//统计当前每个元素需要删除次数
private final Map<Integer, Integer> removeCnt = new HashMap<>();
//实际堆的大小
private int size = 0;
private long sum = 0;
public LazyHeap(Comparator<Integer> comparator){
super(comparator);
}
public int size(){
return size;
}
public long sum(){
return sum;
}
//懒删除操作
public void remove(int x){
removeCnt.merge(x, 1, Integer::sum);
size--;
sum -= x;
}
//实际执行删除操作
private void applyRemove(){
while(removeCnt.getOrDefault(peek(), 0) > 0){
removeCnt.merge(poll(), -1, Integer::sum);
}
}
//查看推顶元素
public int top(){
applyRemove();
return peek();
}
//出堆
public int pop(){
applyRemove();
size--;
sum -= peek();
return poll();
}
//入堆
public void push(int x){
int c = removeCnt.getOrDefault(x, 0);
if(c > 0){
removeCnt.put(x, c - 1);
}else{
offer(x);
}
sum += x;
size++;
}
//push(x), pop()
public int pushPop(int x){
applyRemove();
sum += x;
offer(x);
sum -= peek();
return poll();
}
}