贪心的基本概念
所谓贪心算法,是指在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的仅是在某种意义上的局部最优解。
贪心算法没有固定的算法框架,算法设计的关键是贪心策略的选择。必须注意的是,贪心算法不是对所有问题都能得到整体最优解,选择的贪心策略必须具备无后效性,即某个状态以后的过程不会影响以前的状态,只与当前状态有关。
所以对所采用的贪心策略一定要仔细分析其是否满足无后效性。
贪心策略适用的前提是:局部最优策略能导致产生全局最优解。
实际上,贪心算法适用的情况很少。一般,对一个问题分析是否适用于贪心算法,可以先选择该问题下的几个实际数据进行分析,就可做出判断。
基于贪心算法的几类区间覆盖问题
区间完全覆盖问题
问题描述:
给定一个长度为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;