算法题4

1. 课程表 II

现在你总共有 n 门课需要选,记为 0 到 n-1。

在选修某些课程之前需要一些先修课程。 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们: [0,1]

给定课程总量以及它们的先决条件,返回你为了学完所有课程所安排的学习顺序。

可能会有多个正确的顺序,你只要返回一种就可以了。如果不可能完成所有课程,返回一个空数组。

输入: 4, [[1,0],[2,0],[3,1],[3,2]]
输出: [0,1,2,3] or [0,2,1,3]
解释: 总共有 4 门课程。要学习课程 3,你应该先完成课程 1 和课程 2。并且课程 1 和课程 2 都应该排在课程 0 之后。
因此,一个正确的课程顺序是 [0,1,2,3] 。另一个正确的排序是 [0,2,1,3] 。

分析:
构建一个edges与redges邻接矩阵,若edges[i]为空则表示i可以学习加入队列queue中。依次从队列queue中取出i,放入result中,然后再redges中找出依赖i的节点j,在edges[j]中删除i,若此时edges[j]为空,则将其加入queue。直到queue为空,此时result.size()=n节点数,表示可以学习完,若不等于表示不可学习。

vector<int> findOrder(int numCourses, vector<pair<int, int>>& prerequisites) {//课程表 II
	vector<unordered_set<int>> edges(numCourses, unordered_set<int>());
	vector<unordered_set<int>> redges(numCourses, unordered_set<int>());
	for (auto edge : prerequisites) {
		edges[edge.first].insert(edge.second);
		redges[edge.second].insert(edge.first);
	}
	queue<int> qe;
	vector<int> result;
	for (int i = 0; i < numCourses; ++i) {
		if (edges[i].size() == 0) {
			qe.push(i);
		}
	}
	while (!qe.empty()){
		int tem = qe.front();
		qe.pop();
		result.push_back(tem);
		for (auto end : redges[tem]) {
			edges[end].erase(tem);
			if (edges[end].size() == 0) {
				qe.push(end);
			}
		}
	}
	if (result.size() == numCourses) {
		return result;
	}
	else {
		return {};
	}
}

2. 二叉树展开为链表

给定一个二叉树,原地将它展开为链表。

例如,给定二叉树
1
/ \
2 5
/ \ \
3 4 6

1
\
2
\
3
\
4
\
5
\
6

分析:

后序遍历,先找到最左节点,然后将该节点的左孩子放到右孩子处,将右孩子放到左孩子的右孩子。在处理右孩子时,需要找到左孩子最右的节点。

void flatten(TreeNode* root) {//二叉树展开为链表
	if (root == NULL) return;
	flatten(root->left);
	flatten(root->right);
	TreeNode *tem = root->right;
	root->right = root->left;
	root->left = NULL;
	while (root->right)
	{
		root = root->right;
	}
	root->right = tem;
}

3. 解码方法

一条包含字母 A-Z 的消息通过以下方式进行了编码:

‘A’ -> 1
‘B’ -> 2

‘Z’ -> 26
给定一个只包含数字的非空字符串,请计算解码方法的总数。

分析:动态规划。
i位置判断能跟i-1组成字母,dp[i]+=dp[i-2]或1
i位置判断能自己组成字母,dp[i]+=dp[i-1]或1

bool isLetter(string s) {
	int num = atoi(s.c_str());
	if (s.size() == 2 && s[0] == '0') return false;
	if (num > 26 || num < 1) {
		return false;
	}
	else {
		return true;
	}
}
int numDecodings(string s) {//解码方法
	int n = s.size();
	if (s[0] == '0') return 0;
	vector<int> help(n, 0);
	for (int i = 0; i < n; ++i) {
		if (i - 1 >= 0) {
			string cur = "";
			cur += s[i - 1];
			cur += s[i];
			if (isLetter(cur)) {
				if (i - 2 >= 0) {
					help[i] += help[i - 2];
				}
				else {
					help[i] = 1;
				}
			}
		}
		string cur = "";
		cur += s[i];
		if (isLetter(cur)) {
			if (i - 1 >= 0) {
				help[i] += help[i - 1];
			}
			else {
				help[i] = 1;
			}
		}
	}
	return help[n - 1];
}

4. 单词拆分

给定一个非空字符串 s 和一个包含非空单词列表的字典 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。

说明:

拆分时可以重复使用字典中的单词。
你可以假设字典中没有重复的单词。

输入: s = “catsandog”, wordDict = [“cats”, “dog”, “sand”, “and”, “cat”]
输出: false

