算法设计与分析-自用笔记

算法设计与分析(王红梅、胡明)

第一部分 基础知识

第一章 算法设计基础

重要问题类型

  • 查找问题
  • 排序问题
  • 图问题
  • 组合问题
  • 几何问题

第二章 算法分析基础

算法复杂分析

  • 时间复杂度分析
    • 最好、最坏、平均情况
    • 递归算法的时间复杂度分析
  • 空间复杂度分析:是指算法在执行过程中需要的辅助空间数量
  • 大O符号:复杂度的上界

第二部分 基本的算法设计技术

第三章 蛮力法

贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,算法得到的是在某种意义上的局部最优解 .

常见问题
顺序查找

在集合中依次查找值为k的元素,遍历查找

串匹配问题

定义:

设有主串s和子串t,子串t的定位就是要在主串s中找到一个与子串t相等的子串。通常把主串s称为目标串,把子串t称为模式串,因此定位也称作模式匹配。

模式匹配成功是指在目标串s中找到一个模式串t;不成功则指目标串s中不存在模式串t。

暴力匹配(BF算法):

​ 双循环,从主串s的开头遍历,每遍历一个字符,就依次检查是否与子串t匹配,若匹配则返回对应位置,否则直到主串s结束后,匹配不成功。

选择排序

思想:数组int a[i],进行n-1次选择,从i=0开始,每次在未排序的区间(a[i+1] - a[len-1])中选取一个最小(最大)的元素与当前元素(a[i])交换,经过n-1次选择交换后,数组a有序

气泡顺序

思想:与水中的水泡一样,若a[i]比a[i+1]大或者小则交换,每进行一轮,就确定一个元素的最终位置,进行n-1轮后或者在某一轮中没有交换发生,数组有序

0/1背包问题

问题:给定背包的容量W,一系列物品的重量int Weight[],对应价值int Value[],求这个背包能装的物品的最大重量。物品只能装入或者不装,不能只装物品的一部分。

思路:暴力法,将所有的可以装入背包的物品组合都试一次,计算它们的价值总量的最大值。至于如何得到所有的可以装入背包的物品组合,可以用回溯法DFS,具体思想略。

任务分配问题

问题:有n个任务分配给n个人,一人只能执行一个任务,一个任务只能分配给一个人。给定nxn矩阵C,C[i] [j]代表第i个人执行第j个任务的代价,求总成本最小的分配方案。

思路:与0/1背包类似,定义数组int ans[n],其中,ans[i] = j代表i个人执行第j个任务,任务的编号为0n-1,问题转化为求0n-1的全排列,每个排列对应一个总成本,求所有排列的总成本最小值即为答案。求全排列同样可以通过DFS求,具体略。

哈密顿回路问题

哈密顿回路:在无向图中,要求经过从一个城市出发,经过每个城市恰好一次,然后回到出发城市。

思路:假设有n个城市,编号为0~n-1,定义路径数组int path[n],代表路径,问题转换为用dfs求全排列,当然不是每个排列都是一条哈密顿回路,只有满足两个条件

  1. path中相邻顶点存在边
  2. path中起点和终点存在边

才是哈密顿回路。

TSP问题

TSP问题:就是选取无向图中路程最短的哈密顿回路,思路同上。

第四章 分治法

对于一个规模为n的问题,若该问题可以容易地解决(比如说规模n较小)则直接解决,否则将其分解为k个规模较小的子问题。这些子问题互相独立且与原问题形式相同,递归地解这些子问题,然后将各子问题的解合并得到原问题的解,这种算法设计策略叫做分治法。

三个阶段

  1. 划分
  2. 求解子问题
  3. 合并
常见问题
归并排序
  • 问题:将一个无序数组int a[n]排序

  • 思想:

    • 划分:将数组区间一分为二
    • 求解子问题:对划分阶段的子区间递归调用归并排序函数,继续一分为二,直到区间只有一个数
    • 合并:将已经排序的子段,合并为一个大的排序的子段
  • 代码:

    //合并函数
    void merge(int r[], int s, int m, int t){
    	int r1[1000];//临时数组
        int i = s, j = m+1, k = s;
    	while(i <= m && j <= t){
    		if(r[i] > r[j])
    			r1[k++] = r[j++];
    		else
    			r1[k++] = r[i++];
    	}
    	while(i <= m)
    		r1[k++] = r[i++];
    	while(j <= t)
    		r1[k++] = r[j++];
    	//将辅助数组的树重新赋值给原数组 
    	for(k = s; k <= t; ++k)
    		r[k] = r1[k];
    } 
    //合并排序
    void MergeSort(int r[], int s, int t){
    	
    	if(s == t)return ;
    	else{
    		//简单的从中间划分 
    		int m = (s+t)/2;
    		//求解子问题 
    		MergeSort(r, s, m);
    		MergeSort(r, m+1, t);
    		//归并 
    		merge(r, s, m, t); 
    	} 
    }
    
    • 复杂度
      • 时间:O(NlogN)
      • 空间:O(N)
