杂题部分

杂题部分

1.3131. 找出与数组相加的整数 I

思路:快排+直接找差值

void quickSort(int *arr,int low,int high)
{
if(high<=low) return;
int i=low;
int j=high;
int pivot=arr[low];
while(1)
{
// 1. 自左到右搜索,直到比pivot大后停止。
while(arr[i]<=pivot)
{
i++;
if(i==high) break;
}
// 2. 自右到左搜索,直到比pivot小后停止。
while(arr[j]>=pivot)
{
j--;
if(j==low) break;
}
// 3. 两个指针相遇时或跨越后,停止。
if(i>=j) break;
// 4.交换两个指针的值。
int temp=arr[i];
arr[i]=arr[j];
arr[j]=temp;
}
// 交换pivot与右指针的值
arr[low]=arr[j];
arr[j]=pivot;
// 分段进行快排
quickSort(arr,low,j-1);
quickSort(arr,j+1,high);
}
int addedInteger(int* nums1, int nums1Size, int* nums2, int nums2Size) {
quickSort(nums1,0,nums1Size-1);
quickSort(nums2,0,nums2Size-1);
return nums2[0]-nums1[0];
}

2.3132. 找出与数组相加的整数 II

解法如下(3132)

void quickSort(int *arr,int low,int high)
{
if(high<=low) return;
int i=low;
int j=high;
int pivot=arr[low];
while(1)
{
// 1. 自左到右搜索,直到比pivot大后停止。
while(arr[i]<=pivot)
{
i++;
if(i==high) break;
}
// 2. 自右到左搜索,直到比pivot小后停止。
while(arr[j]>=pivot)
{
j--;
if(j==low) break;
}
// 3. 两个指针相遇时或跨越后,停止。
if(i>=j) break;
// 4.交换两个指针的值。
int temp=arr[i];
arr[i]=arr[j];
arr[j]=temp;
}
// 交换pivot与右指针的值
arr[low]=arr[j];
arr[j]=pivot;
// 分段进行快排
quickSort(arr,low,j-1);
quickSort(arr,j+1,high);
}
int minimumAddedInteger(int* nums1, int nums1Size, int* nums2, int nums2Size) {
quickSort(nums1,0,nums1Size-1);
quickSort(nums2,0,nums2Size-1);
for(int i=2;i>=0;i--)
{
int left=i+1,right=1;
while(left<nums1Size && right < nums2Size)
{
if(nums1[left]-nums2[right]==nums1[i]-nums2[0])
right++;
left++;
}
if(right==nums2Size)
return nums2[0]-nums1[i];
}
return 0;
}

3.3148. 矩阵中的最大得分

思路:两种方法

方法一:动态规划+前缀和

思路与算法

f(i,j) 表示以矩阵 grid 中位置 (i,j) 结束的最大得分。由于每一步只能往右走或者往下走,因此在进行状态转移时,我们分别枚举这两种情况:

对于往右走,我们有:

dp(i,j)=max0k<j{max{dp(i,j),0}+grid(i,j)grid(i,k)}

这里的 max{f(i,k),0} 是由于题目要求「必须至少移动一次」,所以 f(i,k) 中并不包含「以 (i,k) 同时为起点和终点」的情况,需要增加 0 来表示这一情况,才能让状态转移中包含「以 (i,k) 为起点,(i,j) 为终点」的情况。

就算不考虑往右走的情况,上述状态转移方程的时间复杂度也达到了 O(mn)×O(n)=O(mn2),会超出时间限制,因此需要进行优化。

注意到 grid(i,j)k 无关,我们可以将其从 max 中提出:

dp(i,j)=grid(i,j)+max0k<j{max{dp(i,j),0}grid(i,k)}

prerow(i,j)=max0k<j{max{dp(i,j),0}grid(i,k)},那么可以使用前缀和的思想,在计算完 f(i,j) 后用 O(1) 的时间得到它的值:

