1.试比较分治算法、贪心算法与动态规划三者的不同点与相似点?
不同点:
分治算法是将一个较为复杂的问题划分为若干个子问题,然后分别求解这些子问题,最后将子问题的解合并成原问题的解;其每个子问题之间相互独立,不会相互影响;故分治算法一般解决可以被分解为相互独立的子问题的问题,如归并排序、快速排序等。
贪心算法的每一步都选择当前状态下的最优解,而不考虑未来的影响,求解的是局部最优的,不一定能够得到全局最优解;故一般使用贪心算法解具有选择性质的问题,即每一步的最优解可以导致最终结果的最优解。
动态规划也是将问题分解为若干个子问题,但其每个子结构并非孤立,在求解每个子问题的过程中,它会通过记录已经解决过的子问题的解来避免重复计算,从而节省时间;故动态规划通常适用于那些具有最优子结构性质和重叠子问题性质的问题,即问题的最优解可以由其子问题的最优解推导而来,且问题中存在重叠的子问题。
相同点:
三个算法的核心思想都包含将问题分解为若干个规模较小的子问题,然后递归地求解这些子问题;
通常都采用自顶向下的递归或迭代的方式进行求解;核心是子问题的分解后求解。
代码分析:
如对计算Fibonacci数时,
采用分治:即简单递归;
int fibonacci(int n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
贪心算法不能够得到正确的结果,因此不适用于这个问题;
采用动态规划:则大大减少计算量,因其是典型的的重叠子问题模板
#define Max 100
int Fib_1(int n)
{
int i;
int a[Max];
a[0] = 1;
a[1] = 1;
for (i = 2; i <= n; i++)
{
a[i] = a[i - 1] + a[i - 2];
}
return a[n-1];
}
详细的区别:
分治算法适合解决如下问题:
- 排序问题:归并排序、快速排序都是基于分治算法的排序算法。
- 查找问题:k分查找。
- 最大子数组和问题。
- 矩阵乘法: Strassen 算法。
以排序算法和最大子数组和算法为例:
归并排序:
#include <iostream>
using namespace std;
const int N = 10010;
int num[N];//原数组
int tem[N];//临时数组
void merge(int low,int mid,int high)//合并函数
{
int i = low,j = mid + 1,k = low;//i、j指向需要合并的两个序列的首位置
while (i <= mid && j <= high)//i、j指针都不超过合并序列的最后位置
{
//将较小数存储在temp之中,并且后移指针
if (num[i] < num[j]) tem[k++] = num[i++];
else tem[k++] = num[j++];
}
while (i <= mid) tem[k++] = num[i++];//左半序列还有元素
while (j <= high) tem[k++] = num[j++];//右半序列还有元素
for (int i = low;i <= high;i++) num[i] = tem[i];//拷贝元素
}
void merge_sort(int l,int r)//递归设计:排序函数
{
if (l >= r) return;//递归出口:分解至一个数
int mid = (l + r) / 2;
merge_sort(l,mid);//归并排序左半序列
merge_sort(mid + 1,r);//归并排序右半序列
merge(l,mid,r);//将左右合并
}
int main()
{
int n;
cin >> n;
for (int i = 1;i <= n;i++) cin >> num[i];
merge_sort(1,n);//对第1个位置到第n个位置进行归并排序
for (int i = 1;i <= n;i++) cout << num[i] << " ";
return 0;
}
快速排序:
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 10010;
int a[N];
int n;
void quick_sort(int l,int r)
{
if (l >= r) return;
int temp = a[l];//保存基准
int i = l,j = r;
while (i != j)
{
while (a[j] >= temp && i < j) j--;//j指针从右往左试探
while (a[i] <= temp && i < j) i++;//i指针从左往右试探
if (i < j) swap(a[i],a[j]);//当i、j指针都停下后,交换元素位置
}
a[l] = a[i];//i == j退出循环,与基准元素交换位置
a[i] = temp;//第一次快排结束,基准元素更新为temp,基准位置更新为i,再继续排序
quick_sort(l,i - 1);//对基准左边快速排序
quick_sort(i + 1,r);//对基准右边快速排序
}
int main()
{
cin >> n;
for (int i = 1;i <= n;i++) cin >> a[i];
quick_sort(1,n);//对第1个元素到第n个元素进行排序
for (int i = 1;i <= n;i++) cout << a[i] << " ";
return 0;
}
例:
最大子数组和?
#include <vector>
#include <climits>
using namespace std;
int maxSubArraySum(vector<int>& nums, int low, int high) {
if (low == high) {
return nums[low];
}
int mid = low + (high - low) / 2;
// 左边子数组的最大子数组和
int leftMax = maxSubArraySum(nums, low, mid);
// 右边子数组的最大子数组和
int rightMax = maxSubArraySum(nums, mid + 1, high);
// 跨越中间点的最大子数组和
int crossMax = maxCrossingSum(nums, low, mid, high);
return max(max(leftMax, rightMax), crossMax);
}
int maxSubArray(vector<int>& nums) {
int n = nums.size();
if (n == 0) return 0;
return maxSubArraySum(nums, 0, n - 1);
}
贪心算法思想简洁,若使用数学归纳法得证由部分最优可以得到全局最优,则可以较为快速的解决问题。
可以解决以下问题:
- 活动安排问题:给定一组活动,每个活动有一个开始时间和结束时间,目标是安排最多的活动而不发生冲突。
- 背包问题:给定几个限界函数,在其限制下完成算法设计,一般会借助优先队列实现。
- 子段和、递增/递减子序列:判断数列的最长/最短递增/递减子序列长度。
- 最小生成树问题:Prim 算法和 Kruskal 算法都是基于贪心思想的最小生成树算法。
代码:
活动安排:
```cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
struct Activity {
int start;
int finish;
};
bool compare(Activity a, Activity b) {
return (a.finish < b.finish);
}
void printMaxActivities(vector<Activity>& activities) {
int n = activities.size();
sort(activities.begin(), activities.end(), compare);
cout << "Following activities are selected:\n";
int i = 0;
cout << "(" << activities[i].start << ", " << activities[i].finish << "), ";
for (int j = 1; j < n; j++) {
if (activities[j].start >= activities[i].finish) {
cout << "(" << activities[j].start << ", " << activities[j].finish << "), ";
i = j;
}
}
}
int main() {
vector<Activity> activities = {{1, 4}, {3, 5}, {0, 6}, {5, 7}, {3, 8}, {5, 9}, {6, 10}, {8, 11}, {8, 12}, {2, 13}, {12, 14}};
printMaxActivities(activities);
return 0;
}
最长递增子序列:
int dp[1005][2];
int wiggleMaxLength(vector<int>& nums) {
memset(dp, 0, sizeof dp);
dp[0][0] = dp[0][1] = 1;
for (int i = 1; i < nums.size(); ++i) {
dp[i][0] = dp[i][1] = 1;
for (int j = 0; j < i; ++j) {
if (nums[j] > nums[i]) dp[i][1] = max(dp[i][1], dp[j][0] + 1);
}
for (int j = 0; j < i; ++j) {
if (nums[j] < nums[i]) dp[i][0] = max(dp[i][0], dp[j][1] + 1);
}
}
return max(dp[nums.size() - 1][0], dp[nums.size() - 1][1]);
}
};
动态规划适合解决重叠子问题,如以下几类问题:
- 最长递增子序列问题:给定一个序列,找到其中的最长递增子序列。
- 背包问题:多限制条件的背包问题。
- 最短路径问题:寻找图中两个顶点之间的最短路径。
- 编辑距离问题:通过插入、删除、替换操作将一个字符串转换为另一个字符串所需的最少次数。
代码:
距离编辑:
int minDistance(string word1, string word2) {`
vector<vector<int>> dp(word1.size() + 1, vector<int>(word2.size() + 1, 0));
for (int i = 0; i < dp.size(); i++) {
dp[i][0] = i;
}
for (int j = 0; j < dp[0].size(); j++) {
dp[0][j] = j;
}
for (int i = 1; i < dp.size(); i++) {
for (int j = 1; j < dp[i].size(); j++) {
dp[i][j] = min(dp[i - 1][j - 1], min(dp[i - 1][j], dp[i][j - 1])) + 1;
if (word1[i - 1] == word2[j - 1]) {
dp[i][j] = min(dp[i][j], dp[i - 1][j - 1]);
}
}
}
return dp.back().back();
}
最长递增子序列:
int wiggleMaxLength(vector<int>& nums) {
int n = nums.size();
if (n < 2) {
return n;
}
vector<int> up(n), down(n);
up[0] = down[0] = 1;
for (int i = 1; i < n; i++) {
if (nums[i] > nums[i - 1]) {
up[i] = max(up[i - 1], down[i - 1] + 1);
down[i] = down[i - 1];
} else if (nums[i] < nums[i - 1]) {
up[i] = up[i - 1];
down[i] = max(up[i - 1] + 1, down[i - 1]);
} else {
up[i] = up[i - 1];
down[i] = down[i - 1];
}
}
return max(up[n - 1], down[n - 1]);
}
2.在投资分配问题中,分别用到了数组g[],f[],t[],请分别描述它们的作用?
设有n万元钱,投资m个项目,将xi万元钱投入第i个项产生的效益为fi(xi),i=1,2,…,m。请问如何分配这n万元钱,使得投资的总效益最高?
a [ ] [ ] a[][] a[][] 表示某阶段最大获利情况下分配给某个项目资金
f [ i ] f[i] f[i]存储第i个项目初始投资所获得利润
t [ ] t[] t[]存储当前投资额的最大利润
数组g[],f[],t[],请分别描述它们的作用?
设有n万元钱,投资m个项目,将xi万元钱投入第i个项产生的效益为fi(xi),i=1,2,…,m。请问如何分配这n万元钱,使得投资的总效益最高?
[外链图片转存中…(img-N4WmQN4l-1713514074827)]
a [ ] [ ] a[][] a[][] 表示某阶段最大获利情况下分配给某个项目资金
f [ i ] f[i] f[i]存储第i个项目初始投资所获得利润
t [ ] t[] t[]存储当前投资额的最大利润
g [ ] g[] g[]存储每一阶段的最优方案。