快速排序
  • 问题:将一个无序数组int a[n]排序

  • 思想:

    • 划分:根据一个值p(为简单起见,通常取区间的第一个数),然后将大于这个值的数放在数组右边,少于等于的放在数组左边,最后将这个值放中间,p的最终位置确认下来。
    • 求解子问题:对p值的左右区间递归调用函数
    • 合并:在快速排序中,无合并操作
  • 代码:

    //划分函数
    int partion(int a[], int s, int t){
    	//区间只有一个数 
    	if(s >= t)return -1;
    	//以a[s]划分区间,大于等于其的放数组右边,其余放左边 
    	int i = s, j = t;
    	while(i < j){
    		while(i < j && a[j] >= a[i])--j;
    		if(i < j){swap(a[i], a[j]); ++i;}
    		while(i < j && a[j] >= a[i])++i;
    		if(i < j){swap(a[i], a[j]); --j;}
    	}
    	return i; 
    } 
    //快速排序函数
    void quick_sort(int a[], int s, int t){
    	if(s >= t)return ;
    	//划分 
    	int k = partion(a, s, t);
    	//分治:解决子问题
    	quick_sort(a, s, k - 1);
    	quick_sort(a, k + 1, t);
    	//合并:不做任何操作 
    }
    
  • 复杂度:

    • 时间:O(NlogN)
    • 空间:O(N)
最大子段和
  • 问题:给定一个含n个整数的序列,求出该序列的最大子段和,(子段相当于字符串的子串,就是要求连续)

  • 思路:

    最大子段和存在的位置分三种情况:

    • 位于数组左半部分
    • 位于数组右半部分
    • 跨过数组的左右两部分
    • 划分:将数组一分为二。
    • 求解子问题:对左右半部分递归调用函数,得出左、右半部分的最大子段和,leftsum、rightsum。在求跨过数组中间的最大子段和,令为midsum
    • 合并:取leftsum、rightsum、midsum的三者最大值,即为所求的整个整数序列的最大子段和
  • 代码:

    int MaxSum(int a[], int l, int r){
    	//只有一个元素 
    	if(l == r)
    		return a[l];
    	else{
    		int mid = (l+r)/2;
    		int leftMaxSum = MaxSum(a, l, mid);
    		int rightMaxSum = MaxSum(a, mid+1, r);
    		int midMaxSum = 0, s1 = 0, s2 = 0, rights = 0, lefts = 0;
    		for(int i = mid + 1; i <= r; ++i){
    			s1 += a[i];
    			rights = max(rights, s1);
    		}
    		for(int j = mid; j >= l; --j){
    			s2 += a[j];
    			lefts = max(lefts, s2);
    		}
    		midMaxSum = rights + lefts;
    		return max(max(leftMaxSum, rightMaxSum), midMaxSum);	
    	}
    }
    
  • 复杂度:

    • 时间:O(nlogn)
    • 空间:O(1)

第五章 减治法

分治法是把一个大问题划分为若干个子问题,分别求解各个子问题,然后再把子问题的解进行合并得到原问题的解。

减治法同样是把一个大问题划分为若干个子问题,但是这些子问题不需要分别求解,只需求解其中的一个子问题,因而也无需对子问题的解进行合并。

常见问题
折半查找
  • 问题:在一个有序序列中查找值为k的数,假设序列中有且只有一个值为k的数

  • 思想:要利用数组有序的特性,直接与当前区间的中间那个数mid比较:

    1. 若k>mid,则查找区间的右半部分
    2. 若k<mid,则查找区间的左半部分
    3. 若k=mid,则返回mid的数组下标
  • 代码:

    //递归版本
    int BinarySearch(int a[], int left, int right, int k) {
    	//区间不合理
        if(left > right)
    		return -1;
        int mid = (left+right)/2;
        if(a[mid] > k)
            return BinarySearch(a, mid + 1, right, k);
        else if(a[k] < k)
            return BinarySearch(a, left, mid - 1, k);
        else
            return mid;
    }
    
    //迭代版本
    int BinarySearch(int a[], int left, int right, int k) {
        while(left <= right) {
            int mid = (left+right)/2;
            if(a[mid] > k)
                left = mid + 1;
            else if(a[mid] < k)
                right = mid - 1;
           	else
                return mid;
        }
        return -1;
    }
    