输入: s = “applepenapple”, wordDict = [“apple”, “pen”]
输出: true
解释: 返回 true 因为 “applepenapple” 可以被拆分成 “apple pen apple”。
注意你可以重复使用字典中的单词。

分析:
动态规划,使用一个help[0]=-1数组,保存到当前位置i能够拆分的i。若i与help中的任意一个index后的string在wordDict中,则将i加入help。若help最后一位为有n-1,则返回true,否则false。

bool wordBreak(string s, vector<string>& wordDict) {//单词拆分
	int n = s.size();
	vector<int> help;
	help.push_back(-1);
	for (int i = 0; i < n; ++i) {
		int flag = 0;
		for (auto index : help) {
			string cur = s.substr(index + 1, i - index);
			for (auto key : wordDict) {
				if (key == cur) {
					help.push_back(i);
					flag = 1;
					break;
				}
			}
			if (flag == 1) {
				break;
			}
		}
	}
	if (*(help.end() - 1) == n - 1) return true;
	return false;
}

5. 乘积最大子序列

给定一个整数数组 nums ,找出一个序列中乘积最大的连续子序列(该序列至少包含一个数)。

输入: [-2,0,-1]
输出: 0
解释: 结果不能为 2, 因为 [-2,-1] 不是子数组。

输入: [2,3,-2,4]
输出: 6
解释: 子数组 [2,3] 有最大乘积 6。

分析:
动态规划,但是正负号很难处理,因此用两个dp,一个保存当前位置乘积最小子序列(可能是自身),另一个保存当前位置乘积最大子序列(可能是自身)。每次选出当前位置与当上一位置dp最大最小值相乘结果的最大最小值保存。

int maxProduct(vector<int>& nums) {
	int lastMax = nums[0];
	int lastMin = nums[0];
	int result = nums[0];
	for (int i = 1; i < nums.size(); ++i) {
		int temMax = lastMax;
		int temMin = lastMin;
		lastMax = std::max(nums[i], std::max(nums[i] * temMax, nums[i] * temMin));
		lastMin = std::min(nums[i], std::min(nums[i] * temMax, nums[i] * temMin));
		result = std::max(result, lastMax);
	}
	return result;
}

6. 最大正方形

在一个由 0 和 1 组成的二维矩阵内,找到只包含 1 的最大正方形,并返回其面积。

输入:
1 0 1 0 0
1 0 1 1 1
1 1 1 1 1
1 0 0 1 0
输出: 4

分析:动态规划,当matrix[i][j]==1时,dp[i][j]为min{dp[i-1][j],dp[i][j-1]dp[i-1][j-1]}+1。上或左或左上中的最小值+1。

int maximalSquare(vector<vector<char>>& matrix) {//最大正方形
	int n = matrix.size();
	if (n == 0) return 0;
	int m = matrix[0].size();
	vector<vector<int>> dp(n + 1, vector<int>(m + 1, 0));
	int result = 0;
	for (int i = 1; i <= n; ++i) {
		for (int j = 1; j <= m; ++j) {
			if (matrix[i-1][j-1] == '1') {
				dp[i][j] = std::min(dp[i - 1][j], std::min(dp[i][j - 1], dp[i - 1][j - 1])) + 1;
				if (dp[i][j] > result) {
					result = dp[i][j];
				}
			}
		}
	}
	return result*result;
}

7. 只有两个键的键盘

最初在一个记事本上只有一个字符 ‘A’。你每次可以对这个记事本进行两种操作:

Copy All (复制全部) : 你可以复制这个记事本中的所有字符(部分的复制是不允许的)。
Paste (粘贴) : 你可以粘贴你上一次复制的字符。
给定一个数字 n 。你需要使用最少的操作次数,在记事本中打印出恰好 n 个 ‘A’。输出能够打印出 n 个 ‘A’ 的最少操作次数。

输入: 3
输出: 3
解释:
最初, 我们只有一个字符 ‘A’。
第 1 步, 我们使用 Copy All 操作。
第 2 步, 我们使用 Paste 操作来获得 ‘AA’。
第 3 步, 我们使用 Paste 操作来获得 ‘AAA’。

分析:这里没有插入一个A的操作。当想写i个A时,只能找之前能够i%j==0的j,然后复制粘贴,粘贴的次数为i/j-1,加上一次复制为i/j。

int minSteps(int n) {//只有两个键的键盘
	if (n == 1) return 0;
	if (n < 6) return n;
	vector<int> dp(n + 1, n);
    dp[1]=0;
	for (int i = 2; i <= n; ++i) {
		for (int j = 1; j <= (i / 2); ++j) {
			if(i%j==0){
				dp[i] = std::min(dp[i], dp[j] + i / j);
			}
		}
	}
	return dp[n];
}

