区间dp
给你两个长度分别 n 和 m 的整数数组 nums 和 multipliers ,其中 n >= m ,数组下标 从 1 开始 计数。
初始时,你的分数为 0 。你需要执行恰好 m 步操作。在第 i 步操作(从 1 开始 计数)中,需要:
选择数组 nums 开头处或者末尾处 的整数 x 。
你获得 multipliers[i] * x 分,并累加到你的分数中。
将 x 从数组 nums 中移除。
在执行 m 步操作后,返回 最大 分数。
示例 1:
输入:nums = [1,2,3], multipliers = [3,2,1]
输出:14
解释:一种最优解决方案如下:
- 选择末尾处的整数 3 ,[1,2,3] ,得 3 * 3 = 9 分,累加到分数中。
- 选择末尾处的整数 2 ,[1,2] ,得 2 * 2 = 4 分,累加到分数中。
- 选择末尾处的整数 1 ,[1] ,得 1 * 1 = 1 分,累加到分数中。
总分数为 9 + 4 + 1 = 14 。
示例 2:
输入:nums = [-5,-3,-3,-2,7,1], multipliers = [-10,-5,3,4,6]
输出:102
解释:一种最优解决方案如下:
- 选择开头处的整数 -5 ,[-5,-3,-3,-2,7,1] ,得 -5 * -10 = 50 分,累加到分数中。
- 选择开头处的整数 -3 ,[-3,-3,-2,7,1] ,得 -3 * -5 = 15 分,累加到分数中。
- 选择开头处的整数 -3 ,[-3,-2,7,1] ,得 -3 * 3 = -9 分,累加到分数中。
- 选择末尾处的整数 1 ,[-2,7,1] ,得 1 * 4 = 4 分,累加到分数中。
- 选择末尾处的整数 7 ,[-2,7] ,得 7 * 6 = 42 分,累加到分数中。
总分数为 50 + 15 - 9 + 4 + 42 = 102 。
这是力扣上的一道题,今天听y总的课补的,问题其实很简单,就是每次取一段区间的左端点或者右端点做运算,不断的dp即可。
ac代码如下:
class Solution {
public:
int maximumScore(vector<int>& w, vector<int>& c) {
int n = w.size(), m = c.size();
//因为只可能取左边m个和右边m个,因此中间的n-2m个可以直接删除。
if(n >= 2*m){
int x = m, y = n - m;
while(y < n) w[x ++] = w[y ++];
n = x;
}
vector<vector<int>>f(n + 2, vector<int>(n + 2));
for(int len = n - m + 1; len <= n; len++){//先枚举区间长度,再枚举区间左端点。
for(int l = 1; l + len - 1 <= n; l++){
int r = l + len - 1;
f[l][r] = max(f[l+1][r] + w[l-1]*c[n-len], f[l][r-1]+w[r-1]*c[n-len]);//从左端点拿和从右端点拿的最大值。
}
}
return f[1][n];
}
};
当然还看到了另一种解法:
dp[i][j] : nums开头取i个,末尾取j个的最大得分
k = i + j :代表取的总数
遍历 k
状态转移:
i == 0 : 都是取末尾
dp[i][k - i] = dp[i][k - i - 1] + nums[n - k + i] * multipliers[k - 1];
i == k : 都是取前面
dp[i][k - i] = dp[i - 1][k - i] + nums[i - 1] * multipliers[k - 1];
其他情况 : 取前面与取末尾的比较,取较大者
dp[i][k - i] = max(dp[i][k - i - 1] + nums[n - k + i] * multipliers[k - 1], dp[i - 1][k - i] + nums[i - 1] * multipliers[k - 1]);
结果 : k == m 时, dp[i][j]的最大值。 (m == multipliers.size())
时间复杂度:O(n ^2)
空间复杂度:O(n ^ 2)
代码如下:
class Solution {
public:
int maximumScore(vector<int>& nums, vector<int>& multipliers) {
vector<vector<long long>> dp(1005, vector<long long>(1005, 0));//这里的1005可以改为2*m但不能改为2*n,否则会超时
long long m = multipliers.size(), res = INT_MIN, n = nums.size();
for(int k = 1; k <= m; ++k){
for(int i = 0; i <= k; i++){
if(i == 0) dp[i][k - i] = dp[i][k - i - 1] + nums[n - k + i] * multipliers[k - 1];
else if(i == k) dp[i][k - i] = dp[i - 1][k - i] + nums[i - 1] * multipliers[k - 1];
else dp[i][k - i] = max(dp[i][k - i - 1] + nums[n - k + i] * multipliers[k - 1], dp[i - 1][k - i] + nums[i - 1] * multipliers[k - 1]);
if(k == m) res = max(res, dp[i][k - i]);
}
}
return res;
}
};
479. 加分二叉树(acwing)
设一个n个节点的二叉树tree的中序遍历为(1,2,3,…,n),其中数字1,2,3,…,n为节点编号。
每个节点都有一个分数(均为正整数),记第i个节点的分数为di,tree及它的每个子树都有一个加分,任一棵子树subtree(也包含tree本身)的加分计算方法如下:
subtree的左子树的加分 × subtree的右子树的加分 + subtree的根的分数
若某个子树为空,规定其加分为1。叶子的加分就是叶节点本身的分数,不考虑它的空子树。
试求一棵符合中序遍历为(1,2,3,…,n)且加分最高的二叉树tree。
要求输出:
(1)tree的最高加分
(2)tree的前序遍历
输入格式
第1行:一个整数n,为节点个数。
第2行:n个用空格隔开的整数,为每个节点的分数(0<分数<100)。
输出格式
第1行:一个整数,为最高加分(结果不会超过int范围)。
第2行:n个用空格隔开的整数,为该树的前序遍历。如果存在多种方案,则输出字典序最小的方案。
数据范围
n<30
输入样例:
5
5 7 1 2 10
输出样例:
145
3 1 2 4 5
本题思路就是根据根节点所在的位置k对整棵树进行划分,将整棵树分为左子树和右子树,然后不断划分,一直到len = 1(即最终到达叶子结点),用·一个数组g[l][r]存储(l, r)的根节点, 用数组f[l][r]存储从l到r的加分,当加分可以更新时,即f[l][r]<w[k]+right*left时,同时更新两个数组。最后我们还需要输出字典序最小的方案,也就是优先保证根节点的编号最小,也就意味着我们首先要找到最左边的最大值,而我们是从小到大枚举区间左端点的,而且只有当f[l][r] 严格小于score的时候才更新,等于的时候不会更新,因此我们最开始恰好就是存储了最左边的最大值,所以我们必定能保证字典序,即得到根节点的最小编号。当我们把根节点输出之后,会优先输出左子树,字典序最小也就是让左子树的根节点编号最小,而这刚好就是前一个步骤的子问题,我们只需要递归输出就行了。
AC代码如下:
#include<iostream>
using namespace std;
const int N = 35;
int f[N][N], g[N][N], w[N];
void print(int l, int r){
if(l > r) return ;
int k = g[l][r];
cout << k << ' ';
print(l, k - 1);
print(k+1, r);
}
int main(){
int n; cin >> n;
for(int i = 1; i <= n; i++) cin >> w[i];
for(int len = 1; len <= n; len++){
for(int l = 1; l + len - 1 <= n; l++){
int r = l + len - 1;
if(len == 1){
f[l][r] = w[l];
g[l][r] = l;
}
else{
for(int k = l; k <= r; k++){
int left = k == l? 1 : f[l][k-1];
int right = k == r? 1 : f[k+1][r];
int score = w[k] + right * left;
if(f[l][r] < score){
f[l][r] = score;
g[l][r] = k;
}
}
}
}
}
cout << f[1][n] << endl;
print(1, n);
}
1771. 由子序列构造的最长回文串的长度
给你两个字符串 word1 和 word2 ,请你按下述方法构造一个字符串:
从 word1 中选出某个 非空 子序列 subsequence1 。
从 word2 中选出某个 非空 子序列 subsequence2 。
连接两个子序列 subsequence1 + subsequence2 ,得到字符串。
返回可按上述方法构造的最长 回文串 的 长度 。如果无法构造回文串,返回 0 。
字符串 s 的一个 子序列 是通过从 s 中删除一些(也可能不删除)字符而不更改其余字符的顺序生成的字符串。
回文串 是正着读和反着读结果一致的字符串。
示例 1:
输入:word1 = “cacb”, word2 = “cbba”
输出:5
解释:从 word1 中选出 “ab” ,从 word2 中选出 “cba” ,得到回文串 “abcba” 。
示例 2:
输入:word1 = “ab”, word2 = “ab”
输出:3
解释:从 word1 中选出 “ab” ,从 word2 中选出 “a” ,得到回文串 “aba” 。
示例 3:
输入:word1 = “aa”, word2 = “bb”
输出:0
解释:无法按题面所述方法构造回文串,所以返回 0 。
提示:
1 <= word1.length, word2.length <= 1000
word1 和 word2 由小写英文字母组成
这是力扣周赛的一道题,比赛时没有想出来,赛后补题才发现其实这是一个很简单的区间dp, 先将给定的两个字符串相加,然后判断每一个区间内的最长回文串,只有当取出来的两个字母分别属于两个字符串时才更新结果。
const int N =2010;
int dp[N][N];
class Solution {
public:
int longestPalindrome(string a, string b) {
int n = a.size(), m = b.size(), len = n + m;
string w = a + b;
int res = (w[n-1] == w[n]) ? 2 : 0;
for(int i = 0; i < len; i++){
dp[i][i] = 1;
}
for(int i = 0; i < len - 1; i++){
dp[i][i+1] = (w[i] == w[i+1]) ? 2 : 1;
}
for(int l = 2; l < len; l++){
for(int i = 0; i + l < len; i++){
int j = i + l;
if(w[i] == w[j]){
dp[i][j] = dp[i+1][j-1] + 2;
//若i j 分别属于两个字符串,才更新最终结果
if(i < n && j >= n) res = max(dp[i][j], res);
}
else dp[i][j] = max(dp[i+1][j], dp[i][j - 1]);
}
}
return res;
}
};