二叉查找树(二叉搜索树、BST)
  • 问题、思想:同折半查找

  • 代码:

    /*
    struct treenode
    {
    	int val;
    	treenode *left, *right;
    }
    */
    treenode* find(treenode* t, int k) {
        if(!t)
            return NULL;
        if(t->val > k)
            return find(t->left, k);
        else if(t->val < k)
            return find(t->right, k);
        else
            return t;
    }
    
选择问题
  • 问题:查找无序序列的第k小的元素

  • 思想:利用快速排序的划分函数partition,因为它的返回值,指明某个数的最终位置,若该值为k,则直接返回数组下标为k的数,否则继续对其左半部分或者右半部分递归调用函数

  • 代码:

    //与快排的划分一样 
    int partion(int a[], int low, int high){
    	if(low > high)return -1;
    	int i = low, j = high;
    	while(i < j){
    		while(i < j && a[i] <= a[j])--j;
    		if(i < j){swap(a[i], a[j]); ++i;}
    		while(i < j && a[i] <= a[j])++i;
    		if(i < j){swap(a[i], a[j]); --j;}
    	}
    	return i;
    } 
    //根据划分函数返回的下标,从而判断那些区间可以不用查找 
    int selectMinK(int a[], int low, int high, int k){
    	int s = partion(a, low, high);
    	if(s == k)
    		return a[s];
    	else if(s > k)
    		return selectMinK(a, low, s - 1, k);
    	else
    		return selectMinK(a, s + 1, high, k);
    }
    
插入排序

堆排序
  • 代码:

    //大根堆 ,调整函数 
    void sift_heap(int r[], int k, int n){
    	//i为父节点,j为i的孩子结点 
    	int i = k, j = 2*k+1, temp;
    	//j未超出数组范围 
    	while(j < n){
    		//如果右孩子更大,则令j标记右孩子 
    		if(j + 1 < n && r[j] < r[j+1])
    			++j;
    		//如果父节点已经比孩子结点都大,不需再调整,直接返回 
    		if(r[i] > r[j])
    			break;
    		//需要继续往下调整 
    		else{
    			swap(r[i], r[j]);
    			i = j; j = 2*j+1;
    		}
    	}
    }
    void heap_sort(int a[], int n){
    	//从最底层最右边的一个分支结点开始往上调整 ,建堆 
    	for(int i = (n-1)/2; i >=0; --i)
    		sift_heap(a, i, n);
    		
    	//每次将根(最大值)与后面未排序的最后一个元素交换,使得数组后面局部有序 
    	for(int i = 1; i <= n-1; ++i){
    		swap(a[0], a[n - i]);
    		//继续从根向下调整 
    		sift_heap(a, 0 , n - i);
    	}
    }
    
假币问题
  • 问题:在一堆硬币中,有且仅有一枚假硬币,假硬币质量更轻,找出假硬币

  • 思想:将硬币分为三堆,其中第一堆和第二堆数量相等,比较第一第二堆硬币的质量之和,若

    1. 第一堆硬币质量之和比第二堆的轻,则假币在第一堆中,对第一堆硬币递归调用函数
    2. 第一堆硬币质量之和比第二堆的重,则假币在第二堆中,对第二堆硬币递归调用函数
    3. 第一堆硬币质量之与第二堆的相等,则假币在第三堆中,对第三堆硬币递归调用函数
  • 代码:

    int fake_coin(int coins[], int low, int high, int n){
    	//ni为第i份硬币的数目 
    	int i, n1, n2, n3;
    	if(n == 1)//区间内只有一颗硬币
    		return low;//直接返回下标
    	if(n%3 == 0)//若n为三的倍数,平均分为三分 
    		n1 = n2 = n/3;
    	else 
    		n1 = n2 = n/3+1;
    	n3 = n - n1 - n2;
    	
    	//si为第i份硬币的数量 
    	int s1 = 0, s2 = 0;
    	//统计第一份硬币的重量 
    	for(int i = low; i < low + n1; ++i)
    		s1 += coins[i];
    	//统计第一份硬币的重量
    	for(int i = low + n1; i < low + n1 + n2; ++i)
    		s2 += coins[i];
    	//第一份硬币的重量比第二份轻,即假币在第一份,递归调用,去第一份找假币 
    	if(s1 < s2)
    		return fake_coin(coins, low, low + n1 -1, n1);
    	//第二份硬币的重量比第一份轻,即假币在第二份,递归调用,去第二份找假币 
    	else if(s1 > s2)
    		return fake_coin(coins, low + n1, low + n1 + n2 - 1, n2);
    	//第二份硬币的重量与第一份相等,即假币在第三份,递归调用,去第三份找假币
    	else
    		return fake_coin(coins, low + n1 + n2, high, n3);
    } 
    