8. 最佳买卖股票时机含冷冻期

给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。

设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):

你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。

输入: [1,2,3,0,2]
输出: 3
解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]

分析:本题共有三个状态,以后的题注意区分状态。[buy,sell,rest]。可以写出状态转移方程

buy[i] = max{rest[i-1]-price[i], buy[i-1]}  
sell[i] = max{buy[i-1]+price[i], sell[i-1]}  
rest[i] = max{sell[i-1], rest[i-1]}

又由于冷却状态时,rest[i] = sell[i-1]由此可简化上式

buy[i] = max{sell[i-2]-price[i], buy[i-1]}  
sell[i] = max{buy[i-1]+price[i], sell[i-1]}  
int maxProfit(vector<int>& prices) {//最佳买卖股票时机含冷冻期
	int n = prices.size();
	if (n < 2) return 0;
	if (n == 2) return std::max(0, prices[1] - prices[0]);
	vector<int> buy(n, 0);
	vector<int> sell(n, 0);
	buy[0] = -prices[0];
	buy[1] = std::max(buy[0], -prices[1]);
	sell[1] = std::max(sell[0], buy[0] + prices[1]);
	for (int i = 2; i < n; ++i) {
		buy[i] = std::max(buy[i - 1], sell[i - 2] - prices[i]);
		sell[i] = std::max(buy[i - 1] + prices[i], sell[i - 1]);
	}
	return sell[n - 1];
}

9. 戳气球

有 n 个气球,编号为0 到 n-1,每个气球上都标有一个数字,这些数字存在数组 nums 中。

现在要求你戳破所有的气球。每当你戳破一个气球 i 时,你可以获得 nums[left] * nums[i] * nums[right] 个硬币。 这里的 left 和 right 代表和 i 相邻的两个气球的序号。注意当你戳破了气球 i 后,气球 left 和气球 right 就变成了相邻的气球。

求所能获得硬币的最大数量。

说明:

你可以假设 nums[-1] = nums[n] = 1,但注意它们不是真实存在的所以并不能被戳破。
0 ≤ n ≤ 500, 0 ≤ nums[i] ≤ 100

输入: [3,1,5,8]
输出: 167
解释: nums = [3,1,5,8] --> [3,5,8] --> [3,8] --> [8] --> []
coins = 3*1*5 + 3*5*8 + 1*3*8 + 1*8*1 = 167

分析:
每一次最后一次运算为为i乘以最左边leftMax与最右边rightMax的值。因此可以使用逆序来将nums[]分为左右两边来处理。
用动态规划+分而治之来处理,dp[left][right]等于left到
right之间任意一个i与left和right相乘的值加上i到dp[left][i]+dp[i][right]。因此先从最小间距2开始一直到n-1进行如上迭代。

10. 最大的二维点集,右上角没有点的点集。

P为给定的二维平面整数点集。定义 P 中某点x,如果x满足 P 中任意点都不在 x 的右上方区域内(横纵坐标都大于x),则称其为“最大的”。求出所有“最大的”点的集合。(所有点的横坐标和纵坐标都不重复, 坐标轴范围在[0, 1e9) 内)

如下图:实心点为满足条件的点的集合。请实现代码找到集合 P 中的所有 ”最大“ 点的集合并输出。
微信截图_20180909215343.png

分析:可以将x按照降序排序,将y按照升序排序。然后从前往后依次比较y,若 y i &gt; max ⁡ y_i&gt;\max yi>max则满足条件,然后令 max ⁡ = y i \max=y_i max=yi

11. 判断一个数字是否为回文数

每次取第一位跟最后一位比较是否相同,采用除或取余操作。

12. 给定N个整数组成的序列,编程计算该序列的最优M段分割,使M段子序列的和的最大值达到最小

动态规划:
构造一个矩阵dp[n][m],dp[i][j]表示索引i及其之前的数分成j组的最大值。因此有如下状态转移方程:
j = 1 : d p [ i ] [ 1 ] = d p [ i − 1 ] [ 1 ] + n u m [ i ] j ≠ 1 : d p [ i ] [ j ] = m i n { m a x { d p [ i ] [ 1 ] − d p [ k ] [ 1 ] , d p [ k ] [ j − 1 ] } } j=1: dp[i][1] = dp[i-1][1] + num[i]\\ j\neq 1: dp[i][j] = min\{max\{dp[i][1] - dp[k][1],dp[k][j-1]\}\} j=1:dp[i][1]=dp[i1][1]+num[i]j̸=1:dp[i][j]=min{max{dp[i][1]dp[k][1],dp[k][j1]}}
dp[i][1]-dp[k][1]可以理解为将k+1~i分成第j组,然后与前k个数组成j-1组的最大值比较取大。然后遍历所有可能的k,取最小值。

