1. 模式串匹配
题目:给出两个字符串,分别是模式串P和目标串T,
判断模式串和目标串是否匹配,匹配输出 1,不匹配输出 0。
模式串中‘?’可以匹配目标串中的任何字符,
模式串中的 ’*’可以匹配目标串中的任何长度的串,
模式串的其它字符必须和目标串的字符匹配。
输入描述:
输入第一行包含一个字符串p, (1 ≤ |p| ≤ 20).
输入第二行包含一个字符串t, (1 ≤ |t| ≤ 20).
输出描述:
输出仅包含0和1的整数,0表示p和t不匹配,1表示p和t匹配。
#include<iostream>
#include<string>
#include<vector>
using namespace std;
//算法思想:dp[i][j]为s[1...i]与t[1...j]是否匹配。
int main()
{
string s1;
string s2;
while (cin >> s2 >> s1)
{
int m = s1.size(); int n = s2.size();
vector<vector<bool>> dp(m + 1, vector<bool>(n + 1, 0));
dp[0][0] = 1;
for (int i = 1; i <= n; i++)
dp[0][i] = dp[0][i - 1] && s2[i - 1] == '*';
for (int i = 1; i <= m; i++)
{
for (int j = 1; j <= n; j++)
{
if (s1[i - 1] == s2[j - 1] || s2[j - 1] == '?')
dp[i][j] = dp[i - 1][j - 1];
if (s2[j - 1] == '*')
dp[i][j] = dp[i - 1][j] || dp[i][j - 1];
}
}
cout << dp[m][n] << endl;
}
return 0;
}
2. 完全背包—组合——零钱兑换II
给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。
示例 1:
输入: amount = 5, coins = [1, 2, 5]
输出: 4
解释: 有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1
示例 2:
输入: amount = 3, coins = [2]
输出: 0
解释: 只用面额2的硬币不能凑成总金额3。
示例 3:
输入: amount = 10, coins = [10]
输出: 1
算法思想:(类比0/1背包问题)
因为这是个组合问题,我们不关心硬币使用的顺序,而是硬币有没有被用到。是否使用第k个硬币受到之前情况的影响。
状态转移方程如下
if 金额数大于硬币
DP[k][i] = DP[k-1][i] + DP[k][i-k]
else
DP[k][i] = DP[k-1][i]
class Solution {
public:
int change(int amount, vector<int>& coins) {
int K = coins.size() + 1;
int I = amount + 1;
int DP[K][I];
//初始化数组
for (int k = 0; k < K; k++){
for (int i = 0; i < I; i++){
DP[k][i] = 0;
}
}
//初始化基本状态
for (int k = 0; k < coins.size() + 1; k++){
DP[k][0] = 1;
}
//k,i循环可互换
for (int k = 1; k <= coins.size() ; k++){
for (int i = 1; i <= amount; i++){
if ( i >= coins[k-1]) {
DP[k][i] = DP[k][i-coins[k-1]] + DP[k-1][i];
} else{
DP[k][i] = DP[k-1][i];
}
}
}
return DP[coins.size()][amount];
}
};
进阶:
之前爬楼梯问题中,我们将一维数组降维成点。这里问题能不能也试着降低一个维度,只用一个数组进行表示呢?
这个时候,我们就需要重新定义我们的子问题了。
此时的子问题是,对于硬币从0到k,我们必须使用第k个硬币(前k个硬币)的时候,当前金额的组合数。
因此状态数组DP[i]表示的是对于第k个硬币能凑的组合数
状态转移方程如下:
DP[i] = DP[i] + DP[i-coins[k]]
class Solution {
public:
int change(int amount, vector<int>& coins) {
int dp[amount+1];
memset(dp, 0, sizeof(dp)); //初始化数组为0
dp[0] = 1;
for (int coin : coins){ //枚举硬币
for (int i = 1; i <= amount; i++){ //枚举金额
if (i < coin) continue; // coin不能大于amount
dp[i] += dp[i-coin];
}
}
return dp[amount];
}
};
3. 0/1背包——分割等和子集
题目:
给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
注意:
每个数组中的元素不会超过 100
数组的大小不会超过 200
示例 1:
输入: [1, 5, 11, 5]
输出: true
解释: 数组可以分割成 [1, 5, 5] 和 [11].
算法思想:
class Solution {
public:
bool canPartition(vector<int>& nums) {
int n = nums.size();
if (n < 2) {
return false;
}
int sum = accumulate(nums.begin(), nums.end(), 0);
int maxNum = *max_element(nums.begin(), nums.end());
if (sum & 1) {
return false;
}
int target = sum / 2;
if (maxNum > target) {
return false;
}
vector<vector<int>> dp(n, vector<int>(target + 1, 0));
for (int i = 0; i < n; i++) {
dp[i][0] = true;
}
dp[0][nums[0]] = true;
for (int i = 1; i < n; i++) {
int num = nums[i];
for (int j = 1; j <= target; j++) {
if (j >= num) {
dp[i][j] = dp[i - 1][j] | dp[i - 1][j - num];
} else {
dp[i][j] = dp[i - 1][j];
}
}
}
return dp[n - 1][target];
}
};
改进:
vector<int> dp(target + 1, 0);
dp[0] = true;
for (int i = 0; i < n; i++) {
int num = nums[i];
for (int j = target; j >= num; --j) {
dp[j] |= dp[j - num];
}
}
return dp[target];
4. 0/1背包——最后一块石头的重量 II
题目:
有一堆石头,每块石头的重量都是正整数。
每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:
如果 x == y,那么两块石头都会被完全粉碎;
如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。
最后,最多只会剩下一块石头。返回此石头最小的可能重量。如果没有石头剩下,就返回 0。
示例:
输入:[2,7,4,1,8,1]
输出:1
解释:
组合 2 和 4,得到 2,所以数组转化为 [2,7,1,8,1],
组合 7 和 8,得到 1,所以数组转化为 [2,1,1,1],
组合 2 和 1,得到 1,所以数组转化为 [1,1,1],
组合 1 和 1,得到 0,所以数组转化为 [1],这就是最优值。
算法思想:同题3类似,求容量为target = sum /2的最大背包装载量。
class Solution {
public:
int lastStoneWeightII(vector<int>& stones) {
vector<int> dp(15001, 0);
int sum = 0;
for (int i = 0; i < stones.size(); i++) sum += stones[i];
int target = sum / 2;
for (int i = 0; i < stones.size(); i++) { // 遍历物品
for (int j = target; j >= stones[i]; j--) { // 遍历背包
dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
}
}
return (sum - dp[target]) - dp[target];
}
};
5. 预测赢家
题目:
给定一个表示分数的非负整数数组。 玩家 1 从数组任意一端拿取一个分数,随后玩家 2 继续从剩余数组任意一端拿取分数,然后玩家 1 拿,…… 。每次一个玩家只能拿取一个分数,分数被拿取之后不再可取。直到没有剩余分数可取时游戏结束。最终获得分数总和最多的玩家获胜。
给定一个表示分数的数组,预测玩家1是否会成为赢家。你可以假设每个玩家的玩法都会使他的分数最大化。(每个玩家都能看到所有的数字)
示例 1:
输入:[1, 5, 2]
输出:False
解释:一开始,玩家1可以从1和2中进行选择。
如果他选择 2(或者 1 ),那么玩家 2 可以从 1(或者 2 )和 5 中进行选择。如果玩家 2 选择了 5 ,那么玩家 1 则只剩下 1(或者 2 )可选。
所以,玩家 1 的最终分数为 1 + 2 = 3,而玩家 2 为 5 。
因此,玩家 1 永远不会成为赢家,返回 False 。
法一:回溯法
class Solution {
public boolean PredictTheWinner(int[] nums) {
return total(nums, 0, nums.length - 1, 1) >= 0;
}
public int total(int[] nums, int start, int end, int turn) {
if (start == end) {
return nums[start] * turn;
}
int scoreStart = nums[start] * turn + total(nums, start + 1, end, -turn);
int scoreEnd = nums[end] * turn + total(nums, start, end - 1, -turn);
return Math.max(scoreStart * turn, scoreEnd * turn) * turn;
# 此处时关键:玩家一会选择当前total最大的选择,而玩家二会选择当前total小的选择
}
}
//时间复杂度:O(2^n),其中 nn 是数组的长度。
//空间复杂度:O(n),其中 nn 是数组的长度。空间复杂度取决于递归使用的栈空间。
法二:动态规划
//dp[i][j] 表示当数组剩下的部分为下标 ii 到下标 jj 时,当前玩家与另一个玩家的分数之差的最大值,
//注意当前玩家不一定是先手。
class Solution {
public:
bool PredictTheWinner(vector<int>& nums) {
int length = nums.size();
auto dp = vector<vector<int>> (length, vector<int>(length));
for (int i = 0; i < length; i++) {
dp[i][i] = nums[i];
}
for (int i = length - 2; i >= 0; i--) { //
for (int j = i + 1; j < length; j++) {
dp[i][j] = max(nums[i] - dp[i + 1][j], nums[j] - dp[i][j - 1]);
}
}
return dp[0][length - 1] >= 0;
}
};
优化:
auto dp = vector<int>(length);
dp[j] = max(nums[i] - dp[j], nums[j] - dp[j - 1]);
return dp[length - 1] >= 0;
6. 预测赢家2——(偶数堆)石子游戏
题目:
亚历克斯和李用几堆石子在做游戏。偶数堆石子排成一行,每堆都有正整数颗石子 piles[i] 。
游戏以谁手中的石子最多来决出胜负。石子的总数是奇数,所以没有平局。
亚历克斯和李轮流进行,亚历克斯先开始。 每回合,玩家从行的开始或结束处取走整堆石头。 这种情况一直持续到没有更多的石子堆为止,此时手中石子最多的玩家获胜。
假设亚历克斯和李都发挥出最佳水平,当亚历克斯赢得比赛时返回 true ,当李赢得比赛时返回 false 。
示例:
输入:[5,3,4,5]
输出:true
解释:
亚历克斯先开始,只能拿前 5 颗或后 5 颗石子 。
假设他取了前 5 颗,这一行就变成了 [3,4,5] 。
如果李拿走前 3 颗,那么剩下的是 [4,5],亚历克斯拿走后 5 颗赢得 10 分。
如果李拿走后 5 颗,那么剩下的是 [3,4],亚历克斯拿走后 4 颗赢得 9 分。
这表明,取前 5 颗石子对亚历克斯来说是一个胜利的举动,所以我们返回 true 。
法一:同预测赢家问题(动态规划)
class Solution {
public:
bool stoneGame(vector<int>& piles) {
int length = piles.size();
auto dp = vector<vector<int>> (length, vector<int>(length));
for (int i = 0; i < length; i++) {
dp[i][i] = piles[i];
}
for (int i = length - 2; i >= 0; i--) { // 由短到长的新写法(其他写法:len = 2->len,i = 0->n)
for (int j = i + 1; j < length; j++) {
dp[i][j] = max(piles[i] - dp[i + 1][j], piles[j] - dp[i][j - 1]);
}
}
return dp[0][length - 1] >= 0;
}
};
法二:数学方法分析
假设有n堆石子,n是偶数,则每堆石子的下标从 0 到 n-1。根据下标将 n 堆石子分成两组,
每组有n/2堆石子,下标为偶数的石子堆属于第一组,下标为奇数的石子堆属于第二组。
初始时,行的开始处的石子堆位于下标 0,属于第一组,
行的结束处的石子堆位于下标 n-1,属于第二组,因此作为先手的
Alex 可以自由选择取走第一组的一堆石子或者第二组的一堆石子。
如果Alex 取走第一组的一堆石子,则剩下的部分在行的开始处和结束处的石子堆都属于第二组,
因此Lee 只能取走第二组的一堆石子。同理...
所以,如果作为先手的Alex 可以在第一次取走石子时就决定取走哪一组的石子,并全程取走同一组的石子,
就能得到唯一的比赛结果。故先手Alex必胜。
class Solution {
public:
bool stoneGame(vector<int>& piles) {
return true;
}
};
7. 最长递增子序列
题目:
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
示例 2:
输入:nums = [0,1,0,3,2,3]
输出:4
算法:定义dp[i]为以nums[i]结尾的最长公共子序列的长度。
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int n=nums.size(),maxn = 0;;
vector<int> dp(n);
for(int i=0;i<n;i++){
dp[i] = 1;
for(int j=i-1;j>=0;j--){
if(nums[j]>= nums[i]) continue;
dp[i] = max(dp[i],dp[j]+1);
}
}
return *max_element(dp.begin(), dp.end());
}
/* 错解:单调递增栈
int lengthOfLIS(vector<int>& nums) {
stack<int> ascendNums;
int maxlen=1;
ascendNums.push(nums[0]);
for(int i=1;i<nums.size();i++){
while(!ascendNums.empty()&&nums[i]<=ascendNums.top()){
ascendNums.pop();
}
ascendNums.push(nums[i]);
if(ascendNums.size() > maxlen){
maxlen = ascendNums.size();
}
}
return maxlen;
}
*/
};
变体:最长嵌套盒子
题目:
有n个矩形,每个矩形可以用a,b来描述,表示长和宽。矩形X(a,b)可以嵌套在矩形Y(c,d)中当且仅当a
第一行是一个正正数N(0
随后的n行,每行有两个数a,b(0输出描述:
每组测试数据都输出一个数,表示最多符合条件的矩形数目,每组输出占一行
样例输入:
1
10
1 2
2 4
5 8
6 10
7 9
3 1
5 8
12 10
9 7
2 2
样例输出:
5
算法:
先按宽排序,再按高来挑选最长递增子序列。
#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
using namespace std;
struct node{
int x,y;
}p[100009];
int dp[100009];
bool cmp(node a,node b)
{
if(a.x == b.x)
{
return a.y<b.y;
}
return a.x<b.x;
}
int main()
{
int n;
scanf("%d",&n);
while(n--)
{
int n1;
memset(dp,0,sizeof(dp));
memset(p,0,sizeof(p));
scanf("%d",&n1);int rr = 0,k1=0,k2 = 0;
for(int i=0;i<n1;++i)
{
scanf("%d%d",&k1,&k2);
p[rr].x = k1;p[rr].y = k2;rr++;
p[rr].x = k2;p[rr].y = k1;rr++;//把两种状态模拟一下
}
sort(p,p+rr,cmp);int r=0;int max1 = 1;
//套最长上升子序列模板
for(int i = 0;i < rr;++i)
{
dp[i] = 1;
for(int j = 0;j < i;++j)
{
if(p[i].y>p[j].y && p[i].x>p[j].x)
{
dp[i] = max(dp[i],dp[j]+1);
}
}
max1 = max(max1,dp[i]);
}
printf("%d\n",max1);
}
return 0;
}
8. 买卖股票【状态】
多次买卖
题目:可以多次买卖,买一个新股票时手里不能留有股票。求最大利润。
方法:求所有升序和。
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n = prices.size();
int sum = 0;
for(int i=1; i<n;i++){
if(prices[i] - prices[i-1] > 0){
sum += prices[i] - prices[i-1];
}
}
return sum;
}
};
两次买卖——状态+动态规划
题目:可以进行最多两次买卖,买一个新股票时手里不能留有股票(可以先卖再买)
算法:
由于我们最多可以完成两笔交易,因此在任意一天结束之后,我们会处于以下五个状态中的一种:
- 未进行过任何操作;
- 只进行过一次买操作;
- 进行了一次买操作和一次卖操作,即完成了一笔交易;
- 在完成了一笔交易的前提下,进行了第二次买操作;
- 完成了全部两笔交易。
动态规划:转移方程
![S3(U6%N14`BO50CT97XXP3.png
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n = prices.size();
int buy1 = -prices[0], sell1 = 0;
int buy2 = -prices[0], sell2 = 0;
for (int i = 1; i < n; ++i) {
buy1 = max(buy1, -prices[i]);
sell1 = max(sell1, buy1 + prices[i]);
buy2 = max(buy2, sell1 - prices[i]);
sell2 = max(sell2, buy2 + prices[i]);
}
return sell2;
}
};
多次买卖含冷冻期——状态+动态规划
题目:
定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。
设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
示例:
输入: [1,2,3,0,2]
输出: 3
解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]
算法:
用 f[i] 表示第 i 天结束之后的「累计最大收益」
此时会有三种不同的状态:
- 我们目前持有一支股票,尚未卖出,对应的「累计最大收益」记为 f[i][0];
- 我们目前不持有任何股票,因为今天刚卖出,明天将处于冷冻期中,对应的「累计最大收益」记为 f[i][1};
- 我们目前不持有任何股票,对应的「累计最大收益」记为 f[i][2]。
转移方程:
class Solution {
public:
int maxProfit(vector<int>& prices) {
if (prices.empty()) {
return 0;
}
int n = prices.size();
// f[i][0]: 手上持有股票的最大收益
// f[i][1]: 手上不持有股票,并且处于冷冻期中的累计最大收益
// f[i][2]: 手上不持有股票,并且不在冷冻期中的累计最大收益
vector<vector<int>> f(n, vector<int>(3));
f[0][0] = -prices[0];
for (int i = 1; i < n; ++i) {
f[i][0] = max(f[i - 1][0], f[i - 1][2] - prices[i]);
f[i][1] = f[i - 1][0] + prices[i];
f[i][2] = max(f[i - 1][1], f[i - 1][2]);
}
return max(f[n - 1][1], f[n - 1][2]);
}
};
空间优化:
{
int f0 = -prices[0];
int f1 = 0;
int f2 = 0;
for (int i = 1; i < n; ++i) {
int newf0 = max(f0, f2 - prices[i]);
int newf1 = f0 + prices[i];
int newf2 = max(f1, f2);
f0 = newf0;
f1 = newf1;
f2 = newf2;
}
return max(f1, f2);
}
多次买卖含手续费
题目:
给定一个整数数组 prices,其中第 i 个元素代表了第 i 天的股票价格 ;非负整数 fee 代表了交易股票的手续费用。
你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。
返回获得利润的最大值。
注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。
示例 1:
输入: prices = [1, 3, 2, 8, 4, 9], fee = 2
输出: 8
解释: 能够达到的最大利润:
在此处买入 prices[0] = 1
在此处卖出 prices[3] = 8
在此处买入 prices[4] = 4
在此处卖出 prices[5] = 9
总利润: ((8 - 1) - 2) + ((9 - 4) - 2) = 8.
法一:动态规划+状态
(注意此题和上一题的区别,上一题由于冷冻期的存在,买股票的状态只能从非冷冻期的无股票状态买;而本题买股票的状态直接从无股票状态买,即上次一刚刚卖出我本次也可以买,故只需要两状态)
class Solution {
public:
int maxProfit(vector<int>& prices, int fee) {
int n=prices.size();
int f1 = -prices[0];//已买了一只股票
int f2 = 0;//本次手上没有股票
for(int i=1;i<n;i++){
int nextf1 = max(f1,f2-prices[i]);
int nextf2 = max(f2,f1+prices[i]-fee);
f1 = nextf1;
f2 = nextf2;
}
return f2;
}
};
法二:贪心(将手续费算在买股票时支出)
class Solution {
public:
int maxProfit(vector<int>& prices, int fee) {
int n = prices.size();
int buy = prices[0] + fee;
int profit = 0;
for (int i = 1; i < n; ++i) {
if (prices[i] + fee < buy) {
buy = prices[i] + fee;
}
else if (prices[i] > buy) {
profit += prices[i] - buy;
buy = prices[i]; //提供反悔机会
}
}
return profit;
}
};