主要内容提要
如下图:
(虽然不是很想听课,但是也没啥事可以干,所以如听课)
第一节课
没啥好说的,惯例的讲
(1)成绩占比
(2)课程内容
(3)一些其他东西
第二节课
记得好像是讲了归并排序和选择排序啥的。
主要内容就是基于归并排序(merge sort)讲分治算法(divide and conquer),以及计算时间复杂度的过程。
分治算法:
- 分解
- 解决(递归式子)
- 合并
当然,需要注意快排和归并排序的区别。
第三节课
还是在讲分治算法。
给出这三者的概念
《算法导论》里面的函数增长的渐进记号就是介绍该内容。
以归并排序为例的递归(recurrences)时间计算
一共讲了三种方法
- Substitution method(代入法,也就是归纳法)
- Recursion-tree methed(递归树)
- Mater method(主方法)
归纳法
没啥好讲的
(放一个书上的例子)
递归树
主方法
比较重要:
举例如下:
至于主方法的证明就不在此展示了,因为一般只做了解。
具体递归树证明过程在算法导论 4-3 递归式 T(n)=2T(n/2)+n/lgn的复杂度求解 - 简书 (jianshu.com)
第三周算法
Heap Sort (堆排序)
1.1
- 与归并排序( O(nlgn) )一样,但不同于插入排序(O(n) ~O(n2)的是,堆排序的时间复杂度为O(n lgn)。
- 而与插入排序相同,但不同于归并排序的是,堆排序具有空间原址性(stable...)
2.1MAX-HEAPIFY
对于一个树高度为h(lgn)的结点来说,MAX-HEAPIFY的时间复杂度为O(h).
#include<iostream>
using namespace std;
const int maxn=10000;
int heap[maxn],n=10;
//堆总是父节点最大
//对heap在[low,high]范围进行向下调整
//low为预调整的结点数组下标,high一般为堆的最后一个原素的数组下标
void downAdjust(int low,int high){
int i=low,j=2*i; //j--left child
while(j<=high){ //存在孩子结点
//如果右孩子存在,且大于左孩子
if(j+1<=high&&heap[j+1]>heap[j]){
//j存储right child
j=j+1;
}
//
if(heap[i]<heap[j]){
swap(heap[i],heap[j]);
i=j;
j=2*i; //调整,j依旧为i的左孩子
} else{
break;
}
}
}
2.2Build Max-Heap
时间复杂度为o(n)
void creatHeap(){
for(int i=n/2;i>=1;i--){
downAdjust1(i,n);
}
}
/*
删除堆顶元素
1.最后一个元素覆盖堆顶元素
2.然后对根节点继续调整即可
*/
void deleteTop(){
heap[1]=heap[n--];
downAdjust1(1,n);
}
//向上调整
//low一般设置为1,high为想要调整节点的数组下标
void upAdjust(int low,int high){
int i=high;
int j=i/2; //父节点
while(j>=low){
if(heap[i]>heap[j]){
swap(heap[i],heap[j]);
i=j;
j=i/2;
} else break;
}
}
void insert(int x){
heap[++n]=x;
upAdjust(1,n);
}
2.3HeapSort
由于堆中,堆顶元素最大。因此,在建完堆后,直观思路为取出堆顶元素,然后将堆的最后一个元素替换至堆顶,再进行一次downAdjust()即可。如此重复,一直到只有一个元素为止。
void heapSort(){
creatHeap();
for(int i=n;i>1;i--){
swap(heap[1],heap[n]);
downAdjust1(1,i-1);
}
}
堆的常见应用-优先队列
c++优先队列(priority_queue)用法详解_c++ 优先队列-CSDN博客
quickselectSort(快排)
最坏情况O(n2),元素互异的情况下,期望的时间复杂度为O(nlgn))
#include<iostream>
#include<algorithm>
#include<cmath>
#include<ctime>
using namespace std;
int partition1(int arrs[100],int l,int r){
int p=round(1.0*rand()/RAND_MAX*(r-l)+l); //生成[l,r]里面的随机数P;
swap(arrs[p],arrs[l]); // 随机快速排序
int pivot=arrs[l];
while(l<r){
while(l<r&&arrs[--r]>=pivot);
arrs[l]=arrs[r];
while(l<r&&arrs[++l]<=pivot);
arrs[r]=arrs[l];
}
arrs[l]=pivot;
return l;
}
void quickSelect(int arrs[],int l,int r){
if(l<r){
int pivot=partition1(arrs,l,r);
quickSelect(arrs,l,pivot-1);
quickSelect(arrs,pivot+1,r);
}
}
int main(){
int arrs[]={3,2,6,4};
quickSelect(arrs,0,3);
for(int i=0;i<=3;i++){
cout<<arrs[i]<<" ";
}
return 0;
}
线性时间排序
1.1排序算法的下界
- 决策树模型
- 最坏情况的下界(nlgn)
2.1计数排序
稳定的,O(n)
3.1基数排序
线性时间排序总结
感觉在日常刷题中,其实用到基数排序还有二分查找的情况还是挺多的。
动态规划DP
矩阵相乘
- Strassen's algorithm
矩阵乘法的Strassen算法详解 --(算法导论分治法求矩阵) - 简书 (jianshu.com)
最大子数组的分治算法
【算法导论学习】分治法求最大子数组_实现最大子数组的分治算法c语言-CSDN博客
这个感觉没有必要学会吧,一般都是DP/滑动窗口。
这里我讲述的是DP:
用MS[i]表示最大子数组的结束下标为i的情形,则对于i-1,有:
MS[i] = max {MS[i-1], A[i]}.
这样就有了一个子结构,对于初始情形,MS[1]=A[1].遍历i, 就能得到MS这个数组,其最大者即可最大子数组的和。
#include<iostream>
using namespace std;
int a[]={-1,11,-4,13,-1,-2};
int sum=a[0];
int maxS=a[0];
/*
let's set
sum[i]=max(sum[i-1]+a[i],a[i]);
为什么是比较sum+a[i]和a[i],而不是比较sum+a[i]和sum呢
原因很简单,就是因为这子序列必须是要连续的。
如果加了以后比没加还小,说明sum已经是负数了,这时候就有必要把连续的给断开了。
还有很重要的一点就是sum并不是max。
*/
void maxSum(int a[],int n){
int x=0,l=0,r=0;
for(int i = 1; i < n; i++){
if(sum+a[i]>a[i]){
sum=sum+a[i];
}
else{
//加了以后比没加还小,说明sum已经是负数
sum=a[i];
x=i;
}
if(sum>maxS){
maxS=sum;
l=x;
r=i;
}
}
cout<<"max="<<maxS<<endl;
cout<<"targetStr: ";
// cout<<"l="<<a[l]<<"r="<<a[r]<<endl;
for(int j=l;j<=r;j++) cout<<a[j]<<" ";
}
int main(){
int n=sizeof(a)/sizeof(a[0]);
//输出最大sum,以及子序列开头和结尾数字
maxSum(a,n);
return 0;
}
矩阵链相乘问题
对于所给的矩阵链序列
int p[]={30,35,15,5,10,20,25};
我们设
int m[7][7]={0};//计算代价
int s[7][7]={0};//记录分割点k
m[i][j]表示在p[i]~p[j]之间需要计算的代价,那么当i==j,其代价自然为0;
//单矩阵链
for(i=0;i<n-1;i++)
m[i][i]=0;
//从两个矩阵链长度开始遍历
for(L=2;L<=n;L++){
//i:是长度为L的的所有情况,总有n-L+1种
for(i=1;i<=n-L+1;i++)
{
//j:是i情况时,最右边的位置,最左边是i
j=i+L-1;
m[i][j]=-1;//给第i-j个矩阵数值链的乘积赋值
for(k=i;k<=j-1;k++) //k为可能的划分点,在i-j之间
{
//p[i-1]:为i之前的
q=m[i][k]+m[k+1][j]+p[i-1]*p[k]*p[j];
if(q<m[i][j]||m[i][j]==-1)
{
m[i][j]=q;
s[i][j]=k;
cout<<"m[i][j]="<<m[i][j]<<" i:"<<i<<" j:"<<j;
cout<<'\n';
}
}
}
}
矩阵链长度肯定最少是2,1的情况就是刚刚讨论的i==j情况,最大是n.
然后我们对L=2~n的所有情况进行寻找,倘若该子矩阵链左边下标是i,那么右边j=i+L-1。
接着我们对该矩阵链里面的可能划分点k(k在i~j)进行讨论:
假设划分点为k,则此时的代价=左下标i到k下标的代价m[i][k]+k+1下标到右下标j的代价m[k+1][j]+最后合并时p[i-1]*p[k]*p[j];
如果之前的m[i][j]的代价比该k时的大,则更新,或者之前的m[i][j]为-1,第一次。
然后顺便记录s[i][j]=k,在i-j之间的矩阵链划分点在k。
如果只需要最小代价,那么最小代价肯定是m[0][n-1]这整个矩阵链所需要的代价。
那么如果需要打印该过程的话,那么就需要到s[][]矩阵。
我们采用分治的思想,printOptimalParens(int s[7][7],int i,int j)
假如我们需要整个矩阵链1~6的所有划分点,那么printOptimalParens(int s[7][7],1,6)
而假设已知s[1][6],该划分点将矩阵链划分为两个矩阵链,然后我们继续将其划分,直至到自身,这时就是最小的时候了,将其打印出来:
void printOptimalParens(int s[7][7],int i,int j){
//打印最优解
if(i==j)//即节点i为划分点,j为s矩阵里面保存的划分点
{
cout<<"A"<<i;
}
else //如果不是
{
cout<<"(";
//递归
printOptimalParens(s,i,s[i][j]);
printOptimalParens(s,s[i][j]+1,j);
cout<<")";
}
}
为m[][]矩阵,这个矩阵的解释大致为,i为左边(即矩阵行),j为右边(矩阵列),所以i<j,所以会看见只有半个矩阵。
当然这个是1开始的。所以测试的时候还是需要注意,从1开始:
matrixChainOrder(p,n);
for(int i=1;i<=6;i++){
for(int j=1;j<=6;j++){
printf("%8d",m[i][j]);
}
cout<<'\n'<<endl;
}
printOptimalParens(s,1,6);
最长公共子序列
改天再仔细写一下吧。dp就是递归式永远自己写不出来,但是看别人的马上就觉得啊,非常有道理。。。
还没开始写dp的题。。。过几天整理一下写题心得。
。。。
说实话,DP真的没有捷径感觉,确实需要大家多练习题,如果只是应付考试,那其实你掌握了矩阵链,最大子数组和,最长子序列,最长子串应该就可以了,好像上课就讲了这些。
但是,学无止境,虽然距离学DP大概过去了一个月,我还没有开始。
这里分享一张图:
不过推荐大家可以先去学记忆化搜索,也就是备忘录。
最短路径问题
建议大家可以先去学一下DFS,BFS,最小生成树这样子。
最小生成树这部分,我好像真的忘记写了。。。改天补吧。
单源最短路径
Bellman-Ford算法
G=(V,E)
该算法解决的是一般情况下的单源最短路径问题,边的权重可以为负值。
给一个带权重的有向图and权重函数,该算法会返回一个布尔值,表明是否存在一个源节点可以到达的权重为负值的环路。
- 如果存在,算法讲告诉我们不存在解决方案;
- 如果没有这种环路存在,算法将给出最短路径和它们的权重;
刚开始代码如下:
#include <iostream>
#include <cstring>
using namespace std;
const int INF = 0x3f3f3f3f;//也可以INT_MAX
int dis[100009], n, m, k; // n为点数,m为边数,dis[i]为起点到i的最短距离
int pre[100009]; //记录上一个点,为了打印最短路径
struct Edge
{
int u, v, w;
} edge[100009];
void Print_Path(int x)
{
if (pre[x] == -1)
{
cout << x; //起点的pre的-1.x是起点
return;
}
else
{
Print_Path(pre[x]);
cout << "->" << x;
}
}
void bellman_ford(int s, int end)
{
memset(dis, 0x3f, sizeof(dis));
dis[s] = 0;
pre[s] = -1;
for (int i = 0; i < n - 1; i++)
{
bool ok = false;
for (int j = 0; j < m; j++)
{
int x = edge[j].u, y = edge[j].v, w = edge[j].w;
if (dis[x] < INF && dis[x] + w < dis[y])
{
dis[y] = dis[x] + w;
pre[y] = x;
ok = true;
}
}
if (!ok)
{
break;
}
}
if (dis[end] < INF)
{
cout << dis[end] << endl;
}
else
{
cout << "-1\n";
}
Print_Path(end);
}
int main()
{
n = 3;
m = 3;
edge[0].u = 1;
edge[0].v = 2;
edge[0].w = 3;
edge[1].u = 2;
edge[1].v = 3;
edge[1].w = 2;
edge[2].u = 3;
edge[2].v = 2;
edge[2].w = 1;
bellman_ford(1, 3);
cout << "\nnext edge: " << endl;
bellman_ford(3, 1);
return 0;
}
检查有无负环
将dist数组初始化为0,迭代n-1次后进行第n次迭代,如果第n次迭代有进行松弛操作,则一定存在负环,因为不存在负环最多只能进行n-1次松弛操作
上面代码中有一个潜在的问题在于 Bellman-Ford 算法的松弛循环中没有对负环的情况进行处理。当存在负权回路时,Bellman-Ford 算法会一直更新距离数组 dis[] 直到第 n 轮循环,导致 dis[] 不再收敛于最短路径值而是负无穷。
为了解决这个问题,可以在算法中添加一步检测负环的操作。一个简单的方法是在算法的最后再进行一次遍历,如果某个点在第 n 轮循环中还能进行松弛操作,那么该点就属于负环。
#include <iostream>
#include <cstring>
using namespace std;
const int INF = 0x3f3f3f3f;
struct Edge {
int u, v, w;
};
const int MAX_N = 1000;
const int MAX_M = 1000;
int dis[MAX_N];
int pre[MAX_N];
Edge edge[MAX_M];
int n, m;
void Print_Path(int x) {
if (pre[x] == -1) {
cout << x;
return;
}
else {
Print_Path(pre[x]);
cout << " -> " << x;
}
}
void bellman_ford(int s, int end) {
memset(dis, 0x3f, sizeof(dis));
dis[s] = 0;
pre[s] = -1;
for (int i = 0; i < n - 1; i++) {
bool ok = false;
for (int j = 0; j < m; j++) {
int x = edge[j].u, y = edge[j].v, w = edge[j].w;
if (dis[x] < INF && dis[x] + w < dis[y]) {
dis[y] = dis[x] + w;
pre[y] = x;
ok = true;
}
}
if (!ok) {
break;
}
}
bool has_negative_cycle = false;
for (int j = 0; j < m; j++) {
int x = edge[j].u, y = edge[j].v, w = edge[j].w;
if (dis[x] < INF && dis[x] + w < dis[y]) {
has_negative_cycle = true;
break;
}
}
if (has_negative_cycle) {
cout << "Graph contains negative cycle.\n";
}
else {
if (dis[end] < INF) {
cout << "Shortest distance from " << s << " to " << end << ": " << dis[end] << endl;
cout << "Shortest path: ";
Print_Path(end);
cout << endl;
}
else {
cout << "No path from " << s << " to " << end << endl;
}
}
}
int main() {
n = 3;
m = 3;
edge[0] = {1, 2, -1};
edge[1] = {2, 3, -2};
edge[2] = {3, 1, -3};
bellman_ford(1, 3);
return 0;
}
时间复杂度分析:
刚开始初始化:O(V);
循环:O(E),且一共进行V-1次循环
后面的for循环所需O(E)
总运行为O(VE)。
dijkstra
那么dijkstra的时间复杂度应该是多少呢?
就我在这篇文章里面实现的代码来看,好像其实是差不多的吧。
当然根据《算法导论》,我们其实也可以将运行时间改善到O(VlgV+E),方法则是使用斐波那契堆来实现最小优先队列。
所有节点对的最短路径问题
Floyd算法
这个比较重要的核心思想就是“松弛”。其他没啥好讲的感觉,都在代码注释里面了。
时间复杂度为O(n^3),并且不能存在权重为负值的环路,但是可以有负权边。
#include <iostream>
#include <vector>
using namespace std;
const int INF = 1e9; // 设置一个足够大的值
void floydWarshall(vector<vector<int>>& graph, int n) {
vector<vector<int>> dist = graph;
vector<vector<int>> next(n, vector<int>(n, -1)); // 用于存储路径信息
// Floyd算法核心部分
//floyd本质目的是对于每个点对i-j的距离可以被其它点优化,而且可以被多个点共同优化,
//如果循环k在内层,那么i,j每次只能得到一个点的优化。
for (int k = 0; k < n; ++k) { //枚举k 循环k要在最外层
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
if (dist[i][k] != INF && dist[k][j] != INF && dist[i][k] + dist[k][j] < dist[i][j]) {
dist[i][j] = dist[i][k] + dist[k][j];
next[i][j] = k;
}
}
}
}
// 输出路径
cout << "Shortest distance between each pair of vertices:\n";
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
if (i != j && dist[i][j] != INF) {
cout << i << " -> " << j << ": " << dist[i][j] << " (Path: ";
cout << i;
int cur = next[i][j];
while (cur != -1) {
cout << " -> " << cur;
cur = next[cur][j];
}
cout << " -> " << j << ")\n";
}
}
}
}
int main() {
int n = 4; // 图中节点个数
vector<vector<int>> graph = {
{0, 2, 5, INF},
{INF, 0, 1, 4},
{INF, INF, 0, INF},
{3, INF, INF, 0}
};
floydWarshall(graph, n);
return 0;
}
Johnson算法(解决负权边问题)
最短路—Johnson算法(解决负权边,判断负权环) - unique_pursuit - 博客园 (cnblogs.com)
贪心
目录如下:
不过最短路径问题我已经在上一part讲了。
活动选择问题
问题描述
看了老师一下PPT:
如果教室在8~12点允许活动安排,那么最多可以安排多少活动呢?
问题解决
DP解决
#include<iostream>
#include<vector>
using namespace std;
int N=11;
int DP_SelectAc(vector<int>s, vector<int>f,vector<vector<int>>&c){
int i,j,k;
int temp;
//i>=j,c[i][j]=0
for(j=1;j<=N;j++)
for(i=j;i<=N;i++)
c[i][j]=0;
//i<j
for(j=2;j<=N;j++)
for(i=1;i<j;i++){
//k c[i][k],c[k][j]
//s[k]活动开始时间必须大于i结束时间
//活动结束时间必须大于j的开始时间
for(k=i+1;k<j;k++){
if(s[k]>=f[i] && f[k]<=s[j]){
temp=c[i][k]+c[k][j]+1;
if(c[i][j]<temp){
c[i][j]=temp;
//ret[i][j]=k; //print res
}
}
}
}
return c[1][N]+2;
//c[1][N]表示的是在活动1结束后,活动N开始前的个数
}
int main(){
vector<int>s={0,1,3,0,5,3,5,6,8,8,2,12}; //活动开始时间
vector<int>f={0,4,5,6,7,8,9,10,11,12,13,14};
vector<vector<int>> c(N+1, vector<int>(N+1, 0));
int ans=DP_SelectAc(s,f,c);
cout<<ans<<endl;
return 0;
}
贪心
每次选择活动结束最早的那个
当然我们先用set自动排序一下,这样只需要遍历一遍即可。
时间复杂度O(N)
#include<iostream>
#include<vector>
#include<set>
using namespace std;
int N=11;
struct compare {
bool operator()(pair<double, int> a, pair<double, int> b) {
return a.second < b.second ; //结束时间更早的活动
}
};
int GreedySelectAc(vector<int>s, vector<int>f){
int ans=1;
set<pair<int,int>,compare >t;
for(int i=1;i<=N;i++){
t.emplace(s[i],f[i]);
}
auto temp=t.begin();
for(auto it=next(t.begin(), 1);it!=t.end();it++){
if(it->first>=temp->second){
ans++;
temp=it;
}
}
return ans;
}
int main(){
vector<int>s={0,1,3,0,5,3,5,6,8,8,2,12}; //活动开始时间
vector<int>f={0,4,5,6,7,8,9,10,11,12,13,14};
int ans=GreedySelectAc(s,f);
cout<<ans<<endl;
return 0;
}
算法复习-活动选择问题(动态规划法和贪心法) - mdumpling - 博客园 (cnblogs.com)
这是我参考的文章。
背包问题
贪心经典的问题就是背包问题
当然也可以dp了
DP版
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
int KnapsackReturnMaxValue(vector<int> value, vector<int> weight,int sum){
int n=value.size();
vector<vector<int>> dp(n + 1, vector<int>(sum + 1, 0));
//dp[i][v]表示前i件物品恰好装入容量为v的背包所能获得的最大价值
//1.选择第i件物品,则问题为: dp[i-1][v-weight[i]]+value[i]
//2.不选择,则问题为: dp[i-1][v]
//那么最大价值为 dp[i][v]=max(dp[i-1][v],dp[i-1][v-weight[i]]+value[i])
for(int i=1;i<=n;i++){
for(int v=1;v<=sum;v++){
if(weight[i]>v){
dp[i][v]=dp[i-1][v];
}
else{
dp[i][v]=max(dp[i-1][v],dp[i-1][v-weight[i]]+value[i]);
}
}
}
return dp[n][sum];
}
int KnapsackReturnMaxValue1(vector<int> value, vector<int> weight,int sum){
//使用滚动数组
//每计算出一个dp[i][v]相当于把dp[i-1][v]抹消掉
int n=value.size();
vector<int> dp(sum+1,0);
for(int i=1 ;i<=n;i++){
for(int v=sum;v>=weight[i];v--){
dp[v]=max(dp[v],dp[v-weight[i]]+value[i]);
}
}
return dp[sum];
}
int main(){
int sum = 100;
int n=5;
vector<int> value = {0, 20, 30, 65, 40, 60};
vector<int> weight = {0, 10, 20, 30, 40, 50};
cout<<KnapsackReturnMaxValue(value,weight,100);
return 0;
}
贪心版
这里其实是可分割的背包,不是0-1背包了
#include<iostream>
#include<vector>
#include<set>
#include<map>
using namespace std;
struct compare {
bool operator()(pair<double, int> a, pair<double, int> b) {
return a.first > b.first;
}
};
double KnapsackReturnMaxValue1(vector<int> value, vector<int> weight, int sum) {
int n = value.size();
set<pair<double, int>, compare> vw;
for(int i = 0; i < n; ++i) {
if(weight[i] != 0) { // 检查重量是否为0,避免除以0
vw.emplace(static_cast<double>(value[i]) / weight[i], weight[i]);
}
}
double ans = 0;
for(auto it = vw.begin(); it != vw.end(); it++) {
if(it->second <= sum) {
ans += it->second * it->first;
sum -= it->second;
} else {
ans+=sum *it->first;
break;
}
}
return ans;
}
int main() {
int sum = 100;
vector<int> value = {0, 20, 30, 65, 40, 60};
vector<int> weight = {0, 10, 20, 30, 40, 50};
cout << KnapsackReturnMaxValue1(value, weight, sum) << endl;
return 0;
}
回溯版
#include <iostream>
#include <vector>
using namespace std;
int num = 3; // 物品个数
int weights[] = {20, 15, 10};
int values[] = {20, 30, 25};
int capacity = 25; // 背包容量
int max_value = 0;
vector<int> best_solution;
void backtrack(int current_item, int current_weight, int current_value, vector<int> ¤t_solution) {
if (current_item == num || current_weight == capacity) {
if (current_value > max_value) {
max_value = current_value;
best_solution = current_solution;
}
return;
}
// Choose the current item
if (current_weight + weights[current_item] <= capacity) {
current_solution.push_back(current_item);
backtrack(current_item + 1, current_weight + weights[current_item], current_value + values[current_item], current_solution);
current_solution.pop_back();
}
// Not choose the current item
backtrack(current_item + 1, current_weight, current_value, current_solution);
}
int main() {
vector<int> current_solution;
backtrack(0, 0, 0, current_solution);
cout << "Max Value: " << max_value << endl;
cout << "Items included in the knapsack: ";
cout << endl;
return 0;
}
背包问题总感觉没啥好说的,上面的代码里面也有注释,这里就不详细展开叙述了。
哈夫曼编码
回溯
然后就是回溯了。
回溯问题好像看算法导论目录并没有讲到,这部分知识在笔者理解看来差不多就是DFS+剪枝了。
0-1背包回溯版
下面代码实现的是一个背包问题的解决方案,其中使用了回溯算法来求解。
以下是该代码的时间复杂度和空间复杂度评估:
- 时间复杂度:在最坏情况下,该算法会考虑每个物品的选择状态,时间复杂度为O(2^n),其中n为物品个数。因为每个物品都有选和不选两种状态,共有2^n种组合。在每次递归调用时,需要考虑当前物品是否被选择,因此有指数级的时间复杂
-
空间复杂度:空间复杂度主要取决于递归调用过程中使用的空间。在这段代码中,主要使用了一个当前解向量current_solution来存储当前的选择情况,因此空间复杂度主要取决于这个向量的大小。最大的空间复杂度也为O(n),其中n为物品个数,因为向量中最多会存放所有的物品。
-
#include <iostream> #include <vector> using namespace std; int num = 3; // 物品个数 int weights[] = {20, 15, 10}; int values[] = {20, 30, 25}; int capacity = 25; // 背包容量 int max_value = 0; vector<int> best_solution; void backtrack(int current_item, int current_weight, int current_value, vector<int> ¤t_solution) { if (current_item == num || current_weight == capacity) { if (current_value > max_value) { max_value = current_value; best_solution = current_solution; } return; } // Choose the current item if (current_weight + weights[current_item] <= capacity) { current_solution.push_back(current_item); backtrack(current_item + 1, current_weight + weights[current_item], current_value + values[current_item], current_solution); current_solution.pop_back(); } // Not choose the current item backtrack(current_item + 1, current_weight, current_value, current_solution); } int main() { vector<int> current_solution; backtrack(0, 0, 0, current_solution); cout << "Max Value: " << max_value << endl; cout << "Items included in the knapsack: "; for (int item : best_solution) { cout << item << " "; } cout << endl; return 0; }
N皇后
#include<iostream>
#include<vector>
using namespace std;
vector<vector<string> > result;
/*
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
*/
bool isValid(int row, int col, vector<string>& chessboard, int n) {
int count = 0;
// 检查列
for (int i = 0; i < row; i++) { // 这是一个剪枝
if (chessboard[i][col] == 'Q') {
return false;
}
}
// 检查 45度角是否有皇后
for (int i = row - 1, j = col - 1; i >=0 && j >= 0; i--, j--) {
if (chessboard[i][j] == 'Q') {
return false;
}
}
// 检查 135度角是否有皇后
for(int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
if (chessboard[i][j] == 'Q') {
return false;
}
}
return true;
}
void backtracking(int n, int row, vector<string>& chessboard) {
//n:棋牌大小
//row:行数当前
if(row==n){
result.push_back(chessboard);
return;
}
/*递归深度就是row控制棋盘的行,每一层里for循环的col控制棋盘的列,一行一列,确定了放置皇后的位置。*/
/*每次都是要从新的一行的起始位置开始搜,所以都是从0开始。*/
for(int col=0;col<n;col++){
if (isValid(row, col, chessboard, n)) { // 验证合法就可以放
chessboard[row][col] = 'Q'; // 放置皇后
backtracking(n, row + 1, chessboard);
chessboard[row][col] = '.'; // 回溯,撤销皇后
}
}
}
int main() {
vector<string> chessboard(4, string(4, '.')); // 4x4 chessboard with all positions empty
backtracking(4, 0, chessboard);
for (int i = 0; i < result.size(); ++i) {
for (int j = 0; j < result[i].size(); ++j) {
cout << result[i][j] << '\n';
}
cout << '\n';
}
return 0;
}
【算法分析】实验 4. 回溯法求解0-1背包等问题 - pprp - 博客园 (cnblogs.com)
subset sum
子集问题需要与数学中的“排列”问题区分开来。因为子集往往是无序的,但排列是需要考虑顺序的;所以子集问题常常只是一个“组合”问题,而不是“排列”问题。
从另一个角度讲,这种子集和问题是一种背包问题的特例。
问题:
给定n 个整数的集合X={x1,x2,......,xn}和一个正整数y,编写一个回溯算法,
在X中寻找子集Yi,使得Yi中元素之和等于y。
这其实和之前学分治时候的一个题目很相似:已知一组数组,求两数之和等于给出来的数n,可以分治也可以双指针。
这里我们使用回溯进行求解。
子集与子集和问题(Subset sum)的递归回溯解 - 微型葡萄 - 博客园 (cnblogs.com)
13.3 子集和问题 - Hello 算法 (hello-algo.com)
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
void backtrack(vector<int> &state,int target,vector<int> &choices,int start,vector<vector<int>> &res){
if(target==0){
res.push_back(state);
return;
}
for(int i=start;i<choices.size();i++){
//数组已经排序
if(target-choices[i]<0) break;
//update: target,start
state.push_back(choices[i]);
//
backtrack(state,target-choices[i],choices,i,res);
//back
state.pop_back();
}
}
vector<vector<int> > subsetSum(vector<int> &nums,int target){
vector<int> state;
sort(nums.begin(),nums.end());
int start=0;
vector<vector<int> >res;
backtrack(state,target,nums,start,res);
return res;
}
int main(){
vector<int> nums={3,4,5};
int target=9;
vector<vector<int> >res=subsetSum(nums,target);
cout << "Subsets with sum equal to " << target << " are: " << endl;
for (const auto& subset : res) {
cout << "[";
for (int i = 0; i < subset.size(); i++) {
cout << subset[i];
if (i < subset.size() - 1) {
cout << ", ";
}
}
cout << "]" << endl;
}
return 0;
}
这个是可以重复使用元素的情况。
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
void backtrack(vector<int> &state, int target, vector<int> &choices, int start, vector<vector<int>> &res,vector<bool> &used) {
if (target == 0) {
res.push_back(state);
return;
}
for (int i = start; i < choices.size(); i++) {
if (target - choices[i] < 0) break;
//不重复使用已经有的元素,原来是重复元素的也只是使用一次
if (i > start && choices[i] == choices[i - 1] && !used[i - 1]) {
continue;
}
state.push_back(choices[i]);
used[i] = true;
backtrack(state, target - choices[i], choices, i + 1, res, used);
used[i] = false;
state.pop_back();
}
}
vector<vector<int> > subsetSum(vector<int> &nums, int target) {
vector<int> state;
sort(nums.begin(), nums.end());
int start = 0;
vector<vector<int>> res;
vector<bool> used(nums.size(), false);
backtrack(state, target, nums, start, res, used);
return res;
}
int main() {
vector<int> nums = {3, 4,4, 5};
int target = 9;
vector<vector<int>> res = subsetSum(nums, target);
cout << "Subsets with sum equal to " << target << " are: " << endl;
for (const auto& subset : res) {
cout << "[";
for (int i = 0; i < subset.size(); i++) {
cout << subset[i];
if (i < subset.size() - 1) {
cout << ", ";
}
}
cout << "]" << endl;
}
return 0;
}
加入used数组用来判断是否使用过。
Branch and bound(分支定界)
分支定界 (branch and bound)求解TSP问题 - 简书 (jianshu.com)
15 puzzle
NP
如果只是为了考试的话,看往年的题,其实只要把老师上课讲的DP和贪心那块的例题搞懂就好了,也就是这篇文章里面的例题。
但是学习算法的路还很长,勉励前行。