13. LRU缓存机制

运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制。它应该支持以下操作: 获取数据 get 和 写入数据 put 。

获取数据 get(key) - 如果密钥 (key) 存在于缓存中,则获取密钥的值(总是正数),否则返回 -1。
写入数据 put(key, value) - 如果密钥不存在,则写入其数据值。当缓存容量达到上限时,它应该在写入新数据之前删除最近最少使用的数据值,从而为新的数据值留出空间。

LRUCache cache = new LRUCache( 2 /* 缓存容量 */ );
cache.put(1, 1);
cache.put(2, 2);
cache.get(1); // 返回 1
cache.put(3, 3); // 该操作会使得密钥 2 作废
cache.get(2); // 返回 -1 (未找到)
cache.put(4, 4); // 该操作会使得密钥 1 作废
cache.get(1); // 返回 -1 (未找到)
cache.get(3); // 返回 3
cache.get(4); // 返回 4

分析:
为快速添加和删除,我们需要使用链表来设计cache,链表中从头到尾的数据顺序依次是:(最近访问)->…(最旧访问);
因为是最近访问的在链表的最前,我们使用list来保存key值,而使用hashmap来保存(key,value)对即可,同时需要对value进行改造,因为我们还需要将每一个value与list中的key结点进行绑定,组成pair,以便每次操作进行list的最近访问的调整;

添加节点put(key, value):

判断hashmap中是否存在key节点,如果存在则调整List,将key置为头部(最近访问),更新hashmap指向key节点的指针;
如果不存在key值对,则判断capacity是否满容,若不满容量则直接在list和hashmap中加入新节点(list在表头添加);否则则删除list末尾key和hashmap中的对应值对,再进行添加。
新节点插入到表头即可,时间复杂度O(1)。

查找节点get(key):

在hashmap中查询,若不存在则返回-1;
若存在,则调整list位置更新hashmap对应指针,返回key对应的value;
每次节点被查询到时,将节点移动到链表头部,时间复杂度O(1)。

class LRUCache {
public:
	LRUCache(int capacity) {
		_capacity = capacity;
	}

	int get(int key) {
		auto it = cache.find(key);
		if (it == cache.end()) {//不在缓存中
			return -1;
		}
		else {//在缓存中
			adjust(it);//更新key在链表中的位置
			return it->second.first;;
		}
	}

	void put(int key, int value) {//判断是否在,若不在,判断是否满,如果满了删了再加。若在,则提前
		auto it = cache.find(key);
		if (it == cache.end()) {//不在
			if (cache.size() < _capacity) {//不满直接加
				used.push_front(key);
				valListPair tem;
				tem.first = value;
				tem.second = used.begin();
				cache[key] = tem;
			}
			else {
				cache.erase(used.back());//map可以直接删key
				used.pop_back();
				used.push_front(key);
				cache[key] = { value,used.begin() };
			}
		}
		else{//key在缓存中,只需要调整位置
			adjust(it);//将key调整到链表的最前端
			it->second.first = value;//更新key对应的value
		}
	}
private:
	int _capacity;
	typedef pair<int, list<int>::iterator> valListPair;
	typedef unordered_map<int, valListPair> keyValMap;
	list<int> used;
	keyValMap cache;
	void adjust(keyValMap::iterator it) {//将命中的key移到链表的最前面
		int key = it->first;
		used.erase(it->second.second);
		used.push_front(key);
		it->second.second = used.begin();
	}
};

14. 接雨水

给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
微信截图_20181017220404.png
上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。 感谢 Marcos 贡献此图。

分析:设计左右指针lindex,rindex,保存左右指针遇到过的最大值lmax,rmax。左右指针同时向中间移动,若lmax<rmax,water = lmax-high[lindex];lindex++,否则water=rmax-high[rindex];rindex–。

int trap(vector<int>& height) {//接雨水
	if (height.size() <= 1) return 0;
	int lindex = 0;
	int rindex = height.size() - 1;
	int lmax = 0;
	int rmax = 0;
	int result = 0;
	while (lindex<=rindex)
	{
		lmax = std::max(height[lindex], lmax);
		rmax = std::max(height[rindex], rmax);
		if (lmax <= rmax) {
			result += lmax - height[lindex];
			lindex++;
		}
		else {
			result += rmax - height[rindex];
			rindex--;
		}
	}
	return result;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值