1560. 圆形赛道上经过次数最多的扇区
暴力模拟:
时间复杂度: O ( m ∗ n ) O(m*n) O(m∗n)
class Solution {
public:
vector<int> mostVisited(int n, vector<int>& rounds) {
int cnt[101] = {
0};
cnt[rounds[0]-1]++;
for(int i=1;i<rounds.size();i++){
int k = 1;
while((rounds[i-1]+k-1)%n != rounds[i]-1 ){
cnt[(rounds[i-1]+(k++)-1)%n]++;
}
cnt[rounds[i]-1]++;
}
int num = *max_element(cnt,cnt+n);
vector<int> v;
for(int i=0;i<n;i++){
if(cnt[i]==num){
v.push_back(i+1);
}
}
return v;
}
};
思维:忽略中间的过程
O ( n ) O(n) O(n)
class Solution {
public:
vector<int> mostVisited(int n, vector<int>& rounds) {
int s = rounds[0], e = rounds.back();
vector<int> ans;
if(s==e){
ans = {
s};
}else if(s<e){
for(int i=s;i<=e;i++) ans.push_back(i);
}else{
for(int i=1;i<=e;i++) ans.push_back(i);
for(int i=s;i<=n;i++) ans.push_back(i);
}
return ans;
}
};
1561. 你可以获得的最大硬币数目
从小到大排序,每次给第三个人取最小的,给第一第二个人去最大和次大的。
class Solution {
public:
int maxCoins(vector<int>& piles) {
int ans = 0;
sort(piles.begin(),piles.end());
int j = piles.size()-1, i = 0;
while(i<j){
ans += piles[j-1];
j -= 2;
i++;
}
return ans;
}
};
- 方法一
因为是最后一次满足条件操作序号,所以如果倒过来模拟的话就是求第一次满足条件了。
如何模拟区间的分裂?
用开区间 ( l , r ) (l,r) (l,r)表示 [ l − 1 , r − 1 ] [l-1,r-1] [l−1,r−1]是一个完整的01串,然后将第 k k k为变成0,实际上就分裂成两个开区间了—— ( l , k ) , ( k , r ) (l,k),(k,r) (l,k),(k,r)。
为了快速查询分裂点的左右区间号,所以使用set
(用红黑树实现)。
时间复杂度: O ( n ∗ l o g ( n ) ) O(n*log(n)) O(n∗log(n))
// 注意,优先使用数据结构自带的函数,而不要使用通用的函数
// 例如,upper_bound,优先使用set自带的,而不是通用的,否则就是线性时间探测的了。
class Solution {
public:
int findLatestStep(vector<int>& arr, int m) {
int n = arr.size();
if(n == m) return n;
set<int> visited {
0,n+1};
for(int i=n-1; i>=0; --i){
// 找所属区间
auto it = visited.upper_bound(arr[i]);
int left = *prev(it);
int right = *it;
// 删除这堆1,分裂成两堆
if(arr[i] - left - 1 == m || right - arr[i] - 1 == m) return i;
visited.emplace(arr[i]);
}
return -1;
}
};
- 方法二
参考yxc的做法,不过不使用两个指针,一个就可以。
注意每次合并都是一个01串的左右边界和一个1发生合并。
比如 1110111。第四个数变成1的时候。如果左右是1的话,就可以合并在一起了。
具体合并的时候,用数组link[x]
记录点x所在的1串的左右端点序号,(不过,每次发生合并的时候只需要考虑更新左右端点的link
值,换言之,只有一个01串的左右端点的link值才能真实反映1串的左右边界,其他的点不关心。)
时间复杂度: O ( n ) O(n) O(n)
class Solution {
public:
vector<int> link;
int get(int x){
return abs(x-link[x])+1;
}
int findLatestStep(vector<int>& a, int m) {
int n = a.size();
link.resize(n+2,0);
int ans = -1, cnt = 0; // cnt变量记录满足条件的1串的数量
for(int i=0;i<a.size();i++){
int x = a[i];
int l = link[x-1], r = link[x+1];
if(l && r){
if(get(x-1)==m) cnt--;
if(get(x+1)==m) cnt--;
if(get(x-1)+get(x+1)+1==m) cnt++;
link[l] = r;
link[r] = l;
}else if(l){
if(get(x-1)==m) cnt--;
if(get(x-1)+1==m) cnt++;
link[l] = x;
link[x] = l;
}else if(r){
if(get(x+1)==m) cnt--;
if(get(x+1)+1==m) cnt++;
link[r] = x;
link[x] = r;
}else {
link[x] = x;
if(m==1) cnt++;
}
if(cnt) ans = i+1;
}
return ans;
}
};
- 记忆化搜索
用 d p [ l ] [ r ] dp[l][r] dp[l][r]表示存储区间[l,r]所能获得的最大值,对于每个区间,枚举它的所有断点。
状态转移方程:
d p [ l ] [ r ] = m a x l < = k < r ( s u m [ l ] [ k ] < s u m [ k + 1 ] [ r ] ? d p [ l ] [ k ] + s u m [ l ] [ k ] : d p [ k + 1 ] [ r ] + s u m [ k + 1 ] [ r ] ) dp[l][r] =max_{l<=k<r}( sum[l][k]<sum[k+1][r] ?dp[l][k]+sum[l][k] :dp[k+1][r]+sum[k+1][r] ) dp[l][r]=maxl<=k<r(sum[l][k]<sum[k+1][r]?dp[l][k]+sum[l][k]:dp[k+1][r]+sum[k+1][r]) (左侧和与右侧和相等时,都要判断一下)
时间复杂度: O ( n 3 ) O(n^3) O(n3)
比赛的时候如果用迭代去写,很有可能超时(因为力扣测试数据较弱,不够全面,如果用记忆化搜索会减少很多不必要的状态遍历,相当于剪了枝)。
class Solution {
public:
int dp[510][510] = {
0}, pre[510] = {
0};
int stoneGameV(vector<int>& a) {
for(int i=0;i<a.size();i++){
pre[i+1] = pre[i]+a[i];
}
int n = a.size();
return dfs(1,n);
}
int dfs(int l,int r){
if(l==r) return 0;
if(dp[l][r]) return dp[l][r];
int res = 0;
for(int i=l;i<r;i++){
int p1 = pre[i]-pre[l-1];
int p2 = pre[r]-pre[i];
if(p1<p2){
res = max(res,p1+dfs(l,i));
}else if(p1>p2){
res = max(res,p2+dfs(i+1,r));
}else{
res = p1+max(dfs(l,i),dfs(i+1,r));
}
}
return dp[l][r] = res;
}
};
-
区间DP的优化
时间复杂度: O ( n 2 ) O(n^2) O(n2)
hyy大佬的题解
这里只谈一下,理解这种做法的关键点和突破口。
首先由于每个数都是正整数,所以每个区间 [ l , r ] [l,r] [l,r]都会有一个中间点k,使得它成为这个中间点k左边的所有断点截出来的左侧和与右侧和都满足Sum左<Sum右;
k右边的所有断点截出来的左侧和与右侧和都满足Sum左>Sum右;
也就是这样的断点具有单调性。- 如何优化状态转移
一个错误想法,大区间的dp值必大于它的子区间的dp值。所以
d p [ l ] [ r ] = m a x ( s u m [ l ] [ k − 1 ] + d p [ l ] [ k − 1 ] , s u m [ k + 1 ] [ r ] + d p [ k + 1 ] [ r ] ) ; dp[l][r] =max(sum[l][k-1]+dp[l][k-1],sum[k+1][r]+dp[k+1][r]); dp[l][r]=max(sum[l][k−1]+dp[l][k−1],sum[k+1][r]+dp[k+1][r]);
但这样的贪心的想法并不正确。下面就是反例:
[10,5,4] 13
[10,5,4,2] 10
- 如何优化状态转移
之所以有这样的反例是因为每次计算左侧和与右侧和的时候求的一定是前缀和与后缀和。
解决方案:
用mxl[l][r],mxr[l][r]
分别记录区间[l,r]断点在左边、右边能够产生的最大值。
- 断点的具体求法
枚举所有的i(区间左边界),记中间点为k,然后依次枚举右边界j,求出对应区间[i,j]的断点,记为g[i][j]=k
,但要注意中间点k是随着j的增大而增大的(严格来讲,是不减小)。
const int N = 510;
class Solution {
public:
// s存储区间和、g存储区间的中间点、f是动态规划状态数组。
int s[N][N] = {
0}, g[N][N] = {
0}, f[N][N] = {
0}, mxl[N][N] = {
0}, mxr[N][N] = {
0};
int stoneGameV(vector<int>& a) {
int n = a.size();
for(int i=0;i<n;i++){
s[i][i] = a[i];
for(int j=i+1;j<n;j++){
s[i][j] = s[i][j-1]+a[j];
// k 为第一次 左侧和 >= 右侧和 的角标,
// 并且 k 具有单调性,对于每个i,k最多增加到n-1为止.
int k = g[i][j-1];
while(s[i][j]-s[i][k]>s[i][k]){
k++;
}
g[i][j] = k;
}
}
for(int len = 1;len<=n;len++){
for(int l = 0;l+len-1<n;l++){
int r = l+len-1;
int mid = g[l][r];
int ls = s[l][mid], rs = s[mid+1][r];
if(ls==rs){
f[l][r] = max(f[l][r],max(mxl[l][mid],mxr[mid+1][r]));
}else{
if(mid>l) f[l][r] = max(f[l][r],mxl[l][mid-1]);
if(mid<r) f[l][r] = max(f[l][r],mxr[mid+1][r]);
}
int v = f[l][r] + s[l][r];
if(l==r){
mxl[l][r] = mxr[l][r] = s[l][r];
}else{
mxl[l][r] = max(mxl[l][r-1],v);
mxr[l][r] = max(mxr[l+1][r],v);
}
}
}
return f[0][n-1];
}
};