算法第一章 动态规划
一、动态规划的思想
动态规划的核心思想是通过寻找子问题的最优解来构造原问题的最优解。在应用动态规划的过程中,需要找到问题的状态转移方程,并利用储存中间结果的方法来避免重复计算。 总的来说,动态规划的思想包括:分析问题具有的重叠子问题性质、定义子问题的递推关系、储存中间结果以避免重复计算、推导出问题的最优解。
二、动态规划五要素
- 最优子结构
- 重叠子问题
- 状态转移方程
- 边界条件
- 填表法
三、例题
矩阵相乘问题 这道题的关键在于矩阵乘法的次数和举证的行列数有关,2行3列的矩阵乘3行4列的矩阵要乘2*4次
解法1: #include <iostream> #include <climits> using namespace std; int main() { int n; cin >> n; // 输入矩阵个数 int m[n+1][n+1]; // 定义子问题数组 m[i][j] 表示第 i 个矩阵到第 j 个矩阵的最少乘次数 int p[n+1]; // n 个矩阵乘法要有 n+1 个行列数据 for (int i = 0; i <= n; i++) { cin >> p[i]; } for (int i = 1; i <= n; i++) { m[i][i] = 0; // 初始化对角线元素为 0 } for (int l = 2; l <= n; l++) { for (int i = 1; i <= n - l + 1; i++) { int j = i + l - 1; m[i][j] = INT_MAX; for (int k = i; k <= j - 1; k++) { int temp = m[i][k] + m[k+1][j] + p[i-1] * p[k] * p[j]; if (temp < m[i][j]) { m[i][j] = temp; } } } } cout << m[1][n]; return 0; } 解法2: //状态转移方程 : dp[i][j] = min(dp[i][k] + dp[k+1][j] + p[i-1] * p[k] * p[j]) (i < k < j) #include <iostream> #include <algorithm> using namespace std; int main() { int n;//矩阵的个数 cin >> n; int** dp = new int* [n + 1]; // 分配n+1个指向整数数组的指针,dp数组表示从第i个矩阵乘到第j个矩阵 for (int i = 0; i < n + 1; i++) { dp[i] = new int[n + 1]; // 为每个指针分配整数数组 } int *p = new int [n+1];//行列数据 // 对数组进行赋值 for (int i = 0; i <= n; i++) { cin >> p[i]; } //边界条件或初始化 for (int i = 1; i <= n; i++) { dp[i][i] = 0; //自身不用乘,把对角线置为0 } for (int i = n; i >= 1; i--) {//从下往上,自左而右的填表 dp[i][j]必须要知道左侧的和下方的dp解 for (int j = i+1; j <= n; j++) { dp[i][j] = INT_MAX; //初始化为一个最大的值 for (int k = i; k <= j-1; k++) { dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j] + p[i - 1] * p[k] * p[j]); } } } for (int i = 0; i <= n; i++) { for (int j = i; j <= n; j++) { cout << "dp[" << i << "][" << j << "]" << dp[i][j] << endl; } } cout << dp[1][n] <<endl; for (int i = 0; i < n + 1; i++) { delete[] dp[i]; // 释放每个数组 } delete[] dp; // 释放指针数组 return 0; }
最大子段和问题 /* 给定n个整数(可能为负数)组成的序列a[1],a[2],a[3],…,a[n],求该序列如a[i]+a[i+1]+…+a[j]的子段和的最大值。当所给的整数均为负数时,定义子段和为0。 要求算法的时间复杂度为O(n)。 输入格式: 输入有两行: 第一行是n值(1<=n<=10000); 第二行是n个整数。 输出格式: 输出最大子段和。 输入样例: 在这里给出一组输入。例如: 6 -2 11 -4 13 -5 -2 输出样例: 在这里给出相应的输出。例如: 20 */ //状态转移方程dp[i] = max(dp[i-1] + nums[i],nums[i]); //字段的定义是连续的,所以dp[i]只可能接着上一段或者当前数字开头做新一段 #include <iostream> #include <cstring> using namespace std; int MaxSum = 0; int getMaxSum(int arr[], int dp[], int n) { int count = 0; dp[0] = arr[0]; for (int i = 0; i < n; i++) { if (arr[i] < 0) { count++; } } if (count == n) { MaxSum = 0; } else { for (int i = 1; i < n; i++) { dp[i] = max(dp[i - 1] + arr[i], arr[i]); MaxSum = max(MaxSum, dp[i]); } } return MaxSum; } int main(){ int n; cin >> n; int* nums = new int[n]; for (int i = 0; i < n; i++) { cin >> nums[i]; } int* dp = new int[n];//从第个数到第n个数的最大子段和 memset(dp, 0, sizeof dp); int result = getMaxSum(nums, dp, n); cout << result << endl; return 0; }
0-1背包问题 /* 7-20 0-1背包 给定n(n<=100)种物品和一个背包。物品i的重量是wi(wi<=100),价值为vi(vi<=100),背包的容量为C(C<=1000)。 应如何选择装入背包中的物品,使得装入背包中物品的总价值最大? 在选择装入背包的物品时,对每种物品i只有两个选择:装入或不装入。不能将物品i装入多次,也不能只装入部分物品i。 输入格式: 共有n+1行输入: 第一行为n值和c值,表示n件物品和背包容量c; 接下来的n行,每行有两个数据,分别表示第i(1≤i≤n)件物品的重量和价值。 输出格式: 输出装入背包中物品的最大总价值。 输入样例: 在这里给出一组输入。例如: 5 10 2 6 2 3 6 5 5 4 4 6 输出样例: 在这里给出相应的输出。例如: 15 */ //状态转移方程: if (j >= weights[i]) { //dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weights[i]] + values[i]); // } // else { // dp[i][j] = dp[i - 1][j]; #include <iostream> #include <algorithm> using namespace std; int main() { int n, c; cin >> n >> c; int weights[31]; int values[31]; //多定义一个空间,让第i件物品价值对应 for (int i = 1; i <= n; i++) { cin >> weights[i] >> values[i]; } int dp[31][1001] = { 0 }; for (int i = 1; i <= n; i++) { for (int j = 1; j <= c; j++) { if (j >= weights[i]) { dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weights[i]] + values[i]); } else { dp[i][j] = dp[i - 1][j]; } } } //输出最优解 int x = n; int y = c; while(x > 0 && y>0){ if(dp[x-1][y] != dp[x][y]){ cout << x << " "; y-=weights[x]; }x--; } cout << dp[n][c] << endl; return 0; }
子集和 /* 7-2 子集和 给定n个不同的整数的集合,求有多少个子集的和为m 输入格式: 第一行两个数字n(0<n<=100)和m(0<m<=5000),以空格分隔 第二行n个不同的整数,以空格分隔 输出格式: 和为m的子集的个数 输入样例: 5 6 1 2 3 4 5 输出样例: 3 */ /* 状态转移方程: dp[i][j] = dp[i + 1][j]; if (j >= nums[i - 1]) { dp[i][j] += dp[i + 1][j - nums[i - 1]]; } */ #include <iostream> #include <stdio.h> using namespace std; #define MAX 5000 int dp[MAX][MAX];//dp[i][j]表示第i个数开始到结尾,剩余和为j的最优解 int nums[MAX]; int n, m; int solve() { for (int i = 1; i <= m; i++) { dp[n][i] = 0;//第n个数开始到结尾即第i个数和为任何数的子集数为0 } dp[n][0] = 1; if (m >= nums[n]) { dp[n][nums[n]] = 1; } for (int i = n - 1; i >= 1; i--) { for (int j = 0; j <= m; j++) { dp[i][j] = dp[i + 1][j]; if (j >= nums[i]) { dp[i][j] += dp[i + 1][j - nums[i]]; } } } return dp[1][m]; } int main() { cin >> n >> m; for (int i = 1; i <= n; i++) { cin >> nums[i]; } cout << solve() << endl; return 0; }
最长公共子序列问题 #include <iostream> #include <string> using namespace std; int dp[105][105]; //表示长度为i和j的最长公共子序列长度 string A, B; int alen; int blen; int LCSlength() { // 0, A == 0 || B == 0 for (int i = 0; i <= alen; i++) { dp[i][0] = 0; } for (int i = 0; i <= blen; i++) { dp[0][i] = 0; } for (int i = 1; i <= alen; i++) { for (int j = 1; j <= blen; j++) { if (A[i-1] == B[j-1]) { // previously incorrect line: A[i] -> A[i-1] dp[i][j] = dp[i-1][j-1] + 1; } else if (dp[i][j-1] > dp[i-1][j]) { dp[i][j] = dp[i][j - 1]; } else { dp[i][j] = dp[i-1][j]; } } } return dp[alen][blen]; } void printLCS(int alen, int blen){ if(alen == 0 || blen ==0){ return; }else if(x[alen -1] == y[blen -1]){ printLCS(alen-1,blne-1); cout << x[alen-1]; }else if(c[alen-1][blne] > c[alne][blen-1]){ printLCS(alne-1,blen); }else{ printLCS(alen,blen-1); } } int main() { cin >> A >> B; alen = A.length(); blen = B.length(); int l = LCSlength(); cout << l << endl; // previously incorrect line: << result << endl -> << endl. printLCS(alen,blen); return 0; // main function should return a value }
算法第二章 分治法
一、分治法的思想
分治法就是不断的划分子问题,直到子问题的规模足够小到可以直接求解,再通过合并子问题求得原来的子问题的解
二、分治法的时间复杂度
三、分治法之排序问题
快速排序 #include <iostream> #include <algorithm> using namespace std; int partition(int arr[], int left, int right) { int pivot = arr[left]; int i = left + 1; int j = right; while (i <= j) { while (i <= right && arr[i] < pivot) { i++; } while (j >= left + 1 && arr[j] > pivot) { j--; } if (i <= j) { swap(arr[i], arr[j]); i++; j--; } } swap(arr[left], arr[j]); return j; } void quickSort(int arr[], int left, int right) { if (left >= right) { return; } int pivotIndex = partition(arr, left, right); quickSort(arr, left, pivotIndex - 1); quickSort(arr, pivotIndex + 1, right); } int main() { int n; cout << "Enter the size of the array: "; cin >> n; if (n <= 0) { cout << "Invalid array size." << endl; return 0; } int* nums = new int[n]; cout << "Enter the elements of the array: "; for (int i = 0; i < n; i++) { cin >> nums[i]; } quickSort(nums, 0, n - 1); cout << "Sorted array: "; for (int i = 0; i < n; i++) { cout << nums[i] << " "; } cout << endl; delete[] nums; return 0; }
归并排序 #include <iostream> using namespace std; void Merge(int arr[], int temp[], int left, int right, int mid) { int i = left; int j = mid + 1; int k = 0; while (i <= mid && j <= right) { if (arr[i] <= arr[j]) { temp[k++] = arr[i++]; } else { temp[k++] = arr[j++]; } } while (i <= mid) { temp[k++] = arr[i++]; } while (j <= right) { temp[k++] = arr[j++]; } for (int i = 0; i < k; i++) { arr[left + i] = temp[i]; } } void MergeSort(int arr[], int left, int right) { if (left >= right) { return; } int mid = left + (right - left) / 2; MergeSort(arr, left, mid); MergeSort(arr, mid + 1, right); int* temp = new int[right - left + 1]; Merge(arr, temp, left, right, mid); delete[] temp; } int main() { int n; cin >> n; int* nums = new int[n]; int num; for (int i = 0; i < n; i++) { cin >> num; nums[i] = num; } MergeSort(nums, 0, n - 1); for (int i = 0; i < n; i++) { cout << nums[i] << " "; } delete[] nums; return 0; }
四、分治法之查找问题
二分查找(递归)
二分搜索 /* 给定已按降序排好的n个元素a[0:n-1],在这n个元素中找出一特定元素x。 输入格式: 第一行为n值(n<=10^6)和查询次数m(m<=10^3); 第二行为n个整数。 接下来m个数,代表要查询的x 输出格式: 对于每一个查询的x,如果找到,输出x的下标;否则输出-1。 输入样例: 5 2 5 4 3 2 1 3 6 输出样例: 2 -1 */ #include <iostream> #include <algorithm> using namespace std; int binarySearch(int arrs[], int left, int right, int tag) {//参数传入搜索范围就不用每次while循环更改mid值 /*否则如果这样回传搜索范围就要更新 int left = 0; int right = n; int mid = left + (right - left) / 2; */ if (left > right) { return -1; // 未找到 } int mid = left + (right - left) / 2; if (arrs[mid] == tag) { return mid; // 找到目标元素,返回索引 } if (tag < arrs[mid]) { return binarySearch(arrs, left, mid - 1, tag); // 递归搜索左半部分 } if (tag > arrs[mid]) { return binarySearch(arrs, mid + 1, right, tag); // 递归搜索右半部分 } } int main() { int n, m; cin >> n >> m; int nums[n]; for (int i = 0; i < n; i++) { cin >> nums[i]; } int tags[m]; for (int i = 0; i < m; i++) { cin >> tags[i]; } int result[m]; // 使用二分查找前,先保证数组是排好序的 sort(nums, nums + n);//升序排列 for (int i = 0; i < m; i++) { result[i] = binarySearch(nums, 0, n - 1, tags[i]); } for (int i = 0; i < m; i++) { cout << result[i] << endl; } return 0; }
二分查找基础版(迭代)
class Solution { public: int search(vector<int>& nums, int target) { int left = 0; int right = nums.size() - 1; //注意数组的范围 int mid = (left + right) / 2; while(left<=right){ if(nums[mid] == target){ return mid; }else if(nums[mid] > target){ right = mid - 1; //注意查找的范围,既然不等,就不用把mid包含进去 }else{ left = mid + 1; } mid = (left + right) / 2; } return -1; } };
二分搜索返回数组(指针)
//在 C++ 中,函数不能直接返回数组。你可以使用指针或者容器来表示数组。在你的情况下,你可以返回一个 `int*` 指针来表示数组。这个指针指向动态分配的内存,可以在函数外部进行释放。 //另外,C++17 引入了 `std::array` 和 `std::vector` 这样的标准库容器,它们可以更方便地表示数组,并且具有更好的安全性和易用性。你可以考虑使用它们来代替裸指针。 //下面是使用 `int*` 指针表示数组的代码: #include <iostream> using namespace std; int* binarySearch(int arr[], int target, int left, int right) { int* result = new int[2]; int middle; while (left <= right) { int mid = left + (right - left) / 2; if (arr[mid] == target) { result[0] = mid; result[1] = mid; return result; } if (arr[mid] < target) { left = mid + 1; } else { right = mid - 1; } middle = mid; } // 目标值未找到 result[0] = middle;//result[0] = right; result[1] = middle;//result[1] = left; return result; } int main() { int n, x; cin >> n >> x; int* data = new int[n]; for (int i = 0; i < n; i++) { cin >> data[i]; } if (x < data[0]) { cout << -1 << " " << 0 << endl; delete[] data; return 0; } if (x > data[n - 1]) { cout << n - 1 << " " << n << endl; delete[] data; return 0; } int* num = binarySearch(data, x, 0, n - 1); cout << num[0] << " " << num[1] << endl; delete[] data; delete[] num; return 0; } //希望这可以帮助你理解如何在 C++ 中返回数组。
算法第三章 贪心算法
一、贪心算法的思想
贪心算法:在每一次选择中选择当前情况的最优解,达到整体问题的最优解 值得注意的是,贪心问题必须满足 1.贪心选择性质 贪心选择性质指的是,通过贪心策略所做出的每一个局部最优的选择,都能够组合成一个全局最优解。换句话说 就是贪心算法每次选择的都是当前状态下最优的策略,而这些最优的策略组合在一起,可以形成最终的最优解。 2.最优子问题 最优子结构指的是,问题的最优解可以通过子问题的最优解构建出来,换句话说,问题可以被分解成更小的子问题,子 问题的最优解可以构成原问题的最优解。这个性质是动态规划和贪心算法设计的前提之一。
二、例题
完全背包问题 /* 7-35 背包问题 分数 25 作者 郑琪 单位 广东外语外贸大学 给定n(n<=100)种物品和一个背包。物品i的重量是wi,价值为vi,背包的容量为C(C<=1000)。问:应如何选择装入背包中的物品,使得装入背包中物品的总价值最大? 装入背包的物品可以只装入部分物品。 输入格式: 共有n+1行输入: 第一行为n值和c值,表示n件物品和背包容量c; 接下来的n行,每行有两个数据,分别表示第i(1≤i≤n)件物品的重量和价值。 输出格式: 输出装入背包中物品的最大总价值。 输入样例: 5 10 2 6 2 3 6 5 5 4 4 6 输出样例: 16.67 */ #include <algorithm> #include <iostream> using namespace std; struct Goods { double weight; double value; }goods[105]; bool cmp(Goods a, Goods b) { if (a.value / a.weight > b.value / b.weight) return true; else return false; } int main() { int n; double c; //背包容量 cin >> n >> c; for (int i = 0; i < n; i++) { cin >> goods[i].weight >> goods[i].value; } sort(goods, goods + n, cmp); double VALUE = 0; int i = 0; while (c > 0 && i < n) { if (c >= goods[i].weight) { VALUE += goods[i].value; c -= goods[i].weight; i++; } else { VALUE += c * (goods[i].value / goods[i].weight); break; } } printf("%.2f\n", VALUE); return 0; }
迪杰斯特拉算法
#include <bits/stdc++.h> #define MAX 11 using namespace std; int t[MAX][MAX]; //邻接矩阵 表示一个地点到另一个地点花费的时间 int s[MAX]; //经过点的集合 赋值为1则在集合中 赋值为0则不在集合中 int dist[MAX];//特定地点到各个点的最短时间 int main(){ int i; int N, M, D;//D代表有向无向 cin >> N >> M >> D; memset(t, 0x3f3f3f, sizeof t);//给每个邻接矩阵赋很大的值 for(i = 1; i <= M; i++){ int v, u, w; cin >> v >> u >> w; t[v][u] = w; if(!D)//如果是无向图,返过来也要赋值 t[u][v] = w; } memset(s, 0, sizeof s);//集合置为空 int sourse;//原点 cin >> sourse; s[sourse] = 1; //memset(dist, 0x3f, sizeof s); for(i = 1; i <= N; i++) dist[i] = t[sourse][i]; dist[sourse] = 0; for(int k = 1; k <= N; k++){ int minv = 0x3f3f3f; int mini; for(i = 1; i <= N; i++){ if(s[i] == 0 && dist[i] < minv){//不在集合中 mini = i; minv = dist[i]; } } s[mini] = 1; for(i = 1; i <= N; i++) dist[i] = min(dist[i], dist[mini] + t[mini][i]); } for(i = 1; i <= N; i++){ cout << sourse << "->" << i << ":"; if(dist[i] < 0x3f3f3f) cout << dist[i] << endl; else cout << "no path" << endl; } return 0; }
算法第五章 回溯法
1.回溯法的思想
有许多问题,当需要找出它的解集或者要求回答什么解是满足某些约束条件的最佳解时,往往要使用回溯法。 回溯法的基本做法是搜索,或是一种组织得井井有条的,能避免不必要搜索的穷举式搜索法。这种方法适用于解一些组合数相当大的问题。 回溯法在问题的解空间树中,按深度优先策略,从根结点出发搜索解空间树。算法搜索至解空间树的任意一点时,先判断该结点是否包含问题的解。如果肯定不包含,则跳过对该结点为根的子树的搜索,逐层向其祖先结点回溯;否则,进入该子树,继续按深度优先策略搜索。 若用回溯法求问题的所有解时,要回溯到根,且根结点的所有可行的子树都要已被搜索遍才结束。 而若使用回溯法求任一个解时,只要搜索到问题的一个解就可以结束。
-
解空间
1、问题的解空间 复杂问题常常有很多的可能解,这些可能解构成了问题的解空间。解空间也就是进行穷举的搜索空间,所以,解空间中应该包括所有的可能解。确定正确的解空间很重要,如果没有确定正确的解空间就开始搜索,可能会增加很多重复解,或者根本就搜索不到正确的解。 例如,对于有n个物品的0/1背包问题,当n=3时,其解空间是: {(0, 0, 0), (0, 0, 1), (0, 1, 0), (1, 0, 0), (0, 1, 1), (1, 0, 1), (1, 1, 0), (1, 1, 1) }
-
基本步骤
-
剪枝函数
常用剪枝函数: 用约束函数在扩展结点处剪去不满足约束的子树; 用限界函数剪去得不到最优解的子树;
子集树和排列树
- 子集树:当所给的问题是从n个元素的集合S中找出满足某种性质的子集时,相应的解空间树成为子集树。例:0-1背包问题。
- 排列树:当所给问题是确定n个元素的满足排列树:当所给问题是确定n个元素的满足某种性质的排列时,相应的解空间树称为排列树。例:旅行售货员问题。
2. 例题
0-1背包
#include <iostream> #include <algorithm> using namespace std; const int MAX = 100; int result[MAX]; //最优解 int x[MAX];//当前节点的路径 double maxV;//最优解值 double cr;//剩余容量 double cv = 0;//当前价值 int n, c;//n个物品, 背包容量c struct Good{ int index; double weight; double value; }goods[MAX]; bool cmp(Good a,Good b) { return a.value / a.weight > b.value / b.weight; } /*int bound(int t) { double temp = cv; for (int i = t + 1; i <= n; i++) { cv += goods[i].value; } cv = temp; return cv; }*/ //限界函数 double bound(int i) { double cleft = cr; double b = cv; while (i <= n && goods[i].weight < cleft) { b += goods[i].value; cleft -= goods[i].weight; i++; } if (i <= n) { b += goods[i].value * cleft / goods[i].weight; } return b; } void Backtrack(int t) {//以深度优先的方式遍历第t层中的某棵子树(第t层就是选第t个物品) if (t > n) { //遍历到叶子节点了 if (cv > maxV) { maxV = cv; for (int i = 1; i <= n; i++) { result[i] = x[i]; } } return; } if (cr >= goods[t].weight)//该点选择物品时 { cr -= goods[t].weight; cv += goods[t].value; x[t] = 1; Backtrack(t + 1);//遍历完当前节点子树,回到父节点状态 cr += goods[t].weight; cv -= goods[t].value; } //如果满足限界条件进入右子树 if (bound(t + 1) > maxV) { x[t] = 0; Backtrack(t + 1); } } int main() { cin >> n >> c; for (int i = 1; i <= n; i++) { cin >> goods[i].weight >> goods[i].value; goods[i].index = i; } cr = c; sort(goods + 1, goods + 1 + n, cmp); //左不选0 右选1 Backtrack(1); cout << maxV << endl; cout << endl; for (int i = 1; i <= n; i++) { printf("第%d个物品%d\n", goods[i].index, result[i]); } } //二叉树 //叶子节点有2n个, 节点有2^n-1个 //算法时间复杂度为O(2^n)
子集和(子集树)
子集和问题(找到序列中子集和为v并输出) #include <iostream> using namespace std; const int MAX = 1000; int n,v; //数目 子集和 int a[MAX];//数据数组 int ans[MAX];//解集 int cv; //剩余价值 bool flag;//找到解标志记1 void Backsearch(int t) { if(cv == 0) flag = 1;//剩余价值为0 if(t>n || flag == 1) return;//遍历到叶子节点或者找到解就结束这次回溯 if(flag == 0 && a[t] <= cv) {//未找到解且能装下当前带选择数 cv -= a[t]; ans[t] = a[t]; Backsearch(t+1);//向下遍历,没找到解才回归父节点状态 if(flag==0) cv += a[t]; if(flag==0) ans[t] = 0; } if(flag==0) { // 装不下,遍历右子树 Backsearch(t+1); } } int main() { cin >> n >> v; for(int i = 1; i <= n; i++) { cin >> a[i]; } flag = 0; cv = v;//初始化剩余价值 Backsearch(1); if(flag == 1) { for(int i = 1; i <= n; i++) { if(ans[i] != 0) cout<< ans[i] <<" ";//输出解 } } else cout<<"No Solution!"; return 0; }
居民部落问题
#include <iostream> using namespace std; int R[201][201] = {0}; //关系矩阵 int x[201] = {0}, cx[201] = {0}; //x[i]=1表示居民在卫队中,反之不在 int n, m; //n是人数,m是仇敌关系数量 int max_num = 0, cmax = 0; //卫队中居民人数 bool Bound(int t1) //约束函数:当前的居民在卫队中中是否有仇敌关系 { int j; for (j=1; j<t1; j++) { if (cx[j]==1 && R[t1][j]==1) { return false; } } return true; } void Back(int t) { if (t>n) { if (max_num < cmax) { max_num = cmax; for (int i=1; i<=n; i++) { x[i] = cx[i]; } } return; } //1 if (Bound(t) == true) //当前居民在卫队中没有找到仇人 { cx[t] = 1; cmax++; Back(t+1); cmax--; cx[t] = 0; } //0 if (cmax+n-t >= max_num) //剪枝:再往下找,考虑理想情况,卫队居民人数也不可能比当前最大的多了 { Back(t+1); } } int main() { int u, v; cin >> n >> m; for (int i=1; i<=m; i++) { cin >> u >> v; R[u][v] = 1; R[v][u] = 1; //对称矩阵 } Back(1); cout << max_num << endl; for (int i=1; i<=n; i++) //编号从1开始 { cout << x[i] << " "; } return 0; }
旅行售货员问题
/* 7-1 旅行售货员 某售货员要到若干城市去推销商品,已知各城市之间的路程(或旅费)。他要选定一条从驻地出发,经过每个城市一遍,最后回到驻地的路线,使总的路程(或总旅费)最小。 输入格式: 第一行为城市数n 下面n行n列给出一个完全有向图,如 i 行 j 列表示第 i 个城市到第 j 个城市的距离。 输出格式: 一个数字,表示最短路程长度。 输入样例: 3 0 2 1 1 0 2 2 1 0 输出样例: 3 */ #include <bits/stdc++.h> using namespace std; const int N = 105; int g[N][N]; int x[N]; int n; int ans = 0x3f3f3f3f; int now = 0; void backtrack(int t) {//第t层表示已经到了第t - 1个城市 if (t > n) {//叶子节点,此时在第n个城市 if (now + g[x[n]][x[1]] < ans) { ans = now + g[x[n]][x[1]];//回到驻点 } } else { for (int i = t; i <= n; i++) { if (g[x[t - 1]][x[i]] != 0x3f3f3f3f && now + g[x[t - 1]][x[i]] < ans) { swap(x[t], x[i]); now += g[x[t - 1]][x[t]]; backtrack(t + 1); now -= g[x[t - 1]][x[t]]; swap(x[t], x[i]); } } } } int main() { cin >> n; memset(g, 0x3f, sizeof(g)); for (int i = 1; i <= n; i++) { for (int j = 1; j <= n; j++) { cin >> g[i][j]; } } for (int i = 1; i <= n; i++) x[i] = i;//初始化为1 2 3 4 backtrack(2);//从第二层开始遍历,第一层就是从驻地到其他城市 cout << ans; return 0; } //不剪枝时间复杂度为O(n!)
n后问题
在n×n格的棋盘上放置彼此不受攻击的n个皇后。按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。n后问题等价于在n×n格的棋盘上放置n个皇后,任何2个皇后不放在同一行或同一列或同一斜线上 输入格式: 一个数字n 输出格式: 按照深度优先输出所有可行的解 输入样例: 4 输出样例: 2 4 1 3 3 1 4 2
解法一: #include <iostream> #include <cmath> using namespace std; const int MAX = 100; int n; int x[MAX]; bool fuc(int t) { for(int i = 1; i < t; i++) { if(x[t] == x[i] || abs(x[t] - x[i]) == abs(t - i)) { return false; } } return true; } void backtrack(int t) { if(t > n) { for(int i = 1; i <= n; i++) { cout << x[i] << " "; } cout << endl; } for(int i = 1; i <= n; i++) { x[t] = i; if(fuc(t)) { backtrack(t + 1); } } } int main() { cin >> n; backtrack(1); return 0; } 解法二: /* 在n×n格的棋盘上放置彼此不受攻击的n个皇后。按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。n后问题等价于在n×n格的棋盘上放置n个皇后,任何2个皇后不放在同一行或同一列或同一斜线上 输入格式: 一个数字n 输出格式: 按照深度优先输出所有可行的解 输入样例: 4 输出样例: 2 4 1 3 3 1 4 2 */ #include <iostream> #include <cmath> using namespace std; const int MAX = 100; int n; int chosen[MAX] = { 0 }; int result[MAX]; bool check(int t) { for (int i = 1; i < t; i++) { if (abs(t - i) == abs(result[t] - result[i]) || result[t] == result[i]) { return false; } } return true; } void backtrack(int t) { if (t > n) { for (int i = 1; i <= n; i++) { cout << result[i] << " "; } cout << endl; } for (int i = 1; i <= n; i++) { if (chosen[i] == 0) { chosen[i] = 1; result[t] = i; if (check(t)) { backtrack(t + 1); } chosen[i] = 0; } } } int main() { cin >> n; backtrack(1); return 0; }
最小机器重量问题
/* 7-2 最小重量机器设计问题 设某一机器由n个部件组成,每一种部件都可以从m个不同的供应商处购得。设wij是从供应商j 处购得的部件i的重量,cij是相应的价格。 试设计一个算法,给出总价格不超过d的最小重量机器设计。 输入格式: 第一行有3 个正整数n ,m和d, 0<n<30, 0<m<30, 接下来的2n 行,每行m个数。前n行是c,后n行是w。 输出格式: 输出计算出的最小重量,以及每个部件的供应商 输入样例: 3 3 4 1 2 3 3 2 1 2 2 2 1 2 3 3 2 1 2 2 2 输出样例: 在这里给出相应的输出。例如: 4 1 3 1 */ #include<iostream> using namespace std; int n, m, cost; //限定价格 部件数 供应商数 int w[100][100];//w[i][j]为第i个零件在第j个供应商的重量 int c[100][100];//c[i][j]为第i个零件在第j个供应商的价格 int bestx[100];//bestx[i]用来存放第i个零件的最后选择供应商 int x[100];//x[i]临时存放第i个零件的供应商 int cw = 0, cc = 0, bestw = 100000; void Backtrack(int t) // t对应 部件t { if (t > n)//搜索到叶子结点,一个搜索结束,所有零件已经找完 { if (cw < bestw) { bestw = cw; //当前最小重量 for (int j = 1; j <= n; j++) bestx[j] = x[j]; } // return; // 有else就不需要 return,两个选一个 } else { for (int i = 1; i <= m; i++) // 遍历所有供应商 { cc += c[t][i]; cw += w[t][i]; x[t] = i; if (cc <= cost && cw <= bestw) // 剪枝操作 Backtrack(t + 1); cc -= c[t][i]; cw -= w[t][i]; } } } int main() { cin >> n >> m >> cost; for (int i = 1; i <= n; i++) //各部件在不同供应商的重量 cij:物品i在供应商j的价格 for (int j = 1; j <= m; j++) cin >> c[i][j]; for (int i = 1; i <= n; i++) //各部件在不同供应商的价格 wij:物品i在供应商j的重量 for (int j = 1; j <= m; j++) cin >> w[i][j]; Backtrack(1); cout << bestw << endl; // 最低的重量 for (int i = 1; i <= n; i++) // 输出各个部件的供应商 cout << bestx[i] << " "; return 0; }
算法第六章、分支限界法(广度优先或者最小生成树)
分支限界法的基本思想:
不断的广度优先遍历所有的层次
从下一扩展结点的不同方式导致不同的分支限界法。 1、FIFO分支限界法 将活结点表组织成为一个队列,按先进先出原则选择下一个结点。 2、优先队列分支限界法 按照优先队列中规定的优先级选取优先级最高的节点成为当前扩展节点。 - 最大优先队列:使用最大堆,体现最大效益优先 - 最小优先队列:使用最小堆,体现最小费用优先
例题
0-1背包问题(FIFO)
#include <iostream> #include <cstring> #include <queue> #include <algorithm> using namespace std; struct Node { int level; //该节点所在解空间树中的层次 int cw; //该节点的当前载重量 int cp; // 该节点的当前价值 }; queue <Node> q; // 定义队列 struct Obj { double value; double weight; };//每件物品的重量及价值 Obj objs[101]; //定义存储n件物品的数组,方便调用sort函数 int bestv = 0; int n, c; double bound(int t,double cleft) { double v = 0; for(int i = t; i <= n; i++) { if(objs[i].weight < cleft) { v+=objs[i].value; cleft=objs[i].weight; } else { v+= cleft*objs[i].value/objs[i].weight; } } return v; } void bfs() { Node node; node.cp = 0; node.cw = 0; node.level = 1; q.push(node); while(!q.empty()) { Node node = q.front(); q.pop(); if(node.level > n) { if(node.cp > bestv){ bestv = node.cp; } continue; } if (node.cw + objs[node.level].weight <= c) { //判断左分支是否入队 Node nextnode; nextnode.level = node.level + 1; nextnode.cw = node.cw + objs[node.level].weight; nextnode.cp = node.cp + objs[node.level].value; q.push( nextnode ); if ( bestv < nextnode.cp ) bestv = nextnode.cp; //及时刷新暂时最优值,提高效率 } if((node.cp + bound(node.level+1,c - node.cw)) > bestv) { //判断右分支是否入队 Node nextnode; nextnode.level = node.level + 1; nextnode.cw = node.cw; nextnode.cp = node.cp; q.push(nextnode); } } } bool cmp(Obj a, Obj b){ return a.value/a.weight > b.value/b.weight; } int main() { cin >> n>> c; for(int i = 1; i <= n; i++) { cin >> objs[i].weight >> objs[i].value; } sort(objs + 1, objs + 1 + n,cmp); bfs(); cout << bestv << endl; return 0; }
0-1背包(最小堆)
#include <iostream> #include <queue> #include <algorithm> #include <cstdio> using namespace std; struct Node { int level; //该节点所在解空间树中的层次 double cw; //该节点的当前载重量 double cv; // 该节点的当前价值 double uvalue; //该节点的上界 = 当前载重量 + 剩余容量的最大价值(采用背包问题的计算方法) friend bool operator < (Node a, Node b) { return a.uvalue < b.uvalue; } int x[110]; }; struct Obj { double value; double weight; int preindex; };//每件物品的重量及价值 const int MAXN = 110; int n, c, bestc; int bestx[MAXN]; Obj objs[MAXN]; priority_queue < Node > q; // 定义队列 bool cmp(Obj a, Obj b) { //用于sort排序时以单位重量价值降序排序 return a.value / a.weight > b.value / b.weight; } double bound(int t, double left) //利用背包问题(贪心算法)计算剩余容量为left,可选物品为第t~n件时可装入的最大价值 { double maxv = 0; while ( t <= n && left >= objs[t].weight) { maxv += objs[t].value; left -= objs[t].weight; t++; } if ( t <= n ) maxv += left * objs[t].value / objs[t].weight; return maxv; } int bfs() { //初始化根节点,加入队列 Node node; node.level = 1; node.cw = 0; node.cv = 0; node.uvalue = bound(1, c); q.push(node); while (!q.empty()) { Node node = q.top(); //上界最大者出堆 q.pop(); //采用优先级队列,如果优先访问叶子节点,说明该叶子节点的上界值要高于其他所有待扩展节点的上界, //由于该叶子节点的值与上界值相等,所以该叶子节点代表最优解,直接退出循环 if ( node.level > n ) { bestc = node.cv; for (int i = 0; i < n; i++) bestx[i] = node.x[i]; break; } //约束函数,如果第level个物品可以装入背包,则左分支节点进入优先队列 if ( node.cw + objs[node.level].weight <= c ) { Node nextnode; nextnode.level = node.level + 1; nextnode.cw = node.cw + objs[node.level].weight; nextnode.cv = node.cv + objs[node.level].value; nextnode.uvalue = node.uvalue; //复制父节点到根的路径 for (int i = 1; i < node.level; i++) nextnode.x[i] = node.x[i]; nextnode.x[node.level] = 1; //左孩子,路径为1 q.push(nextnode);//节点加入队列 } //限界函数,如果右分支上界大于最优的中间结果,则进入优先队列 double uvalue = bound(node.level+1, c - node.cw) + node.cv; if ( uvalue > bestc) { Node nextnode; nextnode.level = node.level + 1; nextnode.cw = node.cw; nextnode.cv = node.cv; nextnode.uvalue = uvalue; //复制父节点的到根的路径 for (int i = 1; i < node.level; i++) nextnode.x[i] = node.x[i]; nextnode.x[node.level] = 0;//右孩子,路径为0 q.push(nextnode); //节点加入队列 } } return bestc; } int main() { cin >> n >> c; for (int i = 1; i <= n; i++) { cin >> objs[i].weight >> objs[i].value; objs[i].preindex = i; } //按单位重量价值一次性降序排序,便于后续贪心法计算上界 sort( objs + 1, objs + 1 + n, cmp ); //输出最优值 cout << "best value:" << bfs() << endl; //输出最优解,所选物品的重量和价值 cout << "best plan:\nweight\tvalue" << endl; for (int i = 1; i <= n; i++) if (bestx[i] == 1) cout << "选择了第" << objs[i].preindex << "个物品" << objs[i].weight << "\t" << objs[i].value << endl; return 0; }