贪心的基本概念
所谓贪心算法,是指在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的仅是在某种意义上的局部最优解。
贪心算法没有固定的算法框架,算法设计的关键是贪心策略的选择。必须注意的是,贪心算法不是对所有问题都能得到整体最优解,选择的贪心策略必须具备无后效性,即某个状态以后的过程不会影响以前的状态,只与当前状态有关。
所以对所采用的贪心策略一定要仔细分析其是否满足无后效性。
贪心策略适用的前提是:局部最优策略能导致产生全局最优解。
实际上,贪心算法适用的情况很少。一般,对一个问题分析是否适用于贪心算法,可以先选择该问题下的几个实际数据进行分析,就可做出判断。
基于贪心算法的几类区间覆盖问题
区间完全覆盖问题
问题描述:
给定一个长度为m的区间,再给出n条线段的起点和终点(注意这里是闭区间),求最少使用多少条线段可以将整个区间完全覆盖。
样例:
区间长度8,可选的覆盖线段[2,6],[1,4],[3,6],[3,7],[6,8],[2,4],[3,5]
解题过程:
1、将每一个区间按照左端点递增顺序排列,排完序后为[1,4],[2,4],[2,6],[3,5],[3,6],[3,7],[6,8];
2、设置一个变量表示已经覆盖到的区域。在剩下的线段中找出所有左端点小于等于当前已经覆盖到的区域的右端点的线段中,右端点最大的线段,加入,直到已经覆盖全部的区域。
过程:
假设第一步加入[1,4],那么下一步能够选择的有[2,6],[3,5],[3,6],[3,7],由于7最大,所以下一步选择[3,7],最后一步只能选择[6,8],这个时候刚好达到了8,于是退出,所选区间为3。
最大不相交覆盖
问题描述:
给定一个长度为m的区间,再给出n条线段的起点和终点(开区间和闭区间处理的方法不同,这里以开区间为例),问题是从中选取尽量多的线段,使得每个线段都是独立不相交的。
样例:
区间长度8,可选的覆盖线段[2,6],[1,4],[3,6],[3,7],[6,8],[2,4],[3,5]
解题过程:
1、对线段的右端点进行升序排序;
2、从右端点第二大的线段开始,选择左端点最大的那一条,如果加入以后不会跟之前的线段产生公共部分,那么就加入,否则就继续判断后面的线段。
过程:
- 排序:将每一个区间按右端点进行递增顺序排列,排完序后为[1,4],[2,4],[3,5],[2,6],[3,6],[3,7],[6,8];
- 第一步选取[2,4],发现后面只能加入[6,8],所以区间的个数为2。
const int N=100000+10;
int n,s[N]={0},t[N]={0};
int main() {
vector<P> v;
while(cin>>n)) {
for(int i=0;i<n;i++)
{
cin>>s[i]>>t[i];
v.push_back(P(s[i],t[i]));
}
//按照右端点的升序排序
sort(v.begin(),v.end(),myCmp());
int ans=0;
int t=0;
for(int i=0;i<n;i++)
{
if(t<v[i].first)
{
ans++;
t=v[i].second;//前一个线段的右端点
}
}
cout<<ans<<endl;
}
}
区间选点问题
问题描述:
数轴上有n个闭区间[Ai,Bi],取尽量少的点,使得每个区间都至少有一个点。
样例
输入:n=5, [1,5], [8,9], [4,7], [2,6], [3,5]
输出:2 (选择点5,9即可)
贪心策略:把所有区间按照B从小到大排序,如果B相同,按照A从大到小排序,每次都取第一个区间中的最后一个点。
const int N=10000+10;
struct Node {
int L,R;
bool operator<(const Node& rhs) const
{
return R<rhs.R || (R==rhs.R && L>rhs.L);
}
}a[N];
int main()
{
int n;
while(cin>>n) {
me(a);
for(int i=0;i<n;i++)
scanf("%d%d",&a[i].L,&a[i].R);
sort(a,a+n);
int ans=0,p=0;
for(int i=0;i<n;i++) {
if(p<a[i].L) {
p=a[i].R;
ans++;
}
}
cout<<ans<<endl;
}
}
动态规划的基本概念
动态规划是利用存储历史信息使得未来需要历史信息时不需要重新计算, 从而达到降低时间复杂度, 用空间复杂度换取时间复杂度的方法。可以把动态规划分为以下几步:
- 确定递推量。 这一步需要确定递推过程中要保留的历史信息数量和具体含义, 同时也会定下动态规划的维度;
- 推导递推式。 根据确定的递推量, 得到如何利用存储的历史信息在有效时间(通常是常量或者线性时间)内得到当前的信息结果;
- 计算初始条件。 有了递推式之后, 我们只需要计算初始条件, 就可以根据递推式得到我们想要的结果了。 通常初始条件都是比较简单的情况, 一般来说直接赋值即可;
动态规划的时间复杂度是O((维度)×(每步获取当前值所用的时间复杂度))。 基本上按照上面的思路, 动态规划的题目都可以解决, 不过最难的一般是在确定递推量, 一个好的递推量可以使得动态规划的时间复杂度尽量低。
记忆化搜索的基本概念
记忆化搜索=搜索的形式+动态规划的思想。
记忆化搜索的思想是,在搜索过程中,会有很多重复计算,如果我们能记录一些状态的答案,就可以减少重复搜索量。最典型的记忆化搜索的应用就是滑雪问题,见下题329。
DP是从下向上求解的,而记忆化搜索是从上向下的,因为它用到了递归。
329. 矩阵中最长的上升路径(滑雪问题)
思路:
可看作滑雪问题,因为求最长的上升路径也可以理解成求最长的下降路径。
这道题给我们一个二维数组,让我们求矩阵中最长的递增路径,规定我们只能上下左右行走,不能走斜线或者是超过了边界。那么这道题的解法要用记忆化搜索来做,可以避免重复计算。
我们需要维护一个二维动态数组dp,其中dp[i][j]表示数组中以(i,j)为终点的最长递增路径的长度(不包括自己),初始将dp数组都赋为0,当我们用递归调用时,遇到某个位置(x, y), 如果dp[x][y]不为0的话,我们直接返回dp[x][y]即可,不需要重复计算。
我们需要以数组中每个位置都为终点调用递归来做,比较找出最大值。在以一个位置为起点用DFS搜索时,对其四个相邻位置进行判断,如果相邻位置的值小于该位置,则对相邻位置继续调用递归,求该相邻位置的dp。并更新当前该位置的dp,搜素完成后返回即可。
注意最后是返回res+1,因为实际上最长递增路径的终点也是要算进去的。
class Solution {
public:
int longestIncreasingPath(vector<vector<int> >& matrix) {
if (matrix.empty() || matrix[0].empty())
return 0;
int res = 0;
int m = matrix.size();
int n = matrix[0].size();
dp.resize(m, vector<int>(n, 0));
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
res = max(res, dfs(matrix, i, j));
}
}
return res+1;
}
int dx[4]={0,0,-1,1};
int dy[4]={1,-1,0,0};
vector<vector<int> > dp;
int dfs(vector<vector<int> > &matrix, int i, int j)
{
if (dp[i][j]) return dp[i][j];
int m = matrix.size();
int n = matrix[0].size();
int x,y;
for(int k=0;k<4;k++)
{
x = i + dx[k];
y = j + dy[k];
if (x >= 0 && x < m && y >= 0 && y < n && matrix[i][j] > matrix[x][y] )
dp[i][j] = max(dp[i][j], 1 + dfs(matrix, x, y));
}
return dp[i][j];
}
};
用DP和记忆化搜索解最长公共子序列(LCS)
DP
int dp[MAXN][MAXN];
string str1, str2;
int main(void)
{
cin >> str1 >> str2;
for(int i=1; i<=str1.size(); ++i)
{
for(int j=1; j<=str2.size(); ++j)
{
if(str1[i] == str2[j])
dp[i][j] = dp[i-1][j-1]+1;
else dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
}
}
cout << dp[str1.size()][str2.size()] << endl;
return 0;
}
记忆化搜索
int dp[MAXN][MAXN];
string str1, str2;
int LookUp(int i, int j) {
if(dp[i][j])
return dp[i][j];
if(i==0 || j==0)
return 0;
if(str1[i-1] == str2[j-1]) {
dp[i][j] = LookUp(i-1, j-1)+1;
}
else dp[i][j] = max(LookUp(i-1, j), LookUp(i, j-1));
return dp[i][j];
}
int main(void)
{
cin >> str1 >> str2;
LookUp(str1.size(), str2.size());
cout << dp[str1.size()][str2.size()] << endl;
return 0;
}
11. 盛最多的水
给定n个非负整数a1,a2,…,an,其中每个代表一个点坐标(i,ai)。n个垂直线段,线段的两个端点在(i,ai)和(i,0)。在x坐标上找到两个线段,与x轴形成一个容器,使其包含最多的水。
思路:
这是一个贪心策略,每次取两边围栏最矮的一个推进,希望获取更多的水。
容器的宽是两个点的横坐标之差,高是两个点中较短的那个。初始状态是left=0,right=n,然后逐渐朝里靠拢。对于某一次的状态,假设
height[left]<height[right]
那么就不应该是right–,而应该是left++,因为right–不可能使容积变大,此时的短板在left那边。所以每次移动时,都是移动较短的那一边。最后,当left+1=right时结束,返回整个过程中容积的最大值。
class Solution {
public:
int maxArea(vector<int>& height) {
int AreaMax=0;
int temp=0;
int left=0;
int right=height.size()-1;
AreaMax = (right-left)*min(height[left],height[right]);
while(left<right)
{
height[left] < height[right] ? left++ : right--;
temp = (right-left)*min(height[left],height[right]);
if(AreaMax<temp)
AreaMax=temp;
}
return AreaMax;
}
};
343. 拆分整数,使乘积最大
已知n(n>=2),求相加等于n且乘积最大的一组整数的积。
思路:
可以用数学求导解决,也可以用DP思想。
记dp[n]为n对应的最大乘积。
那么递推方程就是:dp[n]=max(i*dp[n-i],i*(n-i))
(其中i从1到n-1)。
边界:dp[2]=1;
class Solution {
public:
int integerBreak(int n) {
vector<int> dp(n+5,0);
dp[1]=1;
dp[2]=1;
for(int i=3;i<=n;i++)
{
for(int j=1;j<i;j++)
{
dp[i]=max(dp[i],max(j*dp[i-j],j*(i-j)));
}
}
return dp[n];
}
};
96. 结点数为n的二叉搜索树的数目
已知整数n,返回结点数为n的结构不同的二叉搜索树的数目。
思路:
对于一颗二叉树,简单来说可以分为:
根结点
/ \ 左子树 右子树
假如整个树有 n 个结点,根结点为 1 个结点,两个子树平分剩下的 n-1 个结点。
假设我们已经知道结点数量为 x 的二叉树有dp[x]种不同的形态。
则一颗二叉树左结点数量为 k 时,其形态数为dp[k] * dp[n - 1 - k]
。
而对于一颗 n 个结点的二叉树,其两个子树分配结点的方案有 n-1 种:
(0, n-1), (1, n-2), ..., (n-1, 0)
因此我们可以得到对于 n 个结点的二叉树,其形态有:
//求和
Sigma(dp[i] * dp[n-1-i]) | i = 0 .. n-1
边界条件为dp[0] = 1。
class Solution {
public:
int numTrees(int n) {
int dp[n+1] = {0};
if(n == 0) return 1;
if(n == 1) return 1;
if(n == 2) return 2;
dp[0] = 1;
dp[1] = 1;
dp[2] = 2;
for(int i = 3;i <= n;i++)
{
for(int j=0;j<i;j++)
dp[i] += dp[j]*dp[i-1-j];
}
return dp[n];
}
};
70. 上台阶
上n层台阶,一次可以上1层,也可以上2层。问上n层台阶总共有多少种不同的方式。
思路:
经典的dp问题。其实是一个斐波那契数列。
class Solution {
public:
int climbStairs(int n) {
int i;
int a = 0, b = 1, c;
if (n <= 1)
return n;
for (i = 2; i <= n; i++)
{
c = a + b;
a = b;
b = c;
}
return c;
}
};
进阶版:
如果题目变成一次可以上1层,也可以上2层……也可以上n层。问上n层台阶总共有多少种方式。
思路:
找规律,可以发现f(n)=f(n-1)+f(n-2)+……f(0)=2*f(n-1)
所以:
class Solution {
public:
int climbStairs(int n) {
if(number<=0)
return 0;
if(number==1)
return 1;
int sum=1<<(number-1);//位运算代替*2 1<<n 意思为2的n次方
return sum;
}
};
322. 组成某个数的最少纸币数(Java)
给定不同面额的纸币,每种面额可以有无限张,给定一个数额,要求组成该数额的最少的纸币数。
思路:
用dp[i]存储组成金额i所需要的纸币数量,当满足:
- i>=coins[j]
- dp[i-coins[j]] != INT_MAX,即可以用纸币组成金额i-coins[j]
时,递推公式为:
dp[i] = min(dp[i], dp[i - coins[j]] + 1)
。
public class Solution {
public int coinChange(int[] coins, int amount) {
int[] dp = new int[amount + 1];
for (int i = 1; i <= amount; i++)
{
dp[i] = Integer.MAX_VALUE;
//i大于等于纸币面额,而且i-coins[j]是可以被组成的
if (i >= coins[j] && dp[i - coins[j]] != Integer.MAX_VALUE)
dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1);
}
return dp[amount] == Integer.MAX_VALUE ? -1 : dp[amount];
}
}
53. 和最大的子串
Find the contiguous subarray within an array (containing at least one number) which has the largest sum.
For example, given the array [−2,1,−3,4,−1,2,1,−5,4],
the contiguous subarray [4,−1,2,1] has the largest sum = 6.
思路:
可以用动态规划的思路解决。
记r[i]表示以nums[i]结尾的子串中最大的那个和,r[i]应该怎么求呢?r[i]可以表示成
r[i]=max(r[i-1]+nums[i],nums[i]);
于是问题就变成求r中的最大值。
class Solution {
public:
int maxSubArray(vector<int>& nums) {
vector<int> r(nums.size());
int result=nums[0];
r[0]=nums[0];
//从第2个数开始,求以nums[i]结尾的子串的最大值
for(int i=1;i<nums.size();i++)
{
r[i]=max(r[i-1]+nums[i],nums[i]);
result=max(result,r[i]);
}
return result;
}
};
309. 买卖股票的最佳策略(带cooldown)
Say you have an array for which the ith element is the price of a given stock on day i.
Design an algorithm to find the maximum profit. You may complete as many transactions as you like (ie, buy one and sell one share of the stock multiple times) with the following restrictions:
You may not engage in multiple transactions at the same time (ie, you must sell the stock before you buy again).
After you sell your stock, you cannot buy stock on next day. (ie, cooldown 1 day)
Example:
prices = [1, 2, 3, 0, 2]
maxProfit = 3
transactions = [buy, sell, cooldown, buy, sell]
思路:
状态的跳转是依据时间的跳转,即第i天的收益情况依赖于第i-1天的收益情况。不过现在需要三个状态,即buy,sell,cooldown(满仓、空仓、空仓并且已冷却过)。
我们记录第i-1天的这三个状态的收益情况是last_ buy,last_sell,last_cooldown。那么第i天的这三个收益情况的依赖关系是:
buy=max(last_buy, last_cooldown - price[i]);
sell = max(last_sell, last_buy + price[i]);
cooldown = max(last_cooldown, last_sell);
代码如下:
class Solution {
public:
int maxProfit(vector<int>& prices) {
int last_sell = 0, last_buy = INT_MIN, last_cooldown = 0, sell = 0, buy = 0, cooldown = 0;
for(auto price:prices) {
buy = max(last_buy, last_cooldown - price);
sell = max(last_sell, last_buy + price);
cooldown = max(last_cooldown, last_sell);
last_buy = buy;
last_sell = sell;
last_cooldown = cooldown;
}
return sell;
}
};
121. 买卖股票的最佳策略(只能买卖一次)
思路:
假设最佳策略是在第i天做出的,那么该天的最大收益是“在第1到i-1天中价格最低的时候买入,当天卖出”得到的。
记lowest为到目前为止的最低价;记m为到目前为止能取得的最大收益。
从头到尾遍历prices,最后得到的m即为所求。
class Solution {
public:
int maxProfit(vector<int>& prices) {
if(prices.size()<=1) return 0;
int lowest=prices[0];
int m=0;
for(int i=1;i<prices.size();i++){
m=max(m,prices[i]-lowest);
lowest=min(lowest,prices[i]);
}
return m;
}
};
312. Burst Balloons(戳穿气球)
Given n balloons, indexed from 0 to n-1. Each balloon is painted with a number on it represented by array nums. You are asked to burst all the balloons. If the you burst balloon i you will get nums[left] * nums[i] * nums[right] coins. Here left and right are adjacent indices of i. After the burst, the left and right then becomes adjacent.
Find the maximum coins you can collect by bursting the balloons wisely.
思路:
记dp[l][r]表示扎破(l, r)范围内(不含边界l和r)所有气球获得的最大硬币数。
假设第k气球破了,num[k-1]和num[k+1]会变成相邻的,如果此时踩num[k-1]或者num[k+1],则都会受到另一个子整体的影响,这样的话,两个子问题就不独立,也就不能用分治了。
可以发现:
N1和N2相互独立 <=> k点是整体N中最后一个被踩破的气球。
也就是k点被踩破之前,N1和N2的气球都不会相互影响。于是我们就成功构造了子问题。因此分治加dp就可以对问题进行求解了。
写一下状态传递方程:
dp[left][right] = max{dp[left][right] , nums[left] * nums[i] * nums[right] + nums[left] * nums[i] + nums[i] * nums[right]};
其中 left<i<right
, dp[left][right]
即为当前子问题:第left和第right之间位置的气球的maxcoin。
l与r的跨度k是从2开始逐渐增大的。
当k = 2时,l = 0, r = 2; l = 1, r = 3; …… 当k = 3时,l = 0, r = 3; l = 1, r = 4;….. ….. 当k = n-1时,l=0,r=n-1。
此时的dp[0][n-1]即为所求。
如果用(n-1)*(n-1)矩阵形象解释的话,就是考察主对角线及以上部分。从dp[0][2]、dp[1][3]…那条斜线开始,不断给对角线赋值。最右上角的dp[0][n-1]即为所求。
class Solution {
public:
int maxCoins(vector<int>& nums) {
int arr[nums.size()+2];
//重新建立一个大小为n+2的空间
for(int i=1;i<nums.size()+1;++i) arr[i] = nums[i-1];
arr[0] = arr[nums.size()+1] = 1;
int dp[nums.size()+2][nums.size()+2]={};
int n = nums.size()+2;
//跨度k从2到n-1
for(int k=2;k<n;++k) {
//left从0到n-k
for(int left = 0;left<n-k;++left) {
//right是left+k
int right = left + k;
//对于dp[left][right],要根据最后破的气球的位置i,确定当前的maxCoins
for(int i=left+1;i< right; ++i) {
dp[left][right] = max(dp[left][right],arr[left]*arr[i]*arr[right] + dp[left][i] + dp[i][right]);
}
}
}
return dp[0][n-1];
}
};
64. 最小路径和
给定一个只含非负整数的m*n网格,找到一条从左上角到右下角的可以使数字和最小的路径。
每次只能向下或者向右移动一步。
思路:
构建一个m*n的矩阵,每个点表示从左上角到该位置的最小路径和。然后,递推式是
sum[i][j]=min(sum[i-1][j],sum[i][j-1])+grid[i][j];
值得注意的是,当i=0或j=0时,要考虑下边界情况。
PS:
如果是要求最大路径和,只要把递推式改成:
sum[i][j]=max(sum[i-1][j],sum[i][j-1])+grid[i][j];
class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
int m=grid.size();
int n=grid[0].size();
if(m==0 && n==0) return 0;
vector<vector<int>> sum(m,vector<int>(n,0));
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
if(i==0 && j==0)
{
sum[i][j]=grid[i][j];
continue;
}
if(i==0)
sum[i][j]=sum[i][j-1]+grid[i][j];
else if(j==0)
sum[i][j]=sum[i-1][j]+grid[i][j];
else
sum[i][j]=min(sum[i-1][j],sum[i][j-1])+grid[i][j];
}
}
return sum[m-1][n-1];
}
};
PSS:
这里用到了m*n的辅助数组sum,实际上如果允许改变原数组grid的话,只需要原地更新grid[i][j]就可以了,而不需要用到sum。
又或者,如果要用辅助空间,也不用开m*n,而是定义一个一维的大小为n的数组pre,加上一个cur变量,用来保存s[i][j-1]中的元素。
class Solution {
public:
int minPathSum(vector<vector<int>>& grid)
{
if (grid.empty())
return 0;
int rows = grid.size();
int cols = grid[0].size();
vector<int> pre(cols, grid[0][0]);
//保存s[i][j-1]中的元素
int cur = grid[0][0];
//根据第0行初始化pre
for (int i = 1; i < cols; ++i)
{
pre[i] = pre[i - 1] + grid[0][i];
}
//获得s[i][j]的最小值
for (int i = 1; i < rows; ++i)
{
cur = grid[i][0] + pre[0];
pre[0] = cur;
for (int j = 1; j < cols; ++j)
{
cur = min(cur, pre[j]) + grid[i][j];
pre[j] = cur;
}
}
return pre[cols - 1];
}
};
300. 最长上升子序列(Longest Increasing Subsequence)
求一个未排序序列中的最长上升子序列(不要求连续)
思路:
建立一个数组lol[n],初始化为1,用于存储到当前元素为止,以当前元素结尾的子序列的长度。当数组全部计算完毕后,找出其中的最大值,即为所求。
状态转移方程为:
lol[i]=max(lol[i],lol[j]+1);
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int n=nums.size(),i,j;
if(n<=1) return n;
int lol[n],maxx=0;
//给lol的所有元素赋初值1
for(i=0;i<n;i++) lol[i]=1;
for(i=1;i<n;i++) {
for(j=0;j<i;j++) {
if(nums[i]>nums[j])
lol[i]=max(lol[i],lol[j]+1);
}
maxx = max(maxx,lol[i]);
}
return maxx;
}
};
279. 和为n的最少的完全平方数
Given a positive integer n, find the least number of perfect square numbers (for example, 1, 4, 9, 16, …) which sum to n.
For example, given n = 12, return 3 because 12 = 4 + 4 + 4; given n = 13, return 2 because 13 = 4 + 9.
思路:
动态规划。
如果一个数x可以表示为一个任意数a加上一个平方数b∗b,也就是x=a+b∗b,那么能组成这个数x最少的平方数个数,就是能组成a最少的平方数个数再加上1(因为b∗b已经是平方数了)。
class Solution {
public:
int numSquares(int n) {
vector<int> dp(n+1,0);
//最坏情况,n全由1组成
for(int i=0;i<n+1;i++) dp[i]=i;
for(int a=0;a<=n;a++)
for(int b=1;a+b*b<=n;b++)
dp[a+b*b]=min(dp[a+b*b],dp[a]+1);//要么本身,要么加一个平方数
return dp[n];
}
};
357. 各位数字都不同的数
Given a non-negative integer n, count all numbers with unique digits, x, where 0 ≤ x < 10^n.
思路:
题目要找出 0≤ x < 10^n中各位数字都不相同的数的个数。要解这道题只需要理解两点:
- 设f(n)表示n位数字中各位都不相同的个数,则有
countNumbersWithUniqueDigits(n) = f(n)+……+f(2)+f(1) = f(n)+countNumbersWithUniqueDigits(n-1);
对于f(n),由于首位不能为0,之后n-1位可以选不重复的任意数字,所以总的可能性为
9*9*8*……
(n超过10则这样的数不存在);理解了以上两点,这道题就很好解出。
class Solution {
public:
int countNumbersWithUniqueDigits(int n) {
return f(n);
}
int f(int n){
if(n==0) return 1;
int num_n=0;
int temp=n;
//位数不能超过10位,否则没有满足条件的数
if(n>0 && n<10){
int c=9;
num_n=9;
n--;
while((n--)>0){
num_n *= (c--);
}
}
return num_n+f(temp-1);
}
};
392. 判断是否是子串(Java)
Given a string s and a string t, check if s is subsequence of t.
思路:
从头至尾考察字符串t。如果s是t的子串,那么在遍历t的过程中,一定能按顺序找到所有s的字符。
public class Solution {
public boolean isSubsequence(String s, String t) {
if (s.length()==0) {
return true;
}
int is = 0,it = 0;
while(it<t.length()){
if (s.charAt(is)==t.charAt(it))
{
is++;
if (is==s.length())
{
return true;
}
}
it++;
}
return false;
}
}
416. 平分数列
Given a non-empty array containing only positive integers, find if the array can be partitioned into two subsets such that the sum of elements in both subsets is equal.
思路:
用背包问题的思路来思考。首先:
- 数组的和必须要是偶数,否则无法划分。共计n个数,这里value和weight都设为等于nums[i]
- 将问题转化为背包问题,即取前i个数(物品),在背包体积为j的前提下,dp[i][j]的最大值
dp[i][j]=max{ dp[i-1][j], dp[i-1][j-weight[i]]+value[i] }
- 最后,如果dp[n][sum/2] 等于sum/2,就证明在使用了这n个数下,正好能加出一个sum/2,符合题意
在下面的具体实现中,没有采用二维数组dp[][],而是使用了简化了的一维数组dp[],效果是一样的:
class Solution {
public:
bool canPartition(vector<int>& nums) {
int length=nums.size();
if(length==0) return true;
if(length==1) return false;
int value[length];
int weight[length];
int sum=0;
for(int i=0;i<length;i++)
{
value[i]=nums[i];
weight[i]=nums[i];
sum += nums[i];
}
if(sum%2) return false;//如果sum是奇数,那么就不可能平分
int dp[sum/2+1]={0};
for(int i=0;i<length;i++)
for(int j=sum/2;j>=0;j--)
if(weight[i]<=j)
dp[j] = max(dp[j],value[i]+dp[j-weight[i]]);
return dp[sum/2]==(sum/2);
}
};
377. 组合的和IV(Java)
Given an integer array with all positive numbers and no duplicates, find the number of possible combinations that add up to a positive integer target.
Example:
nums = [1, 2, 3]
target = 4
The possible combination ways are:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)
Note that different sequences are counted as different combinations.
Therefore the output is 7.
思路:
记dp[i]表示和为i的组合的个数,则有dp[i]=dp[i-j1]+dp[i-j2]+....+dp[i-jn]+k
,其中j1,j2…jn是nums数组中比target小的数,而k则可以表示成:
如果存在j等于target,则k=1;否则k=0。
举个例子:
nums=[1,2,3],target=6, dp[1]=1, dp[2]=dp[1]+1, dp[3]=dp[2]+dp[1]+1, dp[4]=dp[3]+dp[2]+dp[1], dp[5]=dp[4]+dp[3]+dp[2], dp[6]=dp[5]+dp[4]+dp[3]
上面k的取值可以用设定dp[0]来实现,设dp[0]为1,然后状态转移方程可以写成:
ans[i]+=ans[i-nums[j]]; (nums[j]<=i)
完整代码如下:
public class Solution {
public int combinationSum4(int[] nums, int target) {
Arrays.sort(nums);
int[] ans=new int[target+1];
ans[0]=1;
for (int i = 1; i < ans.length; i++)
for (int j = 0; j < nums.length; j++)
if (nums[j]<=i)
ans[i]+=ans[i-nums[j]];
return ans[target];
}
}
375. 猜数字 II(Java)
猜数游戏,不过题目要求你猜错了就得付与你所猜的数目一致的钱,最后应求出保证你能赢的钱数(即求出保证你获胜的最少的钱数)。
思路:
在1-n个数里面,我们任意猜一个数(设为i),保证获胜所花的钱应该为 i + max(w(1 ,i-1), w(i+1 ,n)),这里w(x,y))表示猜范围在(x,y)的数保证能赢应花的钱,则我们依次遍历 1-n作为猜的数,求出其中的最小值即为答案,即最小的最大值问题。
public class Solution {
public int getMoneyAmount(int n) {
int[][] table = new int[n+1][n+1];
return DP(table, 1, n);
}
public int DP(int[][] t, int s, int e){
if(s >= e) return 0;
if(t[s][e] != 0) return t[s][e];
int res = Integer.MAX_VALUE;
for(int x=s; x<=e; x++){
int tmp = x + Math.max(DP(t, s, x-1), DP(t, x+1, e));
res = Math.min(res, tmp);
}
t[s][e] = res;
return res;
}
}
小偷问题 II
After robbing those houses on that street, the thief has found himself a new place for his thievery so that he will not get too much attention. This time, all houses at this place are arranged in a circle. That means the first house is the neighbor of the last one. Meanwhile, the security system for these houses remain the same as for those in the previous street.
Given a list of non-negative integers representing the amount of money of each house, determine the maximum amount of money you can rob tonight without alerting the police.
思路:
House Robber I的升级版. 因为第一个element 和最后一个element不能同时出现. 则分两次call House Robber I.
case 1: 不包括最后一个element.
case 2: 不包括第一个element.
两者的最大值即为全局最大值。
完整代码:
public class Solution {
public int rob(int[] nums) {
if(nums==null || nums.length==0) return 0;
if(nums.length==1) return nums[0];
if(nums.length==2) return Math.max(nums[0], nums[1]);
return Math.max(robsub(nums, 0, nums.length-2), robsub(nums, 1, nums.length-1));
}
private int robsub(int[] nums, int s, int e) {
int n = e - s + 1;
int[] d =new int[n];
d[0] = nums[s];
d[1] = Math.max(nums[s], nums[s+1]);
for(int i=2; i<n; i++) {
d[i] = Math.max(d[i-2]+nums[s+i], d[i-1]);
}
return d[n-1];
}
}
120. 三角数列的最小和(Java)
思路:
DFS方法的时间复杂度有点高,这里使用动态规划。从底向上,对于某行上的某元素,它所在的最小路径,肯定包含它和它下一行的两个相邻元素中的较小者。因此,可以从下往上更新一个数组,直到最上。此时,array[0]即为所求。
之所以要选择从下往上,是因为这样更新比较简单,而且只需要O(n)的空间复杂度。
public class Solution {
public int minimumTotal(List<List<Integer>> triangle) {
int Size=triangle.size();
if(Size==0) return 0;
ArrayList<Integer> array=(ArrayList)triangle.get(Size-1);
for(int i=Size-2;i>=0;i--) {
for(int j=0;j<i+1;j++) {
array.set(j,Math.min(array.get(j),array.get(j+1))+triangle.get(i).get(j));
}
}
return array.get(0);
}
}
55. 跳跃游戏(Java)
Given an array of non-negative integers, you are initially positioned at the first index of the array.
Each element in the array represents your maximum jump length at that position.
Determine if you are able to reach the last index.
For example:
A = [2,3,1,1,4], return true.
A = [3,2,1,0,4], return false.
思路:
用一个数 reach 表示能到达的最远下标,一步步走下去,如果发现在 reach 范围之内某处能达到的范围大于 reach,那么我们就用更大的范围来替换掉原先的 reach,这样一个局部的最优贪心策略,在全局看来也是最优的。
public class Solution {
public boolean canJump(int[] nums) {
int reach = nums[0];
for(int i = 1; i < nums.length && reach >= i; i++)
if(i + nums[i] > reach)
reach = i + nums[i];
if(reach >= nums.length-1) return true;
return false;
}
}
91. 解码的不同方式(Java)
A message containing letters from A-Z is being encoded to numbers using the following mapping:
‘A’ -> 1
‘B’ -> 2
…
‘Z’ -> 26
Given an encoded message containing digits, determine the total number of ways to decode it.
For example,
Given encoded message “12”, it could be decoded as “AB” (1 2) or “L” (12).
The number of ways decoding “12” is 2.
思路:
类似爬楼梯问题,但要加很多限制条件。
定义数组number,number[i]表示:字符串s[0..i-1]可以有number[i]种解码方法。
有以下几种情况:
- 当前数字为0,且前一个数字是1或2,则有
number[i] = number[i-2]
,即这两位只有组合成10或20这一种情形,方式数量没有增加; - 当前数字为0,且前一个数字既不是1也不是2,这是不合法的,所以
number[i] = 0
; - 当前数字不为0,且当前数和前一个数组成的两位数在10和26之间(前一位数不应该是0),则当前数既可以独立,也可以和前一个合起来,所以有
number[i] = number[i-1]+number[i-2]
; - 当前数字不为0,且当前数和前一个数组成的两位数不落在10和26之间,说明当前数只能独立,没有其他方式,所以
number[i] = number[i-1]
。
public class Solution {
public int numDecodings(String s) {
if(s==null || s.length()==0) {
return 0;
}
if(s.charAt(0)=='0') {
return 0;
}
int [] number = new int[s.length() + 1];
number[0] = 1;
number[1] = 1;
int tmp1,tmp2;
for(int i=2;i<=s.length();i++) {
//当前这个数
tmp1 = Integer.valueOf(s.substring(i-1,i));
//当前数字和前一个数字组成的两位数
tmp2 = Integer.valueOf(s.substring(i-2,i));
if(tmp1==0 && (tmp2==10 || tmp2==20)) {
number[i] = number[i-2];
continue;
}
if(tmp1==0 && (tmp2!=10 && tmp2!=20)) {
continue;
}
if(tmp1!=0 && (tmp2>10 && tmp2<=26)) {
number[i] = number[i-1]+number[i-2];
continue;
}
else {
number[i] = number[i-1];
}
}
return number[s.length()];
}
}
363. 和不超过K的子矩阵的最大和
Given a non-empty 2D matrix matrix and an integer k, find the max sum of a rectangle in the matrix such that its sum is no larger than k.
Example:
Given matrix = [
[1, 0, 1],
[0, -2, 3]
]
k = 2
The answer is 2. Because the sum of rectangle [[0, 1], [-2, 3]] is 2 and 2 is the max number no larger than k (k = 2).
思路:
乍一看题目蛮复杂的,如果把题目从二维降到一维该如何处理呢?遍历一维数组,用curSum[j]
表示位置j之前所有数组元素之和,依次将curSum
存入set中,遍历到位置j时,如果j之前存在位置i满足curSum[j] - curSum[i] <= K
,也就是说以位置j结尾的序列有满足条件不大于K的,那么在set中查找不小于curSum[j] - K
的数,查找到的位置即是以j结尾,累加和不大于K的最大序列的开始位置i,最后比较并更新最大序列和;如果不存在满足curSum[j] - curSum[i] <= K
的位置i,表明以位置j结尾的所有序列相加和均大于K。遍历整个一维数组即可得到最终结果。
由于原数组是二维数组,所以需要用两层的for循环将二维数组转换成一维数组。
完整的代码如下:
class Solution {
public:
int maxSumSubmatrix(vector<vector<int>>& matrix, int k) {
if (matrix.empty() || matrix[0].empty()) return 0;
int m = matrix.size(), n = matrix[0].size(), res = INT_MIN;
//从列出发进行考察
for (int i = 0; i < n; i++) {
vector<int> sum(m, 0);
for (int j = i; j < n; j++) {
for (int k = 0; k < m; k++) {
sum[k] += matrix[k][j];//sum[k] 存的是第k行中,从第i列开始,到第j列为止的累加和
}
int curSum = 0, curMax = INT_MIN;
set<int> s;
s.insert(0);
for (auto a : sum) {
curSum += a;
auto it = s.lower_bound(curSum - k);
//如果找到了
if (it != s.end()) curMax = max(curMax, curSum - *it);
s.insert(curSum);
}
res = max(res, curMax);
}
}
return res;
}
};
最大子矩阵和
求一个M*N的矩阵的最大子矩阵和。
比如在如下这个矩阵中:
0 -2 -7 0
9 2 -6 2
-4 1 -4 1 -1 8 0 -2
拥有最大和的子矩阵为:
9 2
-4 1 -1 8
其和为15。
思路:
假设这个最大子矩阵的维数是一维,要找出最大子矩阵, 原理与求“最大子段和问题” 是一样的。最大子段和问题的递推公式是
b[j]=max(b[j-1]+a[j], a[j])
b[j] 指的是从0开始到j的最大子段和。
例子:
假设原始矩阵为:[9, 2, -6, 2], 那么b[] = {9, 11, 5, 7}, 那么最大字段和为11, 如果找最大子矩阵的话,那么这个子矩阵是 [9, 2]
求最大子段和的代码如下:
public int maxSubsequence(int[] array) {
if (array.length == 0) {
return 0;
}
int max = Integer.MIN_VALUE;
int[] maxSub = new int[array.length];
maxSub[0] = array[0];
for (int i = 1; i < array.length; i++) {
maxSub[i] = (maxSub[i-1] > 0) ? (maxSub[i-1] + array[i]) : array[i];
if (max < maxSub[i]) //尝试更新最大子段和
max = maxSub[i];
}
return max;
}
为了找出在原始矩阵里的最大子矩阵,我们要遍历所有的子矩阵的可能情况,也就是说,我们要考虑这个子矩阵有可能只有1行,2行,。。。到n行。而在每一种情况下,我们都要把它所对应的矩阵部分上下相加才求最大子矩阵(局部)。
比如,假设子矩阵是一个3*k的矩阵,而且,它的一行是原始矩阵的第二行,那么,我们就要在
9 2 -6 2
-4 1 -4 1 -1 8 0 -2
里找最大的子矩阵。
如果把它上下相加,我们就变成了 4, 11, -10,1
, 从这个数列里可以看出,在这种情况下,最大子矩阵是一个3*2的矩阵,最大和是15.
为了能够在原始矩阵里很快得到从 i 行到 j 行 的上下值之和,我们这里用到了一个辅助矩阵,它是原始矩阵从上到下加下来的。
假设原始矩阵是matrix, 它每一层上下相加后得到的矩阵是total,那么我们可以通过如下代码实现:
其中,total[i][j]
记录的是第j列上,从第0行到第i行的元素之和。
int[][] total = matrix;
for (int i = 1; i < matrix[0].length; i++)
for (int j = 0; j < matrix.length; j++)
total[i][j] += total[i-1][j];
如果我们要求第 i 行到第 j 行之间上下值的和,我们可以通过total[j][k] – total[i-1][k] 得到, k 的范围从1 到 matrix[0].length – 1。
有了这些知识点,我们只需要在所有的情况下,把它们所对应的局部最大子矩阵进行比较,就可以得到全局最大的子矩阵。代码如下:
public int subMaxMatrix(int[][] matrix) {
int[][] total = matrix;
for (int i = 1; i < matrix[0].length; i++) //初始化total矩阵
for (int j = 0; j < matrix.length; j++)
total[i][j] += total[i-1][j];
int maximum = Integer.MIN_VALUE;
for (int i = 0; i < matrix.length; i++) {
for (int j = i; j < matrix.length; j++) {
//result 保存的是从 i 行 到第 j 行 所对应的矩阵上下值的和
int[] result = new int[matrix[0].length];
for (int f = 0; f < matrix[0].length; f++)
{
if (i == 0) {
result[f] = total[j][f];
} else {
result[f] = total[j][f] - total[i - 1][f];
}
}
//调用计算最大子段和的函数
int maximal = maxSubsequence(result);
if (maximal > maximum) //试图更新最大矩阵和的结果
maximum = maximal;
}
}
return maximum;
}
环形公路加油站
环形公路上有n个加油站,第i个加油站的油量用gas[i]来表示,你有如下的一辆车:
它的油缸是无限量的,初始是空的;
它从第i个加油站到第i+1个加油站消耗油量为cost[i]。
现在你可以从任意加油站开始,路过加油站可以不断的加油,问是否能够走完环形路。如果可以返回开始加油站的编号,如果不可以返回-1。
思路:
开辟一个长度为2*n的数组point,记录gas[i]-cost[i](环转化为线性)。
从头到尾遍历0~n个站点,如果到达当前站点i时的汽油累加和小于0,则说明到不了这个站点i。起点从当前站点i开始算起,直到再次到达第n个站点时汽油累加和还大于等于0,则站点i即为所求。
class Solution {
public:
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
int n = gas.size();
int point[2*n]={0};
for(int i=0;i<2*n;i++)
point[i]=gas[i%n]-cost[i%n];
int begin=0;
int sum=0;
for(int i=0;i<2*n;i++)
{
if(sum<0)
{
begin=i;
sum=point[i];
}
else
{
sum+=point[i];
if(i-begin == n)
return begin;
}
}
return -1;
}
};