第六章 动态规划法

动态规划与分治方法类似,都是通过组合子问题的解来来求解原问题的。再来了解一下什么是分治方法,以及这两者之间的差别,分治方法将问题划分为互不相交的子问题,递归的求解子问题,再将它们的解组合起来,求出原问题的解。而动态规划与之相反,动态规划应用与子问题重叠的情况,即不同的子问题具有公共的子子问题(子问题的求解是递归进行的,将其划分为更小的子子问题)。在这种情况下,分治方法会做许多不必要的工作,他会反复求解那些公共子子问题。而动态规划对于每一个子子问题只求解一次,将其解保存在一个表格里面,从而无需每次求解一个子子问题时都重新计算,避免了不必要的计算工作。

常见问题
多源最短路径
  • 问题:在有向图带权图中,求任意顶点u、v(u≠v)之间的最短距离

  • 思想:动态规划

    设dist[n] [n]矩阵,dist[i] [j] 代表 i 到 j 的最短距离,arc[n] [n]代表有向图的邻接矩阵。

    其中
    $$
    dist_(-1)[i] [j] = arc[i] [j]; \

    dist(k)[i] [j] = min(dist(k-1)[i] [j], dist_(k-1)[i] [k] + dist_(k-1)[k] [j]);
    $$

    dist(k)[i] [j] 表示i到j之间通过顶点不超过k的最短路径

  • 代码:

    void floyd(int arc[n][n], int dist[n][n]) {
        //初始化dist矩阵
        for(int i = 0; i < n; ++i) {
            for(int j = 0; j < n; ++j) {
                dist[i][j] = arc[i][j];
            }
        }
        //进行n轮
        for(int k = 0; k < n; ++k) {
            for(int i = 0; i < n; ++i) {
                for(int j = 0; j < n; ++j) {
                    dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);
                }
            }
        }
    }
    