{prerow(i,0)=max{dp(i,0),0}grid(i,0)prerow(i,j)=max{prerow(i,j1),max{dp(i,j),0}grid(i,j)}

此时状态转移方程变为简单的递推:

dp(i,j)=grid(i,j)+prerow(i,j1)

同理,对于往左走,我们有:

dp(i,j)=grid(i,j)+precol(i1,j)

这里 precol(i,j) 是与 prerow(i,j) 类似的列的前缀最大值。当所有的 dp(i,j) 都计算完成后,其中的最大值即为最终的答案。

题解

int maxScore(int** grid, int gridSize, int* gridColSize) {
// dp 2-dimensional matrix.
int **dp=(int**)malloc(sizeof(int*)*gridSize);
int **preRow=(int**)malloc(sizeof(int*)*gridSize);
int **preCol=(int**)malloc(sizeof(int*)*gridSize);
for(int i=0;i<gridSize;i++)
{
dp[i]=(int*)malloc(sizeof(int)*gridColSize[i]);
preRow[i]=(int*)malloc(sizeof(int)*gridColSize[i]);
preCol[i]=(int*)malloc(sizeof(int)*gridColSize[i]);
for(int j=0;j<gridColSize[i];j++)
{
dp[i][j]=-1e8;
preRow[i][j]=0;
preCol[i][j]=0;
}
}
int ans=-1e8;
for(int i=0;i<gridSize;i++)
{
for(int j=0;j<gridColSize[i];j++)
{
if(i>0)
dp[i][j]=dp[i][j]>grid[i][j]+preCol[i-1][j]?dp[i][j]:preCol[i-1][j]+grid[i][j];
if(j>0)
dp[i][j]=dp[i][j]>grid[i][j]+preRow[i][j-1]?dp[i][j]:grid[i][j]+preRow[i][j-1];
ans=ans>dp[i][j]?ans:dp[i][j];
preRow[i][j]=preCol[i][j]=(dp[i][j]>0?dp[i][j]:0)-grid[i][j];
if(i>0)
preCol[i][j]=preCol[i][j]>preCol[i-1][j]?preCol[i][j]:preCol[i-1][j];
if(j>0)
preRow[i][j]=preRow[i][j]>preRow[i][j-1]?preRow[i][j]:preRow[i][j-1];
}
}
return ans;
}

4.2552.统计四元上升数组

思路

alt

解法

class Solution {
public:
long long countQuadruplets(vector<int>& nums) {
int n=nums.size();
vector<int> pre(n+1);
long long ans=0,cur=0;
for(int j=0;j<n;j++)
{
cur=0;
for(int k=n-1;k>j;k--) // 从后往前进行枚举
{
if(nums[j]>nums[k])
ans += (long long)(pre[nums[k]])*cur;
else
cur++;
}
for (int x=nums[j]+1;x<=n;x++)
pre[x]++; // 统计从0到j-1的小于x的数
}
return ans;
}
};

5.2306. 公司命名

思路(Hash set)

将每一个字符以首字母为key,去掉首字母为后缀存储在hashset中。这样我们就有
a: str1,str2,str3...
b: str1,str2,str3...
然后我们观察是否有重复的后缀。有则去掉。这样就有了
ans+=(a-repeat)*(b-repeat)
作为最终结果。

Code

class Solution {
public:
int get_size(unordered_set<string> &a, unordered_set<string> &b)
{
// Count for the intersected suffix.
int ans=0;
for(const string &s:a)
{
if(b.count(s))
ans++;
}
return ans;
}
long long distinctNames(vector<string>& ideas) {
unordered_map<char, unordered_set<string> > names; // Hash set put all the names with the same first letter together.
for (string &idea:ideas)
{
names[idea[0]].insert(idea.substr(1,idea.size()-1));
}
long long ans=0;
// Check for intersects and count.
for(auto [key1,val1]:names)
{
for(auto [key2,val2]:names)
{
if(key1==key2) continue;
int intersections=get_size(val1,val2);
ans+=static_cast<long long> (val1.size()-intersections)*(val2.size()-intersections);
}
}
return ans;
}
};

6.交替组II

思路:组合数学

统计交替的数字出现的次数。当交替数字出现的频数>=k时,说明出现了一个符合交替的数组。否则,重新将cnt设为1.
注意因为是循环数组,因此我们需要前进到i=n+k-2.

class Solution {
public:
int numberOfAlternatingGroups(vector<int>& colors, int k) {
int cnt = 1;
int n = colors.size();
int ans = 0;
for(int i = 1; i < n + k - 1; i ++)
{
if(colors[i % n] != colors[(i-1) % n])
cnt ++;
else
cnt = 1;
if(cnt >= k)
ans ++;
}
return ans;
}
};

7.3251. 单调数组对的数目II

方法:前缀和 + 二维动态规划

主问题:从下标0到n-1,求取满足arr1单调递增且arr2单调递减(非严格)且arr1[i]+arr2[i]=nums[i]的数组的数目。
子问题:从下标0到下标i-1,满足上述条件数组的数目。
维度扩展:计算arr1[i]=j时满足条件的数组数目dp[i][j].
这样,我们需要满足条件就是:arr1[i-1]<=j,并且nums[i-1] - arr1[i-1] >= nums[i]-j。 这样我们就有:

arr[i1]jarr[i1]j+nums[i1]nums[i]

我们可以写成紧凑形式:arr[i-1]<=j+min(nums[i-1]-nums[i],0)

这样我们就可以累加得到

k=j+min(nums[i1]nums[i],0)dp[i][j]={0 if k<0m=0kdp[i1][m], if k0

采用前缀和维护,也就是sum[j]=m=0jdp[i1][m], 这样我们可以得到最后的结果为:ans=m=0nums[n1]dp[n1][m]

时间复杂度:因为需要计算nums的大小的行和对对应的nums[i]进行迭代,因此为O(nums.size()×max(nums))

空间复杂度:如上。

class Solution {
public:
int countOfPairs(vector<int>& nums) {
int n = *(max_element(nums.begin(),nums.end()))+1;
int m = nums.size()+1;
const int MOD = 1e9+7;
// 0. The dynamic arrays.
vector<vector<long long> > dp(m,vector<long long >(n,0));
vector<long long> sum(n,0);
// 1. Initial value.
for(int i=0;i <= nums[0]; i++)
{
dp[0][i] = 1;
}
partial_sum(dp[0].begin(),dp[0].end(),sum.begin());
// 2. dp.
for(int i = 1; i < m-1; i++)
{
for(int j = 0; j <= nums[i]; j++)
{
int flag = j + min(nums[i-1]-nums[i],0);
dp[i][j] = flag < 0 ? 0 : sum[flag] % MOD;
}
partial_sum(dp[i].begin(),dp[i].end(),sum.begin());
}
// 3. final answer:
long long ans = 0;
for(int i = 0; i <= nums[m-2]; i++)
ans = (ans + dp[m-2][i]) % MOD;
return ans;
}
};

8.[3282. 到達數組末尾的最大得分]

思路:遍歷得到最大的數字就進行跳躍

class Solution {
public:
long long findMaximumScore(vector<int>& nums) {
long long ans = 0;
int mx = 0;
for (int i = 0; i + 1 < nums.size(); i++) {
mx = max(mx, nums[i]);
ans += mx;
}
return ans;
}
};

9.51. N皇后

思路:回溯+记录对角线元素2n+1

题解9

class Solution {
public:
void dfs(vector<vector<string> > &ans, vector<int> &queens, vector<int> &cols, vector<int> &diag1, vector<int> &diag2, int r, int n)
{
// 1. 递归终点:到达最后一行。根据记录下来的第i行所在的第几列编写答案。
if (r==n)
{
vector<string> temp;
for(auto i:queens)
{
string s = string(i, '.')+'Q'+string((n-1-i),'.');
temp.push_back(s);
}
ans.push_back(temp);
return;
}
// 2. 递归部分。对每一行进行递归。
for(int i = 0; i < cols.size(); i++)
{
if (!cols[i] && !diag1[r+i] && !diag2[r-i+n-1])
{
// 没有违反情况的地方放置。
queens[r]=i;
cols[i]=1;
diag1[r+i]=1;
diag2[r-i+n-1]=1;
// 进入下一步递归。
dfs(ans,queens,cols,diag1,diag2,r+1,n); // 发现没有情况满足时回溯。
// 抹去曾经放置的。
cols[i]=0;
diag1[r+i]=0;
diag2[r-i+n-1]=0;
}
}
}
vector<vector<string>> solveNQueens(int n) {
vector<vector<string> > ans;
vector<int> queens(n,0); // 皇后放置在第i行,第queens[i]列上。
/**
* 主对角线上行数-列数不变,总共有2n-1个对角线。也就是r-c+n-1.
* 副对角线上列数+行数不变,有2n-1个对角线。也就是r+c。
*/
vector<int> cols(n,0), diag1(2*n-1,0), diag2(2*n-1,0);
dfs(ans,queens,cols,diag1,diag2,0,n);
return ans;
}
};

10.2056. 棋盘上有效移动组合的数目

题解10

struct pos
{
int x,y; // 起点。
int dx,dy; // 移动的方向。
int step; // 移动
};
// 确定移动方向。
vector<pair<int,int> > directions = {{-1,0},{1,0},{0,-1},{0,1},{1,1},{-1,1},{-1,-1},{1,-1}};
// 棋子类型。
string rook = "rook";
string bishop = "bishop";
string queen = "queen";
// 棋盘大小。
const int SIZE = 8;
class Solution {
public:
// 计算位于(x,y)的棋子在可以移动方向上的合法移动到的位置。
vector<pos> generate_pos(int x0, int y0, vector<pair<int,int> > &directions)
{
vector<pos> positions{{x0,y0,0,0,0}};
for(auto [dx, dy]:directions)
{
int x = x0+dx;
int y = y0+dy;
for(int step = 1; x > 0 && x <= SIZE && y > 0 && y <= SIZE; step++)
{
positions.emplace_back(x0,y0,dx,dy,step);
x += dx;
y += dy;
}
}
return positions;
}
// 判断合法性。
bool is_valid(pos &p1, pos &p2)
{
int x1 = p1.x;
int y1 = p1.y;
int x2 = p2.x;
int y2 = p2.y;
for(int i=0; i < max(p1.step, p2.step); i++)
{
// 一起各走一步。
if(i < p1.step) x1 += p1.dx, y1 += p1.dy;
if(i < p2.step) x2 += p2.dx, y2 += p2.dy;
if(x1 == x2 && y1 == y2)
return false;
}
return true;
}
void dfs(int &ans, int i, int n, vector<vector<pos> > &all_legal_moves, vector<pos> &path)
{
if(i == n)
{
ans ++;
return;
}
for(auto &pos1: all_legal_moves[i])
{
bool flag = true;
for(int j = 0; j < i; j++)
{
if(!is_valid(pos1, path[j]))
{
flag = false;
break;
}
}
if(flag)
{
path[i] = pos1;
dfs(ans, i+1, n, all_legal_moves, path);
}
}
}
int countCombinations(vector<string>& pieces, vector<vector<int>>& positions) {
// 具体到棋子。
vector<pair<int,int> >queen_directions = directions;
vector<pair<int,int> > rook_directions;
vector<pair<int,int> > bishop_directions;
for(int i = 0; i < 4; i++)
{
rook_directions.push_back(directions[i]);
bishop_directions.push_back(directions[i+4]);
}
int n = pieces.size();
// initialize legal movement。
vector<vector<pos> > all_legal_moves(n);
for(int i = 0; i < n; i++)
{
string type = pieces[i];
if(type == rook)
all_legal_moves[i] = generate_pos(positions[i][0], positions[i][1], rook_directions);
else if(type == queen)
all_legal_moves[i] = generate_pos(positions[i][0], positions[i][1], queen_directions);
else if(type == bishop)
all_legal_moves[i] = generate_pos(positions[i][0], positions[i][1], bishop_directions);
}
vector<pos> path(n);
int ans = 0;
dfs(ans, 0, n, all_legal_moves, path);
return ans;
}
};

11.767. 重构字符串

思路:贪心构造

设一个字符串的长度为 n。则最多的字母次数为 n+12 次。因此首先便利字符串,统计字母出现个数,接着再利用最大堆或者计数的方法进行排布。

题解10.1: 基于最大堆的贪心

每次取出最大堆中的出现次数最多的两个字母进行重构。剩下一个字母时直接拼接即可。
这里搓了一个自己喜欢的最大堆。

img

template<class T>
class heap {
/**
* @brief The left child is 2*i+1 and the right child is 2*(i+1). Heap stored in the vector.
*/
private:
vector<T> container;
vector<int> counter;
int _size; // size for currently how many of them were inside.
int _capacity; // The container's size;
function<bool(const T&, const T&, const vector<int>&)> comp;
void swap(int id1, int id2)
{
T temp = container[id1];
container[id1] = container[id2];
container[id2] = temp;
}
void heap_sort()
{
for(int i = 1; i < _size; i++)
{
int index = i;
int parent_index = index % 2 == 0 ? index / 2 - 1 : index / 2;
adjust_bottom_up(index, parent_index);
}
}
// Adjust heap. Compared with the parent. When true, up.
void adjust_bottom_up(int index, int parent_index)
{
// 子节点对母节点偏序关系成立,交换。
while(index > 0 && comp(container[index], container[parent_index], counter))
{
swap(parent_index, index);
index = parent_index;
parent_index = index % 2 == 0 ? index / 2 - 1 : index / 2;
}
}
// for heap top to bottom.
void adjust_top_bottom(int index)
{
while(index < _size)
{
int left_child = index * 2 + 1, right_child = (index+1) * 2;
if(left_child < _size && comp(container[left_child], container[index], counter))
{
if(right_child < _size && comp(container[right_child], container[index], counter)
&& comp(container[right_child], container[left_child], counter))
{
swap(right_child,index);
index = right_child;
}
else
{
swap(left_child,index);
index = left_child;
}
}
else if(right_child < _size && comp(container[right_child], container[index], counter))
{
swap(right_child,index);
index = right_child;
}
else
break;
}
}
public:
friend class Solution;
heap(function<bool(const T&, const T&, const vector<int>&)> &cmp)
{
container = vector<T>();
counter = vector<T>();
_size = 0;
_capacity = 0;
comp = cmp;
}
heap(vector<T> &data, vector<int> &cnt, bool(*cmp)(const T&, const T&, const vector<int>&))
{
if(data.size()!=cnt.size())
throw "Error! Size not match!\n";
comp = *cmp;
container = data;
counter = cnt;
_size = data.size();
_capacity = _size;
heap_sort();
}
heap(vector<int> &cnt, bool(*cmp)(const T&, const T&, const vector<int>&))
{
comp = *cmp;
container = vector<T>();
counter = vector<int>(cnt);
_size = 0;
_capacity = 0;
heap_sort();
}
T top() const
{
return container[0];
}
void pop()
{
container[0] = container[_size - 1];
_size--;
adjust_top_bottom(0);
}
void print()
{
for(int i = 0; i < _size; i++)
{
cout << "element: " << container[i] << " count: " << counter[container[i] - 'a'];
cout << '\n';
}
}
void push(T element)
{
_size++;
if(_size > container.size())
container.push_back(element);
else
container[_size-1] = element;
int index = _size-1;
int parent_index = index % 2 == 0 ? index / 2 - 1 : index / 2;
adjust_bottom_up(index, parent_index);
}
int size()
{
return _size;
}
int &operator[](int index)
{
return counter[index];
}
bool empty()
{
return _size == 0;
}
};
class Solution {
public:
static bool compare(const char &a, const char &b, const vector<int> &count)
{
return count[a-'a'] > count[b-'a'];
}
bool (*comp)(const char &, const char &, const vector<int> &) = compare;
string reorganizeString(string s)
{
string ans = "";
vector<int> count(26,0);
int maxcount = 0;
for(auto ch: s)
{
count[ch-'a']++;
maxcount = max(maxcount, count[ch-'a']);
}
if(maxcount > (s.length() + 1)/2)
return ans;
heap h(count,comp);
for(int i = 0; i < 26; i++)
{
if(count[i])
h.push(char (i+'a'));
}
while(h.size() > 1)
{
char temp1,temp2;
temp1=h.top();
ans.push_back(temp1);
h.pop();
temp2=h.top();
ans.push_back(temp2);
h.pop();
h[temp1-'a']--;
h[temp2-'a']--;
if(h[temp1-'a'])
h.push(temp1);
if(h[temp2-'a'])
h.push(temp2);
}
if(!h.empty())
{
ans.push_back(h.top());
}
return ans;
}
};

12.78.子集I

思路一:迭代法

利用二进制进行枚举,从而可以得到所有的子集。
例如集合 A={1,2,3},我们可以枚举二进制数如下:

二进制 集合 十进制
000 {} 0
001 1
010 2
011 3
100 4
101 5
110 6
111 7

这样通过二进制进行枚举得到子集即可。这里我们通过 & 符号可以快速进行转换。我们让迭代从0到7,并通过&判断每一位上是否具有置位1。

class Solution {
public:
vector<vector<int>> subsets(vector<int>& nums) {
vector<vector<int> > ans;
vector<int> temp;
int n = nums.size();
for(int i=0; i < (1<<n); i++)
{
temp = vector<int>();
// 3个位的bit置位判断。
for(int j=0; j<n; j++)
{
if(i & (1 << j))
temp.push_back(nums[j]);
}
ans.push_back(temp);
}
return ans;
}
};

思路二:递归实现子集枚举

采用dfs,dfs将会判断是否会选择当前数进入。选择了则进入递归,否则不选择。

13.90.子集II

思路一:迭代去重

假设我们有集合 A={1,2,2}。于是我们有:

二进制 集合 十进制
000 {} 0
001 1
010 2
011 3
100 4
101 5
110 6
111 7

我们发现当经过排序后,上个数没有选择并且当前被选择的数字与上一个相同,则需要跳过,不需要选择。因此我们有:

class Solution {
public:
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
vector<int> temp;
vector<vector<int> > ans;
int n = nums.size();
sort(nums.begin(),nums.end());
for(int i = 0; i < (1 << n); i++)
{
temp = vector<int>();
int flag = 1;
for(int j = 0; j < n; j++)
{
// 判断是否有没有置位
if(i & (1 << j))
{
// 出现1-0情况:该位置位,上一位未置位并且相同。
if(j > 0 && !(i & (1 << (j-1))) && (nums[j] == nums[j-1]))
{
flag = 0;
break;
}
else
temp.push_back(nums[j]);
}
}
if(flag)
ans.push_back(temp);
}
return ans;
}
};

1760.袋子里最少数目

寻找最大值最小化:二分

对于每一个袋子,假设最后最大值为 y,原先每个袋子具有 nums[i] 个球,那么每个袋子需要操作次数为:

nums[i]1y

那么这样我们可以从1到 max{nums} 来查找符合条件要求的最小值,采用二分的方式。而对于每个袋子,操作次数总计则为其和,其和不能超过P,而要尽可能地大。当P过小时,调整上界(y)使其变小。否则调整下界。

class Solution {
public:
int minimumSize(vector<int> & nums, int maxOperations)
{
// 采用二分查找的模式。
int left = 1,right = 0;
for(auto i: nums)
right = right > i ? right : i;
long long sumOp = 0;
int ans;
while(left <= right)
{
sumOp = 0;
int optimized = (left + right) / 2;
for(auto i: nums)
sumOp += (i - 1) / optimized;
if(sumOp > maxOperations)
left = optimized + 1;
else
{
right = optimized - 1;
ans = optimized;
}
}
return ans;
}
};

13.2209.用地毯覆盖后最少的白色砖块

通过观察知道,我们需要选择动态规划来解决这样的问题。

动态规划

确认子问题

  • 原问题可以定义成:从 [0,n] 地砖上,铺上j块地毯使得其变得更短。
  • 我们需要从右向左覆盖地砖。因为从左向右覆盖地砖需要我们倒过来进行处理(从字符串最末尾处理到开头),因此我们在第 i 块地砖上铺上地毯,则 [icarpetLen,i] 将都被铺上地毯。
  • 这样的话,地毯数量将会增加1. 问题将会变成:从 [0,ncarpetLen] 块地砖上,铺上j1块地砖使得其变得更短。
  • 这样我们就需要关注两个指标:
    • i: 从 [0,i] 上字符串(地板)的情况。
    • j: 采用了多少块毯子。

确定转移方程

  • 我们根据上面的讨论得到,我们可以选择在第i块地砖铺上毯子与否。

    • 铺上毯子,则j变为j+1,并且地面上 icarpetLeni全部被覆盖。因此将dp视为最小的白色瓷砖数,则就有转移方程:
      • dp[i][j]dp[icarpetLen][j1]
    • 不铺上毯子,则最小白色瓷砖数将会变成前一个的转移+当前是否为白色瓷砖。
    • 这里将地砖的起始下标设为1.
      • dp[i][j]dp[i1][j]+[floor[i1]==1]
  • 这样总体的转移方程为:

dp[i][j]={min{dp[i1][j]+[floor[i1]==1],dp[icarpetLen][j1]}min{dp[i1][j]+[floor[i1]==1],dp[0][j1]}

class Solution {
public:
int minimumWhiteTiles(string floor, int numCarpets, int carpetLen) {
// Using 2-dimension dynamic programming.
vector<vector<int> > dp(floor.length() + 1, vector<int>(numCarpets + 1, (int)1e5));
// 第0块作为起始,不具有白色瓷砖。
for(int i = 0; i <= numCarpets; i++) {
dp[0][i] = 0;
}
// 不使用任何一块地毯,因此具有初始值为白色瓷块数。
for(int i = 1; i <= floor.length(); i++) {
dp[i][0] = dp[i-1][0] + (floor[i - 1] == '1');
}
for(int i = 1; i <= floor.length(); i++) {
for(int j = 1; j <= numCarpets; j++) {
if(i-carpetLen >= 0)
dp[i][j] = min(dp[i-1][j] + (floor[i - 1] == '1'), dp[i-carpetLen][j-1]);
else
dp[i][j] = min(dp[0][j], dp[i-1][j]+(floor[i - 1] == '1'));
}
}
return dp[floor.length()][numCarpets];
}
};

空间优化

dp[][j] 的关系仅依赖于 dp[:][j1] 和自身(位于小于当前i的元素), 因此我们可以进行空间优化,用两个二维数组分别记录 dp[][j]dp[][j1]
这样我们就有:

class Solution {
public:
int minimumWhiteTiles(string floor, int numCarpets, int carpetLen) {
vector<int> f(floor.length() + 1); // dp[][j] main we use.
vector<int> g(floor.length() + 1); // dp[][j-1] prev states.
g[0] = 0;
// 初始状态:对于每个g[i] = dp[i][0],记录白色瓷砖数。
for(int i = 1; i <= floor.length(); i++) {
g[i] = g[i-1] + (floor[i-1] == '1');
}
for(int j = 1; j <= numCarpets; j++) {
// 对于每个f[0] = dp[0][j],均等于0.
f[0] = 0;
for(int i = 1; i <= floor.length(); i++) {
// g[i-carpetLen] = dp[i-carpetLen][j-1]
// f[i-1] = f[i-1][j]
f[i] = min(g[max(0, i - carpetLen)], f[i - 1] + (floor[i-1] == '1'));
}
// 转移给g上一个状态。
g = f;
}
return f[floor.length()];
}
};

那么,我们是否还可以进行进一步的优化呢?注意到我们的最小值仅取决于已经计算好的j块地毯的值和先前状态的值,我们就可以首先利用先前状态和初始值计算出一个已经计算好的值(从尾部到头部),再从头部到尾部利用计算好的值进行计算。

class Solution {
public:
int minimumWhiteTiles(string floor, int numCarpets, int carpetLen) {
vector<int> f(floor.length() + 1); // dp[][j] main we use.
f[0] = 0;
for(int i = 1; i <= floor.length(); i++) {
f[i] = f[i-1] + (floor[i-1] == '1');
}
for(int j = 1; j <= numCarpets; j++) {
f[0] = 0;
// 现在拥有j-1状态的值,于是从尾部到头部计算。
for(int i = floor.length(); i > 0; i--)
f[i] = min(f[max(0, i-carpetLen)], f[i]);
// 现在拥有了计算好的值,再从头部到尾部计算。
for(int i = 1; i <= floor.length(); i++)
f[i] = min(f[i], f[i-1] + (floor[i-1] == '1'));
}
return f[floor.length()];
}
};

14.2222.选择建筑的方案数

方法一:计算101和010出现的数目

  • 题目中仅有3栋建筑选择。
  • 因此我们只需要考虑交叉的建筑物选择,101或010.
  • 那么只需要得到0左右两边的建筑数目,1左右两边的建筑数目就可以了。
class Solution {
public:
long long numberOfWays(string s) {
using ll = long long;
vector<ll> count0, count1;
ll countFor0 = 0, countFor1 = 0;
for(auto i: s) {
if(i == '1') {
countFor1++;
}
else {
countFor0++;
}
count0.push_back(countFor0);
count1.push_back(countFor1);
}
ll sumOf0 = count0.back(), sumOf1 = count1.back();
ll ans = 0;
for(int i = 0; i < s.length(); i++) {
if(s[i] == '1') {
ans += count0[i] * (sumOf0 - count0[i]);
}
else
ans += count1[i] * (sumOf1 - count1[i]);
}
return ans;
}
};

方法二:空间优化

15.131.分割回文串(2025.03.01)

回溯法

  • 首先,我们需要先了解最基础的处理回文串的方法。

  • 我们可以定义s[0,...,i-1]为一个回文串。接下来我们就需要的是判断s[i,...,j]是不是一个回文字符串即可。

  • 这样,我们可以采用递归的方式进行:

    • 首先从s[0,...,i1],当i1为回文字符串时,进入新的递归,递归起点为i1+1。
    • 接着从s[i1+1,...,i2],继续进行递归。
    • 到达末尾,结束,储存分割结果,并回溯,并继续上述递归的遍历(例如从i1+1 到 i2+1)。
  • 但是这样会导致出现重复判断。

  • 对于字符串aaba,我们有:

    • s[0] -> s[1] -> s[2] -> s[3], 递归第一次到达终点,有a,a,b,a
    • 回溯到s[2,3],不是回文字符串。继续回溯。
    • s[0] -> s[1,2,3], 储存a,aba
    • s[0,1] -> s[2] -> s[3] 有 aa,b,a
    • s[0,1] -> s[2,3] x
    • s[0,1,2] x
    • s[0,1,2,3] x
  • 这里会出现重复进行判断的情况。对于s[2,3]可能有重复判断多次的情况。我们可以使用记忆化搜索或者是预处理来解决这样的问题。

记忆化搜索

我们可以发现,我们可以建立记忆化搜索方法,判断是否为回文字符串后,直接利用结果进行使用即可。例如,针对s="aaba",我们进行记忆化搜索后,可以实现:

0 1 2 3
0 1 1 0 0
1 / 1 0 1
2 / / 1 0
3 / / / 1

预先处理

我们发现我们可以利用我们已经做好的判断,来辅助我们进行的回文字符串的判断。当我们的s[i,...,j]为回文字符串,且s[i-1],s[j+1]均为相同字符时,字符串可以从中间扩展开来。这样我们就有

  • ij,f[i][j]=1
  • f[i+1][j1]=1,s[j]=s[i]f[i][j]=1
class Solution {
public:
vector<vector<string> > partition(string s) {
int n = s.length();
vector<vector<int> > isPalindrome(n, vector<int>(n,0));
// First: pre dealing with the palindromes.
for(int i = 0; i < n; i++) {
for(int j = 0; j <= i; j++) {
isPalindrome[i][j] = 1;
}
}
for(int i = n - 2; i >= 0; i--) {
for(int j = n - 1; j > i; j--) {
if(isPalindrome[i+1][j-1] && s[i]==s[j]) {
isPalindrome[i][j] = 1;
}
}
}
vector<vector<string> > ans;
vector<string> tmp;
// 进入dfs回溯。
auto dfs = [&](this auto && self, int i){
if(i == n) {
ans.push_back(tmp);
return;
}
for(int j = i; j < n; j++) {
if(isPalindrome[i][j]) {
tmp.push_back(s.substr(i,j-i+1));
self(j+1);
tmp.pop_back();
}
}
};
dfs(0);
return ans;
}
};

15.132.分割回文串II(2025.03.02)

预先处理+dp转移

我们还是像上次一样进行预处理后,进行动态规划的转移。我们可以:

  • 枚举从0到i的回文字符串,选择其中分裂最少的值。
  • 当当前的j使得0到j不是回文字符串时,有:
    • dp[j] = min(dp[i]) + 1
    • 其中i+1到j是回文字符串。
  • 当自身为回文字符串时,就不再需要分割了,因此处理为0.
class Solution {
public:
int minCut(string s) {
int n = s.length();
vector<vector<int> > isPalindrome(n, vector<int>(n, 0));
for(int i = 0; i < n; i++) {
for(int j = 0; j <= i; j++) {
isPalindrome[i][j] = 1;
}
}
for(int i = n-2; i >= 0; i--) {
for(int j = i+1; j < n; j++) {
if(isPalindrome[i+1][j-1] && s[i] == s[j])
isPalindrome[i][j] = 1;
}
}
vector<int> ans(n, (int)1e9);
for(int i = 0; i < n; i++) {
// 当还是回文串时,记成0
if(isPalindrome[0][i])
ans[i] = 0;
else {
for(int j = 1; j <= i; j++) {
if(isPalindrome[j][i]) {
ans[i] = min(ans[i], ans[j - 1] + 1);
}
}
}
}
return ans[n-1];
}
};
posted @   木木ちゃん  阅读(2)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
点击右上角即可分享
微信分享提示