算法分析与设计(xd张立勇老师)总结ing之期末复习篇(附代码)

主要内容提要

如下图:

(虽然不是很想听课,但是也没啥事可以干,所以如听课)

第一节课

没啥好说的,惯例的讲

(1)成绩占比

(2)课程内容

(3)一些其他东西

第二节课

记得好像是讲了归并排序和选择排序啥的。

主要内容就是基于归并排序(merge sort)讲分治算法(divide and conquer),以及计算时间复杂度的过程。

分治算法:

  1. 分解
  2. 解决(递归式子)
  3. 合并

当然,需要注意快排和归并排序的区别。

第三节课

还是在讲分治算法。

给出\mho \Theta O这三者的概念

《算法导论》里面的函数增长的渐进记号就是介绍该内容。

以归并排序为例的递归(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排序算法的下界

  • 决策树模型
  • 最坏情况的下界\Omega(nlgn)

2.1计数排序

稳定的,O(n)

3.1基数排序

基数排序 详细讲解-CSDN博客

线性时间排序总结

感觉在日常刷题中,其实用到基数排序还有二分查找的情况还是挺多的。

动态规划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);

最长公共子序列

动态规划(最长公共子序列)-CSDN博客

改天再仔细写一下吧。dp就是递归式永远自己写不出来,但是看别人的马上就觉得啊,非常有道理。。。

还没开始写dp的题。。。过几天整理一下写题心得。

。。。

说实话,DP真的没有捷径感觉,确实需要大家多练习题,如果只是应付考试,那其实你掌握了矩阵链,最大子数组和,最长子序列,最长子串应该就可以了,好像上课就讲了这些。

但是,学无止境,虽然距离学DP大概过去了一个月,我还没有开始。

这里分享一张图:

不过推荐大家可以先去学记忆化搜索,也就是备忘录。

最短路径问题

建议大家可以先去学一下DFS,BFS,最小生成树这样子。

算法学习之BFS(广度优先搜索)-CSDN博客

算法学习之DFS(深度优先搜索)+并查集模板-CSDN博客

最小生成树这部分,我好像真的忘记写了。。。改天补吧。

单源最短路径

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算法实现)-CSDN博客

那么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> &current_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;
}

背包问题总感觉没啥好说的,上面的代码里面也有注释,这里就不详细展开叙述了。

哈夫曼编码

AcWing 148. 合并果子 - AcWing

回溯

然后就是回溯了。

回溯问题好像看算法导论目录并没有讲到,这部分知识在笔者理解看来差不多就是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> &current_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和贪心那块的例题搞懂就好了,也就是这篇文章里面的例题。

但是学习算法的路还很长,勉励前行。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值