最长递增子序列
  • 问题:求一个序列中递增子序列的最大长度

  • 思想:令状态数组dp,dp[i]代表在区间0~i的最长递增子序列的长度,有公式
    d p [ i ] = { d p [ i ] = 1 i = 0 d p [ i ] = m a x d p [ j ] + 1 a j < a i a n d 1 < = j < i dp[i]=\left\{ \begin{array}{rcl} dp[i] = 1 & & {i = 0}\\ dp[i] = max{dp[j] + 1} & & {a_j < a_i and 1 <= j < i}\\ \end{array} \right. dp[i]={dp[i]=1dp[i]=maxdp[j]+1i=0aj<aiand1<=j<i

  • 代码:

    int maxIncreasOrder(int a[], int n) {
        int ans = 1;
        int dp[n] = {0};
        dp[0] = 1;
        for(int i = 1; i < n; ++i) {
            for(int j = 0; j < i; ++j) {
                if(a[j] < a[i]) {
                    dp[i] = max(dp[i], dp[j] + 1);
                	ans = max(ans, dp[i]);
                }
            }
        }
    }
    
最长公共子序列
  • 问题:给定两序列s1、s2,求出两者之间最长的公共子序列,公共子序列不一定连续

  • 思路:设定矩阵dp,其中dp[i] [j]代表**s1[0]~ s1[i]s2[0]~s2[j]的最长公共子序列的长度,显然dp[len1] [len2]**就是答案.
    d p [ i ] [ j ] = { d p [ i ] [ j ] = d p [ i − 1 ] [ j − 1 ] + 1 s 1 [ i ] = s 2 [ j ] d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i ] [ j − 1 ] ) s 1 [ i ] ≠ s 2 [ j ] dp[i][j]=\left\{ \begin{array}{rcl} dp[i][j] = dp[i-1][j-1]+1 & & s1[i]=s2[j]\\ dp[i][j] = max (dp[i-1][j] , dp[i][j-1]) & & s1[i]\neq s2[j]\\ \end{array} \right. dp[i][j]={dp[i][j]=dp[i1][j1]+1dp[i][j]=max(dp[i1][j],dp[i][j1])s1[i]=s2[j]s1[i]=s2[j]

  • 代码:

    int longestCommonSubsequence(string s1, string s2) {
        int len1 = s1.size(), len2 = s2.size();
        int ans = 0;
        //状态矩阵
        vector<vector<int>> dp(len1+1, vector<int>(len2+1, 0));
        //开始迭代,注意从小标从1开始,记得要减1
        for(int i = 1; i <= len1; ++i) {
            for(int j = 1; j <= len2; ++j) {
                if(s1[i-1] == s2[j-1])
                    dp[i][j] = dp[i-1][j-1] + 1;
                else
                    dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
                ans = max(ans, dp[i][j]);
            }
        }
        return ans;
    }
    
0/1背包
  • 问题:有n 个物品,编号1~n,它们有各自的重量w和价值v,现有给定容量c的背包,如何让背包里装入的物品具有最大的价值总和

  • 思路:设定矩阵dp,dp[i] [j]代表从1~i号物品选取能装入最大载量为j的背包的最大价值
    d p [ i ] [ j ] = { d p [ i ] [ j ] = d p [ i − 1 ] [ j ] w e i g h t [ i ] > j , 装 不 下 d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , v a l u e [ i ] + d p [ i − 1 ] [ j − w e i g h t [ i ] ] ) w e i g h t [ i ] ≤ j , 装 得 下 dp[i][j]=\left\{ \begin{array}{rcl} dp[i][j] = dp[i-1][j] && weight[i]>j,装不下\\ dp[i][j] = max (dp[i-1][j] , value[i]+dp[i-1][j-weight[i]]) && weight[i]\leq j,装得下\\ \end{array} \right. dp[i][j]={dp[i][j]=dp[i1][j]dp[i][j]=max(dp[i1][j],value[i]+dp[i1][jweight[i]])weight[i]>j,weight[i]j,

  • 代码:

    int knapSack(int w[], int v[], int n, int c) {
        int ans = 0;
        vector<vector<int>> dp(n+1, vector<int>(c+1, 0));
        //注意i、j是从1开始的,记得减1
        for(int i = 1; i <= n; ++i) {
            for(int j = 1; j <= n; ++j) {
                if(w[i-1] > j)//装不下
                    dp[i][j] = dp[i-1][j];
                else//装得下
                    dp[i][j] = max(dp[i-1][j], v[i] + dp[i-1][j-w[i]]);
                ans = max(ans, dp[i][j]);
            }
        }
    }
    

第七章 贪心法

贪心算法是指在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,只做出在某种意义上的局部最优解。贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择,选择的贪心策略必须具备无后效性,即某个状态以前的过程不会影响以后的状态,只与当前状态有关。

常见问题
图着色问题
  • 问题:在无向连通图中,将图中各个顶点涂上颜色,要求相邻顶点颜色不能相同

  • 思路:

    假定k个颜色的集合为{1,2,3…,k}。贪心策略,选取一种颜色,用该颜色尽可能涂尽可能多的顶点。具体来说,先取颜色1,将所有能涂该颜色的顶点涂上,然后再取颜色2,以此类推

  • 代码:

    bool OK(int G[][], int n, int i, int color[]) {
        for(int j = 0; j < n; ++j) {
            if(G[i][j] == 1 && color[i] == color[j])
                	return false;
        }
        return true;
    }
    void coloGraph(int G[][], int n) {
    	int color[n] = {0};//color[i]=0代表i顶点未上色,color[i]=j代表i顶点上色j
        int k = 0;//当前上的颜色
        int flag = 1;//1代表还有顶点未上色1
        while(flag) {
            k++; 
            flag = 0;//假设这轮能将剩下的结点都上色
            for(int i = 0; i < n; ++i) {
                if(color[i] == 0) {//如果顶点i未上色
                    color[i] = k;//尝试上色
                    if(!OK(G, n, i, color)) {//如果上色有冲突,取消上色,并置flag为1
                        flag = 1;
                        color[i] = 0;
                    }
                }
            }
        }
    }
    
最小生成树问题
  • 问题:给定无向连通图,求最小生成树
  • 思想:贪心,两种贪心策略,一是选边,而是选点
prim 和 dijkstra

两者代码大致相同,放在一起

  • 思想:选取一个点为出发点v,令集合V为已加入当前生成树的顶点集合,起始时,V={v},然后选取与V最靠近的顶点u加入V,然后更新距离向量,若某个顶点通过u达到V中的任意顶点的距离变小,则更新距离向量。以此类推,直到将所有顶点加入V。

  • 代码:

    int prim(int G[][], int n, int v) {
        //距离向量
    	vector<int> dist(n, 0x3f3f3f3f);
        //访问标记向量
        vector<int> visited(n, 0);
        //标记已经访问
        dist[v] = 0;
        visited[v] = 1;
        //进行n轮
        for(int k = 0; k < n; ++k) {
            //找出未访问结点中离V最近的结点
            int temp = -1;
            int min = INT_MAX;
            for(int i = 0; i < n; ++i) {
                if(!visited[i] && dist[i] < min) {
                    temp = i;
                    min = dist[i];
                }
            }
            //将temp加入V,则标记temp已经访问
            visited[temp] = 1;
            for(int j = 0; j < n; ++j) {//j顶点通过temp达到V中的任意顶点的距离变小
                if(!visited[j] && dist[j] > G[j][temp]) {
                    dist[j] = G[j][temp];
                }
            }
        }
        //求最小生成树的代价
      	int ans = 0;
        for(int i = 0; i < n; ++i)
            ans += dist[i];
        return ans;
    }
    
    
    int dijkstra(int G[][], int n, int v, int u) {
        //距离向量
    	vector<int> dist(n, 0x3f3f3f3f);
        //访问标记向量
        vector<int> visited(n, 0);
        //路径向量,path[i] = j 代表 i顶点的前一个顶点是j
        vector<int> path(n, -1);
        //标记已经访问
        dist[v] = 0;
        visited[v] = 1;
        //进行n轮
        for(int k = 0; k < n; ++k) {
            //找出未访问结点中离V最近的结点
            int temp = -1;
            int min = INT_MAX;
            for(int i = 0; i < n; ++i) {
                if(!visited[i] && dist[i] < min) {
                    temp = i;
                    min = dist[i];
                }
            }
            //将temp加入V,则标记temp已经访问
            visited[temp] = 1;
            for(int j = 0; j < n; ++j) {
                if(!visited[j] && dist[j] > dist[temp] + G[j][temp]){//只有这一行与prim不同
                    dist[j] = G[j][temp];
                    path[j] = temp;
                }
            }
        }
        //返回起点与终点的最短距离
        return dist[u];
    }
    
kruskal
  • 思想:将无向图中的边,按权值从小到大排序,从权值最小的边开始,若加入该边,不产生回路,则将该边加入,否则,选择下一条边,直至所有顶点都联通

  • 代码:

//表示边的结构体,u、v为边的端点,val为权值
struct edge
{
	int u, v;
	int val;
    edge() {
        u = v = val = -1;
    }
}
//比较边的比较函数
bool cmp(const edge& a, const edge& b) {
    return a.val < b.val;
}
//输入边
vector<edge> input_Edge() {
    int n;
    cin>>n;
    vector<edge> Edge(n);
    for(int i = 0; i < n; ++i) {
        cin>>Edge[i].u>>Edge[i].v>>cin>>Edge[i].val;
    }
    return Edge;
} 
//并查集,看是否i,j位于同一个并查集中
bool isSame(int i, int j, vector<int>& root) {
    int r1 = i, r2 = j;
    while(root[r1] != -1) {
        r1 = root[r1];
    }
    while(root[r2] != -1) {
        r2 = root[r2];
    }
    if(r1 == r2)//i、j位于同一并查集中
        return false;
    //合并
    root[j] = r1;
   	return true;
}
void kruskal(int n) {//n为图中顶点数
    //输入边
    vector<edge> Edge = input_Edge();
    //边排序
    sort(Edeg.begin(), Edge.end(), cmp);
    //最小生成树的边
    vector<edge> ans;
    //边的数目,等于n-1时存在最小生成树
    int count = 0;
    //最小生成树代价
    int cost = 0;
    //并查集
    vector<int> root(n, -1);
    //加边
    for(int i = 0; i < Edge.size(); ++i) {
        if(!isSame(Edge[i].u, Edge[i].v, root)) {
            cost += Edge[i].val;
            ans.push_back(Edge[i]);
            ++count;
            if(count == n - 1)
                break;
        }
    }
    if(count != n -1)
        cout<<"没有最小生成树"<<endl;
}
背包问题
  • 思路:就是按单位重量价值,由大到小排序,优先选单位重量价值大的物品装入
  • 代码:略
活动安排问题

设有n个活动的集合 E = {1,2,…,n},其中每个活动都要求使用同一资源,如演讲会场等,而在同一时间内只有一个活动能使用这一资源。

每个活动 i 都有一个要求使用该资源的起始时间 si 和一个结束时间 fi,且 si < fi。如果选择了活动i,则它在半开时间区间 [si ,fi ) 内占用资源。若区间 [si , fi )与区间 [sj, fj ) 不相交,则称活动i与活动j是相容的。当 si ≥ fj 或 sj ≥ fi 时,活动 i 与活动 j 相容。

活动安排问题就是在所给的活动集合中选出最大的相容活动子集合。

  • 思想:将n个活动按结束时间升序排序,尽快能选择结束时间早的活动,为后面的活动余留更多的时间,然后选取与当前活动相容的活动加入,以此类推,直至最后一个活动。

  • 代码:

    void GreedySelector(int n, int s[], int f[], bool b[])
    {
        b[1]=true;  //默认将第一个活动先安排
        int j=1;    //记录最近一次加入b中的活动
    
          //依次检查活动i是否与当前已选择的活动相容
        for(int i=2;i<=n;i++)
        {
            if (s[i]>=f[j])
            {
                b[i]=true;
                j=i;
            }
            else
                b[i]=false;
        }
    }
    
多机调度问题

设有n个独立的作业,由m台相同的机器进行加工处理。作业i所需的处理时间为t[i]。 任何作业可以在任何一台机器上面加工处理,但未完工之前不允许中断处理。任何作业不能拆分成更小的作业。 要求给出一种作业调度方案,使所给的n个作业在尽可能短的时间内由m台机器加工处理完成。

  • 思想:采用最长处理时间作业优先的贪心选择策略
  • 代码:略

第三部分 基于搜索的算法设计技术

第八章 回溯法

回溯法思路的简单描述是:把问题的解空间转化成了图或者树的结构表示,然后使用深度优先搜索策略进行遍历,遍历的过程中记录和寻找所有可行解或者最优解。

常见问题
图着色问题

假定k个颜色的集合为{1,2,3…,k}。贪心策略,选取一种颜色,用该颜色尽可能涂尽可能多的顶点。具体来说,先取颜色1,将所有能涂该颜色的顶点涂上,然后再取颜色2,以此类推

  • 思想:用dfs遍历所有的上色方案。

  • 代码:

    /*
    G为邻接矩阵
    n为顶点数
    m为颜色数
    k为当前上色的顶点下标,注意:从1开始
    color为所有顶点的颜色
    */
    void dfs(int G[][], int n, int m, int k, int color[]) {
    	//所有颜色已经上色
        if(k > n) {
            //处理结果
            return ;
        }
    	else {
            //尝试将当前结点上遍所有颜色
    		for(int i = 1; i <= m; ++i) {
    			bool f = true;
                //检查颜色是否有冲突
    			for(int i = 1; i < k; ++i) {
    				if(G[k][i] == 1 && color[i] == color[k]) {
    					f = false;
     					break;                 
    				}
    			}
                //若上色合法,继续dfs
                if(f)
                    dfs(G, n, m, k, color);
                //回溯
                color[k] = 0;
    		}
    	}
    }
    
哈密顿回路
  • 代码

    //x[k]代表哈密顿回路中第k个结点,G为无向图的邻接矩阵
    void BackTrack(int k, int n, int G[][], int x[], int visited[]) {
    	//x[]已经是哈密顿通路
        if(k > n) {
            //若起点和终点构成回路,则x[]是哈密顿回路
            if(G[x[1]][x[n]] != 0)
            	;//此时x[]为哈密顿回路,输出x[]
            return ;
        }
        //依次放入与x[k-1]相邻的且未访问过的结点到x[k]中
        for(int i = 1; i <= n; ++i) {
            if(!G[k-1][i] && !visited[i]) {
                x[k] = i;
         		visited[i] = 1;
                BackTrack(k+1, n, G, x, visited);
                //回溯
                visited[i] = 0;
            }
        }
    }
    
N皇后

N皇后问题是一个经典的问题,在一个N*N的棋盘上放置N个皇后,每行一个并使其不能互相攻击(同一行、同一列、同一斜线上的皇后都会自动攻击)。

  • 思想:显然每一行只能放一个皇后,所以可以将第1~N行的皇后放的列数用一个数组place记录,然后就是尝试将每一个皇后从第1列放到第N列,然后检查是否冲突。检查的方法为,由于皇后已经放在不同的行了,现在只须确保不同的皇后放在不同的列以及不同的对角线。检查不同的列,即是检查place都不同;不同的对角线,位于同一对角线,两者之间的斜率要么为,要么为-1,所以只需检查两两皇后所在坐标斜率的绝对值不为1即可。

  • 代码:

    void dfs(int N, int k, int place[]) {
    	if(k > N) {
    		//输出palce数组
    		return ;
    	}
        else {
            //尝试将第k个皇后放在第1~N列
            for(int i = 1; i <= N; ++i) {
                place[k] = i;
               	//检查是否有冲突
                bool f = true;
                for(int j = 1; j < k; ++j) {
                    if(place[k] == place[j] || abs(place[j] - place[k]) == abs(j - k)) {
                        f = false;
                        break;
                    }
                }
                //没有冲突,继续dfs
                if(f)
                    dfs(N, k + 1, place)
                //回溯
                place[k] = 0;
            }
        }
    }
    
作业调度问题

n个作业 N={1,2,…,n}要在2台机器M1和M2组成的流水线上完成加工。每个作业须先在M1上加工,然后在M2上加工。M1和M2加工作业 i 所需的时间分别为 ai 和bi,每台机器同一时间最多只能执行一个作业。

流水作业调度问题要求确定这n个作业的最优加工顺序,使得所有作业在两台机器上都加工完成所需最少时间。

  • 思想:最优调度序列应使M1没有空闲,M2空闲最少。设**x[n]表示n个作业批处理的一种调度方案,其中x[k]**代表第k个作业的编号,**sum1[n]和sum2[n]**表示调度过程中M1和M2的当前完成时间,**sum1[k]**代表安排第k个作业后M1的当前完成时间,**sum2[k]**代表安排第k个作业后M2的当前完成时间
    s u m 1 [ k ] = s u m 1 [ k − 1 ] + 第 k 个 作 业 在 M 1 上 的 处 理 时 间 s u m 2 [ k ] = m a x ( s u m 1 [ k ] , s u m 2 [ k − 2 ] ) + 第 k 个 作 业 在 M 2 上 的 处 理 时 间 sum1[k] = sum1[k-1] + 第k个作业在M1上的处理时间 \\ sum2[k] = max(sum1[k], sum2[k-2]) + 第k个作业在M2上的处理时间 sum1[k]=sum1[k1]+kM1sum2[k]=max(sum1[k],sum2[k2])+kM2

  • 代码:

    //a代表n个作业在M1上的处理时间,b代表n个作业在M2上的处理时间,x存储n个作业的具体调度,sum1[n+1]和sum2[n+1]表示调度过程中M1和M2的当前完成时间,best当前执行的最短时间
    void Batch(vector<int>& a, vector<int>& b, vector<int>& x, vector<int>& sum1, vector<int>& sum2, int n, int& bestTime, int k) {
    	//已经调度完所有任务
        if(k > n) {
            //记录最短执行时间
            if(bestTime > sum2[n])
                bestTime = sum2[n];
            return ;
        }
    	//
        for(int i = 1; i <= n; ++i) {
            //尝试将编号为i的任务放在第k个调度
            x[k] = i;
            //检查编号为i的任务是否之前已经调度过
            bool f = false; 
            for(int j = 1; j < k; ++j) {
                if(x[j] == x[k]) {
                    f = true;
                    break;
                }
            }
            //若已经调度,则将跳过此处循环
            if(f) 
                continue;
            
            //若还未调度,则更新sum1、sum2
            sum1[k] = sum[k-1] + a[i];
            sum2[k] = max(sum1[k], sum2[k-1]) + b[i];
            //如果,当前时间已经大于最短时间,则跳出
            if(sum2[k] > bestTime)
                continue;
            
            //继续dfs
            Batch(a, b, x, sum1, sum2, n, bestTime,k + 1);
        }
    }
    

第九章 分支界限法

分支限界法(branch and bound method)按广度优先策略搜索问题的解空间树,在搜索过程中,对待处理的节点根据限界函数估算目标函数的可能取值,从中选取使目标函数取得极值(极大或极小)的结点优先进行广度优先搜索,从而不断调整搜索方向,尽快找到问题的解。分支限界法适合求解最优化问题。

TSP(特别篇)

  1. 暴力法:城市顺序全排列,找到最短距离
  2. 回溯法:用dfs求哈密顿回路,不过要记录最短的哈密顿回路
  3. 分支定界法:
  4. 动态规划法:
  • 9
    点赞
  • 55
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值