一 排序算法 工具函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 #pragma once #include <iostream> #include <cassert> #include <ctime> using namespace std ;namespace SortTestHelper { int * generateRandomArray (int n, int l, int r) { assert(l < r); int * arr = new int [n]; srand(time(NULL )); for (int i = 0 ; i < n; i++) { arr[i] = rand() % (r - l + 1 ) + l; } return arr; } template <typename T> void printArray (T arr[], int n) { for (int i = 0 ; i < n; i++) { cout << arr[i] << " " ; } cout << endl ; return ; } template <typename T> bool isSorted (T arr[], int n) { for (int i = 0 ; i < n - 1 ; i++) { if (arr[i] > arr[i + 1 ]) return false ; } return true ; } template <typename T> void testSort (string sortName, void (*sort)(T[], int ), T arr[], int n) { clock_t startTime = clock(); sort(arr, n); clock_t endTime = clock(); assert(isSorted(arr, n)); cout << sortName << " : " << double (endTime - startTime) / CLOCKS_PER_SEC << " s" << endl ; return ; } }
桶排序、计数排序、基数排序
https://blog.csdn.net/qq_55624813/article/details/121316256
桶排序
稳定排序
思路
时间复杂度
O(m*n/mlogn/m) = nlogn/m(划分m个桶), m越接近n则越接近O(n)
空间复杂度
应用场景
计数排序
稳定排序
思想
类似桶排序,不过区间大小为1
只适合范围比较小的数据,n个数据,最大值为k,需要k不能大于n太多,只能处理非负数
时间复杂度
空间复杂度
应用场景
基数排序
稳定排序
思想
按位排序,先排低位,再排高位,每次排序用基数排序,
适合位数不是特别大的数据
时间复杂度
O(k*n) k为位数,例如排序手机号,k = 11
空间复杂度
应用场景
选择排序
时间复杂度:O(n*n)
需要额外空间: O(1)
从数组第一个元素开始,在其后选择一个最小的元素与之交换,然后后移一格,再在其后选择一个最小的元素与之交换,以此类推
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 #include <iostream> #include "Student.h" #include "SortTestHelper.h" using namespace std ;template <typename T>void selectSort (T arr[], int n) { for (int i = 0 ; i < n; i++) { int minIndex = i; for (int j = i + 1 ; j < n; j++) { if (arr[j] < arr[minIndex]) { minIndex = j; } } swap(arr[i], arr[minIndex]); } } int main () { int a[10 ] = { 10 ,9 ,8 ,7 ,6 ,5 ,4 ,3 ,2 ,1 }; selectSort(a, 10 ); for (int i = 0 ; i < 10 ; i++) { cout << a[i] ; } cout << endl ; string c[3 ] = { "C" ,"B" , "A" }; selectSort(c, 3 ); for (int i = 0 ; i < 3 ; i++) { cout << c[i]; } cout << endl ; Student s[4 ] = { {"D" , 100 },{"C" ,95 },{"B" , 95 },{"A" , 91 } }; selectSort(s, 4 ); for (int i = 0 ; i < 4 ; i++) { cout << s[i]; } cout << endl ; int n = 10000 ; int * arr = SortTestHelper::generateRandomArray(n, 0 , n); selectSort(arr, n); SortTestHelper::printArray(arr, n); delete [] arr; return 0 ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <iostream> using namespace std ;struct Student { string name; int score; bool operator <(const Student& otherStudent) { return score != otherStudent.score ? score < otherStudent.score : name < otherStudent.name; } friend ostream& operator <<(ostream& os, const Student& student) { os << "Student: " << student.name << " " << student.score << endl ; return os; } };
插入排序
时间复杂度:O(n*n)
需要额外空间: O(1)
相对选择排序的优势: 内层循环可以提前终止
相对选择排序优势更明显的情况: 数组的有序性本身很高,这样内层循环就会更早提前终止
对于近乎有序的排序情况,插入排序比O(n*logn)的排序算法还要好
数组本身有序时,插入排序时间复杂度:O(N)
从第二个元素开始,依次往后移动,每次将当前元素插入到前面已经排序好的序列中正确的位置,即比自己大的就往后移动
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 #include <iostream> #include "Student.h" #include "SortTestHelper.h" using namespace std ;template <typename T>void selectSort (T arr[], int n) { for (int i = 0 ; i < n; i++) { int minIndex = i; for (int j = i + 1 ; j < n; j++) { if (arr[j] < arr[minIndex]) { minIndex = j; } } swap(arr[i], arr[minIndex]); } } template <typename T>void insertSort (T arr[], int n) { for (int i = 1 ; i < n; i++) { T temp = arr[i]; int j; for ( j = i; j > 0 && arr[j-1 ]>temp ; j--) { arr[j] = arr[j - 1 ]; } arr[j] = temp; } } int main () { int n = 10000 ; int * arr = SortTestHelper::generateNearlyOrderedArray(n, 10 ); int * arr2 = SortTestHelper::copyIntArray(arr, n); SortTestHelper::testSort("selectSort" , selectSort, arr, n); SortTestHelper::testSort("insertSort" , insertSort, arr2, n); delete [] arr; delete [] arr2; return 0 ; }
希尔排序
https://blog.csdn.net/yuan2019035055/article/details/120246584
希尔排序 (Shell Sort)是插入排序 的一种。也称缩小增量排序,是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定 排序算法。
时间复杂度:O(n*1.3), 最坏:O(n^2) 比选择和插入排序快
需要额外空间: O(1)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 template <typename T>void shellSort (T arr[], int n) { for (int step = n/2 ; step >0 ; step /= 2 ){ for (int i = 0 ; i < step ; i++){ for (int j = i + step ; j < n; j += step ){ T temp = arr[j]; int m; for (m = j-step ; m >= 0 && arr[m] > temp; m -= step ){ arr[m+step ] = arr[m]; } arr[m+step ] = temp; } } } } int main () { int vec[] = {12 ,34 ,5 ,632 ,5 ,689 ,34 ,67 ,89 }; shellSort(vec, 9 ); for (int i = 0 ; i < 9 ; i++){ cout <<vec[i]<<" " ; } cout <<endl ; }
归并排序
时间复杂度:O(n*logn)
需要额外空间: O(n)
自顶向下
自底向上 适合链表的归并,因为对数组的索引操作比较少
优化:
1: 拆分的最小单元用插入排序进行排序
2: 已经有序的数组则不需要进行合并操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 #include <iostream> using namespace std ;template <typename T>void __merge(T arr[], int l, int mid, int r) { T* aux = new int [r - l + 1 ]; for (int i = l; i <= r; i++) { aux[i - l] = arr[i]; } int i = l; int j = mid + 1 ; for (int k = l; k <= r; k++) { if (i > mid) { arr[k] = aux[j - l]; j++; } else if (j > r) { arr[k] = aux[i - l]; i++; } else if (aux[i - l] < aux[j - l]) { arr[k] = aux[i - l]; i++; } else { arr[k] = aux[j - l]; j++; } } delete [] aux; } template <typename T>void __mergeSort(T arr[], int l, int r) { if (r - l <= 15 ) { insertSort(arr, l, r); return ; } int mid = (l + r) / 2 ; __mergeSort(arr, l, mid); __mergeSort(arr, mid + 1 , r); if (arr[mid] > arr[mid + 1 ]) __merge(arr, l, mid, r); } template <typename T>void mergeSort (T arr[], int n) { __mergeSort(arr, 0 , n - 1 ); } template <typename T>void mergeSortBU (T arr[], int n) { for (int i = 0 ; i < n; i += 16 ) { insertSort(arr, i, min (i + 15 , n - 1 )); } for (int sz = 16 ; sz < n; sz += sz) { for (int i = 0 ; i + sz < n; i += sz + sz) { if (arr[i+sz-1 ]>arr[i+sz]) __merge(arr, i, i + sz - 1 , min (i + sz + sz - 1 , n - 1 )); } } }
归并求解逆序对
https://blog.csdn.net/qq_41550842/article/details/81215935
讲解更清晰:https://blog.csdn.net/Keep_Trying_Go/article/details/127175000
逆序对:一个数组中,后面比前面大的数字对有多少个
只需要每次比较的时候记录后面放到前面去了,则逆序对加一
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 #include <iostream> using namespace std ;template <typename T>int __merge(T arr[], int l, int mid, int r){ int * temp = new int [r-l+1 ]; for (int i = 0 ; i < r-l+1 ; ++i){ temp[i] = arr[i+l]; } int i = 0 ; int j = mid-l+1 ; int k = l; int count = 0 ; while (i <= mid -l || j <= r-l) { if (i > mid -l){ arr[k++] = temp[j++]; }else if (j > r-l){ arr[k++] = temp[i++]; }else if (temp[i] <= temp[j]){ arr[k++] = temp[i++]; }else { arr[k++] = temp[j++]; count += mid-l+1 - i; } } delete [] temp; return count; } template <typename T>int repair (T arr[], int n) { int count = 0 ; for (int sz = 1 ; sz < n; sz += sz){ for (int i = 0 ; i + sz < n; i += sz+sz){ count += __merge(arr, i, i+sz-1 , min (i+sz+sz-1 , n-1 )); } } return count; } template <typename T>int repairForce (T arr[], int n) { int count = 0 ; for (int i = 0 ; i < n; ++i){ for (int j = i+1 ; j< n; ++j){ if (arr[j] < arr[i]){ count++; } } } return count; }
快速排序
时间复杂度:O(n*logn)
需要额外空间: O(logn)
重复元素较多的情况使用三路快排
快排优化版本
优化1:最小单元的排序使用插入排序
优化2:分割位置的元素随机选择, (不随机的情况下,近乎有序数组的排序时间复杂度就会退化到O(n*n))
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 #include <iostream> #include <ctime> using namespace std ;template <typename T>int __partition(T arr[], int l, int r) { swap(arr[l], arr[rand() % (r - l + 1 ) + l]); T v = arr[l]; int j = l; for (int i = l + 1 ; i <= r; i++) { if (arr[i] < v) { swap(arr[j + 1 ], arr[i]); j++; } } swap(arr[l], arr[j]); return j; } template <typename T>void __quickSort(T arr[], int l, int r) { if (r - l <= 15 ) { insertSort(arr, l, r); return ; } int p = __partition(arr, l, r); __quickSort(arr, l, p - 1 ); __quickSort(arr, p + 1 , r); } template <typename T>void quickSort (T arr[], int n) { srand(time(NULL )); __quickSort(arr, 0 , n - 1 ); }
两路快排
用于解决重复元素很多的情况
重复元素很多的情况下,划分的结果也会很不平衡,导致树的深度接近n,时间复杂度就会退化到O(n*n))
排序完成后,两边都包含了等于v的部分,v正好放在中间,两边划分就比较平衡
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 #include <iostream> using namespace std ;template <typename T>int __partition2(T arr[], int l, int r) { swap(arr[l], arr[rand() % (r - l + 1 ) + l]); T v = arr[l]; int i = l + 1 ; int j = r; while (true ) { while (i <= r && arr[i] < v) { i++; } while (j>= l+1 && arr[j] > v) { j--; } if (i> j) break ; swap(arr[i], arr[j]); i++; j--; } swap(arr[l], arr[j]); return j; } template <typename T>void __quickSort2(T arr[], int l, int r) { if (r - l <= 15 ) { insertSort(arr, l, r); return ; } int p = __partition2(arr, l, r); __quickSort2(arr, l, p - 1 ); __quickSort2(arr, p + 1 , r); } template <typename T>void quickSort2 (T arr[], int n) { srand(time(NULL )); __quickSort2(arr, 0 , n - 1 ); }
三路快排
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 #pragma once #include <iostream> using namespace std ;template <typename T>void __quickSort3(T arr[], int l, int r) { if (r - l <= 15 ) { insertSort(arr, l, r); return ; } swap(arr[l], arr[rand() % (r - l + 1 ) + l]); T v = arr[l]; int lt = l; int gt = r + 1 ; int i = l + 1 ; while (i < gt) { if (arr[i] < v) { swap(arr[i], arr[lt + 1 ]); i++; lt++; } else if (arr[i] > v) { swap(arr[gt - 1 ], arr[i]); gt--; } else { i++; } } swap(arr[l], arr[lt]); __quickSort3(arr, l, lt - 1 ); __quickSort3(arr, gt, r); } template <typename T>void quickSort3 (T arr[], int n) { srand(time(NULL )); __quickSort3(arr, 0 , n - 1 ); }
快排求解第n大元素
正确性待检验
使用一路或二路快排,每次排完的p都正好在整个数组排好序的位置上,如果查找nmax == p 则可以提前终止排序
而且排序后arr[l,p-1] <= arr[p] arr[p+1, r] >= arr[p]
如果nmax != p
也只需要大小判断后选择左右某一部分进入递归查找即可 类似二分查找
时间复杂度: n + n/2 + n/4 + n/8 + n/16 + …. + 1 = O(2n) = O (n)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 #include <iostream> using namespace std ;template <typename T>int partion1 (T arr[], int l, int r) { swap(arr[l], arr[rand()%(r-l+1 )+l]); T v = arr[l]; int i = l+1 ; int j = r; while (i < j) { while (arr[i] < v && i<=r) ++i; while (arr[j] > v && j >= l+1 ) --j; swap(arr[i++], arr[j--]); } swap(arr[l], arr[j]); return j; } template <typename T>T __quickSort(T arr[], int l, int r, int nMax){ int p = partion1(arr, l, r); if (p == nMax){ cout <<"p: " <<p<<endl ; return arr[p]; }else if ( p < nMax){ __quickSort(arr, p+1 , r, nMax); }else { __quickSort(arr, l, p-1 , nMax); } } template <typename T>T nthmax (T arr[], int n, int nMax) { srand(time(NULL )); __quickSort(arr, 0 , n-1 , nMax); }
堆排序
时间复杂度:O(n*logn)
需要额外空间: O(1)
堆必须是一颗完全二叉树
完全二叉树: 只有最后一层是不满的,其它层都是满节点
堆排序大多用于动态数据的维护,而不用于系统级别的排序,因为比归并和快排慢
构建堆
堆排序
将n个元素逐个插入到一个空堆中,时间复杂度是O(nlogn)
heapify的过程,时间复杂度为O(n), 时间复杂度证明较复杂,需要数学证明,记住结论即可
heapify就是原地构造堆,在原来的数组上构造堆,从第一个非叶子节点开始shifDown,每次shifDown后以这个节点为根的子树就符合最大堆要求了
为什么不是从0开始shiftDown,举例即可知道:15,17,19,13,22,16,28,30,41,62
构建最大堆,可以实现升序排序也可以实现逆序排序
升序排序: 将依次取出的堆顶元素按数组正序写入
逆序排序: 将依次取出的堆顶元素按数组逆序写入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 #include <iostream> #include <cassert> using namespace std ;template <typename Item>class MaxHeap {private : Item* data; int count; int capacity; void shiftUp (int k) { while (k > 0 && data[k / 2 ] < data[k]) { swap(data[k / 2 ], data[k]); k /= 2 ; } } void shiftDown (int k) { while (2 * k <= count-1 ) { int j = 2 * k; if (j + 1 <= count-1 && data[j + 1 ] > data[j]) j++; if (data[k] >= data[j]) break ; swap(data[k], data[j]); k = j; } } public : MaxHeap(int capacity) { data = new Item[capacity ]; count = 0 ; this ->capacity = capacity; } MaxHeap(Item arr[], int n) { data = new Item[n]; capacity = n; for (int i = 0 ; i < n; i++) data[i] = arr[i]; count = n; for (int i = count-1 / 2 ; i >= 0 ; i--) shiftDown(i); } ~MaxHeap() { delete [] data; } int size () { return count; } bool isEmpty () { return count == 0 ; } void insert (Item item) { assert(count < capacity); data[count] = item; shiftUp(count); count++; } Item extractMax () { assert(count > 0 ); Item ret = data[0 ]; swap(data[0 ], data[count-1 ]); count--; shiftDown(0 ); return ret; } Item getMax () { assert(count > 0 ); return data[0 ]; } }; template <typename T>void heapSort1 (T arr[], int n) { MaxHeap<T> maxheap = MaxHeap<T>(n); for (int i = 0 ; i < n; i++) { maxheap.insert(arr[i]); } for (int i = n - 1 ; i >= 0 ; i--) { arr[i] = maxheap.extractMax(); } } template <typename T>void heapSort2 (T arr[], int n) { MaxHeap<T> maxheap = MaxHeap<T>(arr, n); for (int i = n - 1 ; i >= 0 ; i--) { arr[i] = maxheap.extractMax(); } }
索引堆
引入索引堆的原因
不是int型的数据,交换位置很耗费性能,例如string类型
堆排序后,查找元素不好索引,只能遍历数组查找,时间复杂度上升为O(n)
索引堆的实现
堆排序的所有操作都用int型的索引(index)来实现,数据本身(data)位置不变
注意事项:
一定注意count的值是否是预期的值,还有边界值,一定要明确,如果不清楚就用实例测试
不用swap交换时,要注意用temp记录比较的值 (shiftUp shiftDown中使用时)
reverse的引入
引入reverse的意义: 当data中的数据修改后,就需要重新调整堆,那么就需要知道修改的这元素在index中是在什么位置,以便调用shiftUp()或shiftDown()来调整堆
例如修改了data[4] = 26, 那么就需要知道data[4]的索引4
在index中存储的位置,而reverse就保存了这个位置,即reverse[4]=9,
data[index[9]] = 26, 所以拿到索引9
后执行shiftUp(9)或shiftDown(9)
即可完成堆的调整
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 #include <iostream> #include <cassert> using namespace std ;template <typename Item>class MaxHeap {private : Item* data; int * index; int count; int capacity; void shiftUp (int k) { while (k > 0 && data[index[k / 2 ]] < data[index[k]]) { swap(index[k / 2 ], index[k]); reverse[index[k / 2 ]] = k / 2 ; reverse[index[k]] = k; k /= 2 ; } } void shiftDown (int k) { while (2 * k <= count - 1 ) { int j = 2 * k; if (j + 1 <= count - 1 && data[index[j + 1 ]] > data[index[j]]) j++; if (data[index[k]] >= data[index[j]]) break ; swap(index[k], index[j]); reverse[index[k]] = k; reverse[index[j]] = j; k = j; } } bool isInHeap (int i) { assert(i >= 0 && i < capacity); return reverse[i] != -1 ; } public : MaxHeap(int capacity) { data = new Item[capacity]; index = new int [capacity]; reverse = new int [capacity]; for (int i = 0 ; i < capacity; i++) { reverse[i] = -1 ; } count = 0 ; this ->capacity = capacity; } MaxHeap(Item arr[], int n) { data = new Item[n]; index = new int [n]; capacity = n; for (int i = 0 ; i < n; i++) { data[i] = arr[i]; index[i] = i; } count = n; for (int i = count - 1 / 2 ; i >= 0 ; i--) shiftDown(i); } ~MaxHeap() { delete [] data; delete [] index; delete [] reverse; } int size () { return count; } bool isEmpty () { return count == 0 ; } void insert (int i, Item item) { assert(count < capacity); assert(i >= 0 && i < capacity); data[i] = item; index[count] = i; reverse[i] = count; shiftUp(count); count++; } Item extractMax () { assert(count > 0 ); Item ret = data[index[0 ]]; swap(index[0 ], index[count - 1 ]); reverse(index[0 ]) = 0 ; reverse(index[count - 1 ]) = -1 ; count--; shiftDown(0 ); return ret; } Item getMax () { assert(count > 0 ); return data[index[0 ]]; } int getMaxIndex () { assert(count > 0 ); return index[0 ]; } Item getItem (int i) { assert( isInHeap(i)); return data[i]; } void change (int i, Item newItem) { assert(isInHeap(i)); data[i] = newItem; int j = reverse[i]; shiftUp(j); shiftDown(j); } }; template <typename T>void heapSort1 (T arr[], int n) { MaxHeap<T> maxheap = MaxHeap<T>(n); for (int i = 0 ; i < n; i++) { maxheap.insert(arr[i]); } for (int i = n - 1 ; i >= 0 ; i--) { arr[i] = maxheap.extractMax(); } } template <typename T>void heapSort2 (T arr[], int n) { MaxHeap<T> maxheap = MaxHeap<T>(arr, n); for (int i = n - 1 ; i >= 0 ; i--) { arr[i] = maxheap.extractMax(); } } #include <iostream> using namespace std ;template <typename T>class IndexMaxHeap { private : int count; int capacity; T* data; int * index; int * rev; void shiftUp (int k) { T temp = data[index[k]]; int tempIndex = index[k]; while (k>0 && data[index[(k-1 )/2 ]] < temp){ index[k] = index[(k-1 )/2 ]; rev[index[k]] = k; k = (k-1 )/2 ; } index[k] = tempIndex; rev[tempIndex] = k; } void shiftDown (int k) { int tempIndex = index[k]; T temp = data[tempIndex]; while (2 *k+1 < count) { int j = 2 *k+1 ; if (j+1 < count && data[index[j+1 ]] > data[index[j]]) ++j; if (data[index[j]] <= temp) break ; index[k] = index[j]; rev[index[k]] = k; k = j; } index[k] = tempIndex; rev[tempIndex] = k; } bool isInHeap (int k) { return rev[k] != INT32_MAX; } public : IndexMaxHeap(int capacity){ this ->capacity = capacity; count = 0 ; data = new T[capacity]; index = new int [capacity]; rev = new int [capacity]; for (int i = 0 ; i < capacity; ++i){ rev[i] = INT32_MAX; } } ~IndexMaxHeap(){ delete [] data; delete [] index; delete [] rev; } int getSize () { return count; } bool isEmpty () { return count == 0 ; } void insert (T item) { assert(count<capacity); data[count] = item; index[count] = count; rev[count] = count; shiftUp(count); ++count; } T extractMax () { T res = data[index[0 ]]; swap(index[0 ], index[--count]); rev[index[0 ]] = 0 ; rev[index[count]] = INT32_MAX; shiftDown(0 ); return res; } void change (int i, T item) { assert(isInHeap(i)); data[i] = item; int k = rev[i]; shiftDown(k); shiftUp(k); } void print () { cout <<" : " ; for (int i = 0 ; i< capacity; ++i){ cout <<i << "\t" ; } cout <<endl ; cout <<"index: " ; for (int i = 0 ; i< count; ++i){ cout <<index[i] << "\t" ; } cout <<endl ; cout <<"data : " ; for (int i = 0 ; i< capacity; ++i){ cout <<data[i] << "\t" ; } cout <<endl ; cout <<"rev : " ; for (int i = 0 ; i< capacity; ++i){ if (rev[i] == INT32_MAX){ cout << "x" <<"\t" ; }else { cout <<rev[i] << "\t" ; } } cout <<endl ; cout <<"max : " ; for (int i = 0 ; i< count; ++i){ cout <<data[index[i]] << "\t" ; } cout <<endl ; cout <<"----------------------------------------------------------------" <<endl ; } }; template <typename T>void IndexHeapSort (T arr[], int n) { IndexMaxHeap<int > heap = IndexMaxHeap<int >(n); for (int i = 0 ; i< n; ++i){ heap.insert(arr[i]); } for (int i = 0 ; i< n; ++i){ arr[i] = heap.extractMax(); } }
堆的应用
排序算法总结
原地排序:就在原来的数组上操作
稳定性:如下图
插入排序和归并排序在元素相等的情况下不会交换 所以稳定
快排在选择标定点的时候是随机选择的就会存在不稳定的情况,且对于二路归并相等的元素是会对调的,所以也不稳定
堆排序不稳定的情况:取出堆顶元素后会将堆顶元素与最后一个元素交换,这个过程可能导师相同元素不交换,导致不稳定
搜索算法 二分查找法
Binary Search 对于有序数列,才能使用二分查找法(排序的作用)
时间复杂度O(logn)
非递归算法在性能上有微弱优势
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #include <iostream> using namespace std ;template <typename T>int binarySearch (T arr[], int n, T target ) { int l = 0 ; int r = n - 1 ; while (l < r) { int mid = l + (r - l) / 2 ; if (target == arr[mid]) { return mid; } else if (target < arr[mid]) { r = mid - 1 ; } else { l = mid + 1 ; } } return -1 ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include <iostream> using namespace std ;template <typename T>int __binarySearch(T arr[], int l, int r, T target) { if (l > r) return -1 ; int mid = l + (r - l) / 2 ; if (arr[mid] == target) return mid; else if (arr[mid] < target) return __binarySearch(arr, mid + 1 , r, target); else return __binarySearch(arr, l, mid - 1 , target); } template <typename T>int binarySearch2 (T arr[], int n, T target) { return __binarySearch(arr, 0 , n - 1 , target); }
二分搜索树
二分搜索数是一个二叉树,但不一定是完全二叉树
定义:每个节点的键值大于左孩子;每个节点的键值小于右孩子;以左右孩子为根的子树仍为二分搜索树
二分搜索树中插入、搜索、删除时间复杂度都是O(logn)
二分搜索树的插入和搜索
二分搜索树的遍历
深度优先
前序遍历∶先访问当前节点,再依次递归访问左右子树
中序遍历:先递归访问左子树,再访问自身,再递归访问右子树
后续遍历︰先递归访问左右子树,再访问自身节点
后续遍历应用:二叉搜索树的空间释放,对new出来的节点进行delete
广度优先
二分搜索树中删除最大值和最小值
删除最大值思路:一直找右节点,直到没有右节点则这个点是最大值,删除后用这个点的左节点来顶替这个点
删除最小值思路:一直找左节点,直到没有左节点则这个点是最小值,删除后用这个点的右节点来顶替这个点
二分搜索树中删除一个节点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 #include <iostream> #include <queue> #include <cassert> using namespace std ;template <typename Key, typename Value>class BST {private : struct Node { Key key; Value value; Node* left; Node* right; Node(Key key, Value value) { this ->key = key; this ->value = value; this ->left = this ->right = NULL ; } Node(Node* node) { this ->key = node->key; this ->value = node->value; this ->left = node->left; this ->right = node->right; } }; Node* root; int count; Node* insert (Node* node, Key key, Value value) { if (node == NULL ) { count++; return new Node(key, value); } if (node->key == key) node->value = value; else if (node->key < key) node->right = insert(node->right, key, value); else node->left = insert(node->left, key, value); return node; } bool __contain(Node* node, Key key) { if (node == NULL ) return false ; if (node->key == key) return true ; else if (key < node->key) return __contain(node->left, key); else return __contain(node->right, key); } Value* __search(Node* node, Key key) { if (node == NULL ) return NULL ; if (node->key == key) return &(node->value); else if (key < node->key) return __search(node->left, key); else return __search(node->right, key); } void __preOrder(Node* node) { if (node != NULL ) { cout << node->key << endl ; __preOrder(node->left); __preOrder(node -> right); } } void __inOrder(Node* node) { if (node != NULL ) { __inOrder(node->left); cout << node->key << endl ; __inOrder(node->right); } } void __postOrder(Node* node) { if (node != NULL ) { __postOrder(node->left); __postOrder(node->right); cout << node->key << endl ; } } void destroy (Node* node) { if (node != NULL ) { destroy(node->left); destroy(node->right); delete node; count--; } } Node* mininum (Node* node) { if (node->left == NULL ) return node; return mininum(node->left); } Node* maxinum (Node* node) { if (node->right == NULL ) return node; return maxinum(node->right); } Node* removeMin (Node* node) { if (node->left == NULL ) { Node* rightNode = node->right; delete node; count--; return rightNode; } node->left = removeMin(node->left); return node; } Node* removeMax (Node* node) { if (node->right == NULL ) { Node* leftNode = node->left; delete node; count--; return leftNode; } node->right = removeMax(node->right); return node; } Node* __remove(Node* root, Key key){ if (root->key == key){ if (root->left ==NULL && root->right ==NULL ){ delete root; --count; return NULL ; } if (root->left ==NULL ){ Node* node = root->right; delete root; --count; return node; } if (root->right == NULL ){ Node* node = root->left; delete root; --count; return node; } Node* rightMin = __mininum(root->right); root->key = rightMin->key; root->value = rightMin->value; root->right = __removeMin(root->right); }else if (key < root->key){ root->left = __remove(root->left, key); }else { root->right = __remove(root->right, key); } return root; } Node* remove (Node* node, Key key) { if (node == NULL ) { return NULL ; } if (key < node->key) { node->left = remove (node->left, key); return node; }else if (key > node->key){ node->right = remove (node->right, key); return node; }else { if (node->left == NULL ) { Node* rightNode = node->right; delete node; count--; return rightNode; } if (node->right == NULL ) { Node* leftNode = node->left; delete node; count--; return leftNode; } Node* tempNode = new Node(mininum(node->right)); count++; tempNode->left = node->left; tempNode->right = removeMin(node->right); delete node; count--; return tempNode; } } public : BST() { root = NULL ; count = 0 ; } ~BST() { destroy(root); } int size () { return count; } bool isEmpty () { return count == 0 ; } void insert (Key key , Value value) { root = insert(root,key, value); } bool contain (Key key) { return __contain(root,key); } Value* search (Key key) { return __search(root, key); } void preOrder () { __preOrder(root); } void inOrder () { __inOrder(root); } void postOrder () { __postOrder(root); } void levelOrder () { queue <Node*> q; q.push(root); while (!q.empty()) { Node* node = q.front(); q.pop(); cout << node->key << endl ; if (node->left) q.push(node->left); if (node->right) q.push(node->right); } } Key mininum () { assert(count != 0 ); Node* minNode = mininum(root); return minNode->key; } Key maxinum () { assert(count != 0 ); Node* maxNode = maxinum(root); return maxNode->key; } void removeMin () { if (root) root = removeMin(root); } void removeMax () { if (root) root = removeMax(root); } void remove (Key key) { root = remove (root, key); } };
二分搜索树的顺序性
二分搜索树的局限性
局限性:
当插入的元素是有序的时,插入的结果就不是一个二叉树,而是只有左孩子(降序)或只有右孩子(升序)的情况,树的深度就为n了,时间复杂度就退化为O(n)
以上情况虽然和链表的时间复杂度都是O(n),但实际情况比链表还要差,因为链表只维护一个指针,而二分搜索树需要维护左节点和右节点两个指针,而且还要一直判断左右节点是否为空
解决方案:
平衡二叉树:红黑树、2-3 tree、AVL tree、Splay tree
并查集
并查集Union Find 主要功能:
主要方法:
union( p , q ) //将p,q合并到一个组中
find( p ) //找到p所在的组
isConnected( p , q ) //判断p、 q是否相连
Quick Find
用一个数组存储数据, id的值存储的是组号,组号相同则是相连的
查的时间复杂度为O(1)
并的时间复杂度为O(n)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 #include <iostream> #include <cassert> using namespace std ;namespace UF1 { class UnionFind { private : int *id; int count; public : UnionFind(int n) { count = n; id = new int [n]; for (int i = 0 ; i < n; i++) { id[i] = i; } } ~UnionFind() { delete [] id; } int find (int p) { assert(p >= 0 && p < count); return id[p]; } bool isConnected (int p, int q) { return find (p) == find (q); } void unionElements (int p, int q) { int unionP = find (p); int unionQ = find (q); if (unionP == unionQ) return ; for (int i = 0 ; i < count; i++) { if (id[i] == unionP) { id[i] = unionQ; } } } }; }
Quick Union
用节点存储数据,节点a与节点b相连则节点a的指针指向节点b,返回过来也可以, 即每个节点有一个指针,和谁相连指针就指向谁
思想用指针但实际用数组 ,因为每个节点只有指针这一个属性,则可以用数组的值存储,parent是几就表示和谁相连
find(): 找到根元素
uion(): 查找到各自的根,然后合并到根元素
isConnected(): 判断根元素相同则相连
查时间复杂度O(h), h为树的高度
并时间复杂度O(h), h为树的高度,用到了查的过程,除开查其实时间复杂度为O(1)
合并操作
先找到要合并的两个点的根节点
再将其中一个根节点指向另一个根节点
为什么不直接让合并的两个点中的一个指向另一个而是让它们的根节点中的一个指向另一个
因为构造的结果是树,查找的时候树的深度越浅,查找越快,所以尽量不要构造成很长的链,所以让它们的根节点中的一个指向另一个,这样树的深度就会尽量的浅
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 #include <iostream> #include <cassert> using namespace std ;namespace UF2 { class UnionFind { private : int * parent; int count; public : UnionFind(int n) { count = n; parent = new int [n]; for (int i = 0 ; i < n; i++) { parent[i] = i; } } ~UnionFind() { delete [] parent; } int findRoot (int p) { assert(p >= 0 && p < count); while (p != parent[p]) p = parent[p]; return p; } bool isConnected (int p, int q) { return findRoot(p) == findRoot(q); } void unionElements (int p, int q) { int rootP = findRoot(p); int rootQ = findRoot(q); if (rootP == rootQ) return ; parent[rootP] = rootQ; } }; }
Quick Union的优化
Quick Union存在的问题
合并的时候是任意选择一个根节点指向另一个根节点
如果是层数高的根节点指向了层数低的根节点,则整体层数增加了
如果是层数低的根节点指向了层数高的根节点,则整体层数没有增加,所以每次合并应该选择这种方式,而不是任意的
例如上面图中的9和4合并,如果是8指向了9则层数变为4, 如果是9指向了8则层数还是3
size优化和rank优化都是使得树的高度降低,即H尽量小,时间复杂仍然都是O(H) H为数的高度
基于size的优化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 #include <iostream> #include <cassert> using namespace std ;namespace UF3 { class UnionFind { private : int * parent; int * sz; int count; public : UnionFind(int n) { count = n; parent = new int [n]; sz = new int [n]; for (int i = 0 ; i < n; i++) { parent[i] = i; sz[i] = 1 ; } } ~UnionFind() { delete [] parent; delete [] sz; } int findRoot (int p) { assert(p >= 0 && p < count); while (p != parent[p]) p = parent[p]; return p; } bool isConnected (int p, int q) { return findRoot(p) == findRoot(q); } void unionElements (int p, int q) { int rootP = findRoot(p); int rootQ = findRoot(q); if (rootP == rootQ) return ; if (sz[rootP] >= sz[rootQ]) { parent[rootQ] = rootP; sz[rootP] += sz[rootQ]; } else { parent[rootP] = rootQ; sz[rootQ] += sz[rootP]; } } }; }
基于rank的优化
基于size的优化并没有真的比较两个根节点所在的树的深度,而是比较的根节点所在的树的节点个数,这导致结果仍然不理想
如下图,按size方法,size[7] = 6 > size[8] = 3 ,所以是8指向7,结果仍然使得层数增加了
基于rank则是完全按层数来选择
如下图, 按rank方法,rank[7] = 1 < rank[8] =3, 所以应该是层数少的7指向层数高的8
rank优化的时间复杂度和size优化的时间复杂度差不多,只是可以处理一些特殊情况(避免树的层数太深),应用更广
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 #include <iostream> #include <cassert> using namespace std ;namespace UF4 { class UnionFind { private : int * parent; int * rank; int count; public : UnionFind(int n) { count = n; parent = new int [n]; rank = new int [n]; for (int i = 0 ; i < n; i++) { parent[i] = i; rank[i] = 1 ; } } ~UnionFind() { delete [] parent; delete [] rank; } int findRoot (int p) { assert(p >= 0 && p < count); while (p != parent[p]) p = parent[p]; return p; } bool isConnected (int p, int q) { return findRoot(p) == findRoot(q); } void unionElements (int p, int q) { int rootP = findRoot(p); int rootQ = findRoot(q); if (rootP == rootQ) return ; if (rank[rootP] > rank[rootQ]) { parent[rootQ] = rootP; } else if (rank[rootP] < rank[rootQ]) { parent[rootP] = rootQ; } else { parent[rootQ] = rootP; rank[rootP] += 1 ; } } }; }
路径压缩
在find的过程中将树的高度压缩即路径压缩
下面两个路径压缩的结果都使得时间复杂度近乎O(1)
理论上递归的方式,树的层数更少,速度应该更快,但实际测试非递归的方式用时更短,应该是递归的过程使得时间更长了
路径压缩过程中需要更新rank,递归反方式可以更新,但是非递归则不太方便(不会)
非递归的方式
优化find函数,在查找一个元素的根元素的过程中同时降低树的层数
4不是根节点则让4的父节点指向父节点的父节点即4->2, 此时跳到2,2不是根节点则2的父节点指向父节点的父节点即2->0,结果如下图,此时跳到0,0为根节点,找到返回
此过程优化两个方面
查找过程是跳着找的,中间没有访问3和1
查找过程顺便压缩了树的高度,使得下一次查找路径更短
递归的方式
上面的压缩方式并不是最优结果
最优结果应该如下图所示
从4开始递归,4->find(4), 3->find(3),2->find(2),1->find(1),递归到底返回则一条链上的点都指向了根节点
思路是一次递归到底,拿到根节点,再一层层返回赋值给链上的点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 #include <iostream> #include <cassert> using namespace std ;namespace UF5 { class UnionFind { private : int * parent; int * rank; int count; public : UnionFind(int n) { count = n; parent = new int [n]; rank = new int [n]; for (int i = 0 ; i < n; i++) { parent[i] = i; rank[i] = 1 ; } } ~UnionFind() { delete [] parent; delete [] rank; } int findRoot (int p) { assert(p >= 0 && p < count); while (p != parent[p]) { parent[p] = parent[parent[p]]; p = parent[p]; } return p; } int findRoot (int p) { assert(p >= 0 && p < count); if (p != parent[p]) { parent[p] = findRoot(parent[p]); } return parent[p]; } int findRoot (int p) { assert(p >= 0 && p < count); int parentp; if (p != parent[p]) { parentp = findRoot(parent[p]); }else { parentp = p; } parent[p] = parentp return parentp; } bool isConnected (int p, int q) { return findRoot(p) == findRoot(q); } void unionElements (int p, int q) { int rootP = findRoot(p); int rootQ = findRoot(q); if (rootP == rootQ) return ; if (rank[rootP] > rank[rootQ]) { parent[rootQ] = rootP; } else if (rank[rootP] < rank[rootQ]) { parent[rootP] = rootQ; } else { parent[rootQ] = rootP; rank[rootP] += 1 ; } } }; }
图论 图论基础 图的分类
图的连通性
简单图
图的表示
邻接矩阵 适合表示稠密的图
邻接表 适合表示稀疏的图
邻边迭代器
遍历一个点的所有邻边
因为遍历一个点的所有邻边需要拿到存储图的二维数组g,但是g是对象的私有属性,用户不能直接使用
所以考虑用迭代器,通过迭代器在迭代器内部可以访问g,将访问结果返回,就可以遍历所有邻边了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 #include <iostream> #include <vector> #include <cassert> using namespace std ;class SparseGraph {private : int vec; int edge; bool directed; vector <vector <int >> g; public : SparseGraph(int n, bool directed) { assert(n >= 0 ); this ->vec = n; this ->edge = 0 ; this ->directed = directed; g = vector <vector <int >>(n, vector <int >()); } ~SparseGraph() {} int getE () { return edge; } int getV () { return vec; } void addEdge (int v, int w) { assert(v >= 0 && v < vec); assert(w >= 0 && w < vec); g[v].push_back(w); if (v!=w && !directed) g[w].push_back(v); edge++; } bool hasEdge (int v, int w) { assert(v >= 0 && v < vec); assert(w >= 0 && w < vec); for (int i = 0 ; i < g[v].size (); i++) { if (g[v][i] == w) return true ; } return false ; } void show () { cout << "SparseGraph: " << endl ; for (int i = 0 ; i <vec; i++) { cout << i << ": " ; for (int j = 0 ; j < g[i].size (); j++) { cout << g[i][j] << "\t" ; } cout << endl ; } cout << endl ; } class adjIterator { private : SparseGraph& G; int v; int index; public : adjIterator(SparseGraph& G, int v):G(G) { this ->v = v; this ->index = 0 ; } ~adjIterator() {} int begin () { index = 0 ; if (G.g[v].size ()) { return G.g[v][index]; } return -1 ; } int next () { index++; if (index < G.g[v].size ()) return G.g[v][index]; return -1 ; } bool end () { return index >= G.g[v].size (); } }; };
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 #include <iostream> #include <vector> #include <cassert> using namespace std ;class DenseGraph {private : int vec; int edge; int directed; vector <vector <bool >> g; public : DenseGraph(int n, bool directed) { assert(n >= 0 ); this ->vec = n; this ->edge = 0 ; this ->directed = directed; g = vector <vector <bool >>(n, vector <bool >(n,false )); } ~DenseGraph() { } int getE () { return edge; } int getV () { return vec; } void addEdge (int v, int w) { assert(v >= 0 && v < vec); assert(w >= 0 && w < vec); if (hasEdge(v,w)) return ; g[v][w] = true ; if (!directed) g[w][v] = true ; edge++; } bool hasEdge (int v, int w) { assert(v >= 0 && v < vec); assert(w >= 0 && w < vec); return g[v][w]; } void show () { cout << "DenseGraph: " << endl ; for (int i = 0 ; i < vec; i++) { cout << i << ": " ; for (int j = 0 ; j < vec; j++) { cout << g[i][j] <<"\t" ; } cout << endl ; } cout << endl ; } class adjIterator { private : DenseGraph& G; int v; int index; bool isEnd; public : adjIterator(DenseGraph& G, int v) :G(G) { this ->v = v; this ->index = -1 ; } ~adjIterator() {} int begin () { index = -1 ; return next(); } int next () { for (index += 1 ; index < G.getV(); index++){ if (G.g[v][index]) { return index; } } return -1 ; } bool end () { return index >= G.getV(); } }; };
读取图
c++中文件读写部分 fstream相关
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 #include <iostream> #include <string> #include <fstream> #include <sstream> #include <cassert> using namespace std ;template <typename Graph>class ReadGraph {public : ReadGraph(Graph& g, string filename) { ifstream file (filename) ; string line ; int V, E; assert(file.is_open()); assert(getline(file, line )); stringstream ss (line ) ; ss >> V >> E; assert(V == g.getV()); for (int i = 0 ; i < V; i++) { assert(getline(file, line )); stringstream ss (line ) ; int a, b; ss >> a >> b; assert(a >= 0 && a < V); assert(b >= 0 && b < V); g.addEdge(a, b); } } };
1 2 3 string filename = "testG1.txt" ;DenseGraph dg = DenseGraph(13 , false ); ReadGraph<DenseGraph> rg1 (dg, filename) ;
深度优先遍历
此部分实现了深度优先遍历、计算图的连通分量个数、用并查集记录同一个连通分量
深度优先遍历时间复杂度
邻接表:O(v+E) 访问了每个点和每个边
邻接矩阵:O(v^2) 整个矩阵都访问了一遍
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 #include <iostream> using namespace std ;template <typename Graph>class Component {private : Graph& G; bool * visited; int ccount; int * id; void dfs (int v) { visited[v] = true ; id[v] = ccount; typename Graph::adjIterator adj (G, v) ; for (int w = adj.begin (); !adj.end (); w = adj.next()) { if (!visited[w]) dfs( w); } } public : Component(Graph& G) :G(G) { visited = new bool [G.getV()]; ccount = 0 ; id = new int [G.getV()]; for (int i = 0 ; i < G.getV(); i++) { visited[i] = false ; id[i] = -1 ; } for (int i = 0 ; i < G.getV(); i++) { if (!visited[i]) { dfs(i); ccount++; } } } ~Component() { delete [] visited; delete [] id; } int count () { return ccount; } bool isConnected (int v, int w) { assert(v >= 0 && v < G.getV()); assert(w >= 0 && w < G.getV()); return id[v] == id[w]; } };
寻路
通过深度优先的方式找到两点之间的一条路径(不管是不是最近的一条)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 #include <iostream> #include <stack> using namespace std ;template <typename Graph>class Path {private : Graph& G; bool * visited; int s; int * from; void dfs (int v) { visited[v] = true ; typename Graph::adjIterator adj (G, v) ; for (int w = adj.begin (); !adj.end (); w = adj.next()) { if (!visited[w]) { from[w] = v; dfs(w); } } } public : Path(Graph& G , int s) :G(G) { assert(s >= 0 && s < G.getV()); visited = new bool [G.getV()]; from = new int [G.getV()]; this ->s = s; for (int i = 0 ; i < G.getV(); i++) { visited[i] = false ; from[i] = - 1 ; } dfs(s); } ~Path() { delete [] visited; delete [] from; } bool hasPath (int w) { assert(w >= 0 && w < G.getV()); return visited[w]; } void path (int w, vector <int > &vec) { assert(hasPath(w)); stack <int > s; int p = w; while (p != -1 ) { s.push(p); p = from[p]; } vec.clear (); while (!s.empty()) { vec.push_back(s.top()); s.pop(); } } void showPath (int w) { assert(hasPath(w)); vector <int > vec; path(w, vec); for (int i = 0 ; i < vec.size (); i++) { cout << vec[i] ; if (i == vec.size () - 1 ) cout << endl ; else cout << "--->" ; } } };
广度优先遍历
广度优先遍历时间复杂度
邻接表:O(v+E) 访问了每个点和每个边
邻接矩阵:O(v^2) 整个矩阵都访问了一遍
以下代码实现了广度优先方式的寻路,可对比上面的深度优先方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 #include <iostream> #include <stack> #include <queue> using namespace std ;template <typename Graph>class ShortestPath {private : Graph& G; bool * visited; int s; int * from; int * ord; public : ShortestPath(Graph& G, int s) :G(G) { assert(s >= 0 && s < G.getV()); visited = new bool [G.getV()]; from = new int [G.getV()]; ord = new int [G.getV()]; this ->s = s; for (int i = 0 ; i < G.getV(); i++) { visited[i] = false ; from[i] = -1 ; ord[i] = -1 ; } queue <int > q; q.push(s); visited[s] = true ; ord[s] = 0 ; while (!q.empty()) { int w = q.front(); q.pop(); typename Graph::adjIterator adj (G, w) ; for (int v = adj.begin (); !adj.end (); v = adj.next()) { if (!visited[v]) { q.push(v); visited[v] = true ; from[v] = w; ord[v] = ord[w] + 1 ; } } } } ~ShortestPath() { delete [] visited; delete [] from; delete [] ord; } bool hasPath (int w) { assert(w >= 0 && w < G.getV()); return visited[w]; } void path (int w, vector <int >& vec) { assert(hasPath(w)); stack <int > s; int p = w; while (p != -1 ) { s.push(p); p = from[p]; } vec.clear (); while (!s.empty()) { vec.push_back(s.top()); s.pop(); } } void showPath (int w) { assert(hasPath(w)); vector <int > vec; path(w, vec); for (int i = 0 ; i < vec.size (); i++) { cout << vec[i]; if (i == vec.size () - 1 ) cout << endl ; else cout << "--->" ; } } int length (int w) { assert(w >= 0 && w < G.getV()); return ord[w]; } };
最小生成树
生成树:图中有n个节点,通过n-1条边将n个节点连接起来所构成的树就叫生成树
最小生成树:生成树中n-1条边的权值相加最小的那个生成树
最小生成树针对:带权无向图、连通图
有权图
邻接矩阵实现带权图: 矩阵的存储值直接存储权值,为了和下面的邻接表统一,不直接存储权值,而是也存储边的指针,若两点间没有边则存储NULL
邻接表实现带权图: 边作为一个单独的类,邻接表存储边的指针
边的定义 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 #include <iostream> using namespace std ;template <typename Weight>class Edge {private : int a; int b; Weight w; public : Edge(int a, int b, Weight w) { this ->a = a; this ->b = b; this ->w = w; } Edge() {} ~Edge() {} int getv () { return a; } int getw () { return b; } Weight getWt () { return w; } int other (int v) { assert(v == a || v == b); return v == a ? b : a; } friend ostream& operator <<(ostream& os, const Edge& e) { os << e.a << "-" << e.b << ": " << e.w; return os; } bool operator <(Edge<Weight>& e) { return w < e.getWt(); } bool operator <=(Edge<Weight>& e) { return w <= e.getWt(); } bool operator >(Edge<Weight>& e) { return w > e.getWt(); } bool operator >=(Edge<Weight>& e) { return w >= e.getWt(); } bool operator ==(Edge<Weight>& e) { return w == e.getWt(); } };
邻接矩阵构建图 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 #include <iostream> #include <vector> #include <cassert> #include "Edge.h" using namespace std ;template <typename Weight>class DenseGraph {private : int vec; int edge; int directed; vector <vector <Edge<Weight> *>> g; public : DenseGraph(int n, bool directed) { assert(n >= 0 ); this ->vec = n; this ->edge = 0 ; this ->directed = directed; g = vector <vector <Edge<Weight> *>>(n, vector <Edge<Weight> *>(n, NULL )); } ~DenseGraph() { for (int i = 0 ; i < vec; i++) { for (int j = 0 ; j < vec; j++) { if (g[i][j] != NULL ) delete g[i][j]; } } } int getE () { return edge; } int getV () { return vec; } void addEdge (int v, int w ,Weight wt) { assert(v >= 0 && v < vec); assert(w >= 0 && w < vec); if (hasEdge(v, w)) { delete g[v][w]; if (v!= w && !directed) delete g[w][v]; edge--; } g[v][w] = new Edge<Weight>(v,w,wt); if (v!=w && !directed) g[w][v] = new Edge<Weight>(w,v,wt); edge++; } bool hasEdge (int v, int w) { assert(v >= 0 && v < vec); assert(w >= 0 && w < vec); return g[v][w] != NULL ; } void show () { cout << "DenseGraph: " << endl ; for (int i = 0 ; i < vec; i++) { cout << i << ": " ; for (int j = 0 ; j < vec; j++) { if (g[i][j]) cout << g[i][j]->getWt() << "\t" ; else cout << "NULL\t" ; } cout << endl ; } cout << endl ; } class adjIterator { private : DenseGraph& G; int v; int index; bool isEnd; public : adjIterator(DenseGraph& G, int v) :G(G) { this ->v = v; this ->index = -1 ; } ~adjIterator() {} Edge<Weight>* begin () { index = -1 ; return next(); } Edge<Weight>* next () { for (index += 1 ; index < G.getV(); index++) { if (G.g[v][index]) { return G.g[v][index]; } } return NULL ; } bool end () { return index >= G.getV(); } }; };
邻接表构建矩阵 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 #include <iostream> #include <vector> #include <cassert> using namespace std ;template <typename Weight>class SparseGraph {private : int vec; int edge; bool directed; vector <vector <Edge<Weight> *>> g; public : SparseGraph(int n, bool directed) { assert(n >= 0 ); this ->vec = n; this ->edge = 0 ; this ->directed = directed; g = vector <vector <Edge<Weight> *>>(n, vector <Edge<Weight> *>()); } ~SparseGraph() { for (int i = 0 ; i < vec; i++) { for (int j = 0 ; j < g[i].size (); j++) { delete g[i][j]; } } } int getE () { return edge; } int getV () { return vec; } void addEdge (int v, int w, Weight wt) { assert(v >= 0 && v < vec); assert(w >= 0 && w < vec); g[v].push_back(new Edge<Weight>(v,w,wt)); if (v != w && !directed) g[w].push_back(new Edge<Weight>(w,v,wt)); edge++; } bool hasEdge (int v, int w) { assert(v >= 0 && v < vec); assert(w >= 0 && w < vec); for (int i = 0 ; i < g[v].size (); i++) { if (g[v][i]->other(v) == w) return true ; } return false ; } void show () { cout << "SparseGraph: " << endl ; for (int i = 0 ; i < vec; i++) { cout << "vertex " << i << ":\t" ; for (int j = 0 ; j < g[i].size (); j++) { cout << "( to:" << g[i][j]->getw() << ",wt:" << g[i][j]->getWt() << ")\t" ; } cout << endl ; } cout << endl ; } class adjIterator { private : SparseGraph& G; int v; int index; public : adjIterator(SparseGraph& G, int v) :G(G) { this ->v = v; this ->index = 0 ; } ~adjIterator() {} Edge<Weight>* begin () { index = 0 ; if (G.g[v].size ()) { return G.g[v][index]; } return NULL ; } Edge<Weight>* next () { index++; if (index < G.g[v].size ()) return G.g[v][index]; return NULL ; } bool end () { return index >= G.g[v].size (); } }; };
读取并构建带权图 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 #include <iostream> #include <string> #include <fstream> #include <sstream> #include <cassert> using namespace std ;template <typename Graph, typename Weight>class ReadGraph {public : ReadGraph(Graph& g, string filename) { ifstream file (filename) ; string line ; int V, E; assert(file.is_open()); assert(getline(file, line )); stringstream ss (line ) ; ss >> V >> E; assert(V == g.getV()); for (int i = 0 ; i < E; i++) { assert(getline(file, line )); stringstream ss (line ) ; int a, b; Weight w; ss >> a >> b>>w; assert(a >= 0 && a < V); assert(b >= 0 && b < V); g.addEdge(a, b, w); } } };
最小堆 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 #pragma once #include <iostream> #include <cassert> using namespace std ;template <typename Item>class MinHeap {private : Item* data; int count; int capacity; void shiftUp (int k) { while (k > 0 && data[k / 2 ] > data[k]) { swap(data[k / 2 ], data[k]); k /= 2 ; } } void shiftDown (int k) { while (2 * k <= count - 1 ) { int j = 2 * k; if (j + 1 <= count - 1 && data[j + 1 ] < data[j]) j++; if (data[k] <= data[j]) break ; swap(data[k], data[j]); k = j; } } public : MinHeap(int capacity) { data = new Item[capacity]; count = 0 ; this ->capacity = capacity; } MinHeap(Item arr[], int n) { data = new Item[n]; capacity = n; for (int i = 0 ; i < n; i++) data[i] = arr[i]; count = n; for (int i = count - 1 / 2 ; i >= 0 ; i--) shiftDown(i); } ~MinHeap() { delete [] data; } int size () { return count; } bool isEmpty () { return count == 0 ; } void insert (Item item) { assert(count < capacity); data[count] = item; shiftUp(count); count++; } Item extractMin () { assert(count > 0 ); Item ret = data[0 ]; swap(data[0 ], data[count - 1 ]); count--; shiftDown(0 ); return ret; } Item getMin () { assert(count > 0 ); return data[0 ]; } };
最小索引堆 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 #pragma once #include <iostream> #include <cassert> using namespace std ;template <typename Item>class IndexMinHeap {private : Item* data; int * index; int * reverse; int count; int capacity; void shiftUp (int k) { while (k > 0 && data[index[k / 2 ]] > data[index[k]]) { swap(index[k / 2 ], index[k]); reverse[index[k / 2 ]] = k / 2 ; reverse[index[k]] = k; k /= 2 ; } } void shiftDown (int k) { while (2 * k <= count - 1 ) { int j = 2 * k; if (j + 1 <= count - 1 && data[index[j + 1 ]] < data[index[j]]) j++; if (data[index[k]] <= data[index[j]]) break ; swap(index[k], index[j]); reverse[index[k]] = k; reverse[index[j]] = j; k = j; } } bool isInHeap (int i) { assert(i >= 0 && i < capacity); return reverse[i] != -1 ; } public : IndexMinHeap(int capacity) { data = new Item[capacity]; index = new int [capacity]; reverse = new int [capacity]; for (int i = 0 ; i < capacity; i++) { reverse[i] = -1 ; } count = 0 ; this ->capacity = capacity; } IndexMinHeap(Item arr[], int n) { data = new Item[n]; index = new int [n]; capacity = n; for (int i = 0 ; i < n; i++) { data[i] = arr[i]; index[i] = i; } count = n; for (int i = count - 1 / 2 ; i >= 0 ; i--) shiftDown(i); } ~IndexMinHeap() { delete [] data; delete [] index; delete [] reverse; } int size () { return count; } bool isEmpty () { return count == 0 ; } void insert (int i, Item item) { assert(count < capacity); assert(i >= 0 && i < capacity); data[i] = item; index[count] = i; reverse[i] = count; shiftUp(count); count++; } Item extractMin () { assert(count > 0 ); Item ret = data[index[0 ]]; swap(index[0 ], index[count - 1 ]); reverse[index[0 ]] = 0 ; reverse[index[count - 1 ]] = -1 ; count--; shiftDown(0 ); return ret; } int extracIndexMin () { assert(count > 0 ); Item ret = index[0 ]; swap(index[0 ], index[count - 1 ]); reverse[index[0 ]] = 0 ; reverse[index[count - 1 ]] = -1 ; count--; shiftDown(0 ); return ret; } Item getMax () { assert(count > 0 ); return data[index[0 ]]; } int getMaxIndex () { assert(count > 0 ); return index[0 ]; } Item getItem (int i) { assert(isInHeap(i)); return data[i]; } void change (int i, Item newItem) { assert(isInHeap(i)); data[i] = newItem; int j = reverse[i]; shiftUp(j); shiftDown(j); } bool contain (int index) { return reverse[index ] != 0 ; } };
Prim算法 切分定理 把图中的结点分为两部分,成为一个切分(Cut)
如果一个边的两个端点,属于切分(Cut)不同的两边,这个边称为横切边(CrossingEdge)
切分定理: 给定任意 切分,横切边中权值最小的边必然属于最小生成树。
Lazy Prim
时间复杂度O(ElogE) 需要遍历每条边(E),每条边都需要插入堆和取出堆(logE)
用一个最小堆维护权值
从0开始,将与0相连的所有边加入堆中,此时堆顶就是横切边中最短的边,即0-7:0.16
再将与7相连的所有边加入堆中(加入不重复,判断边的另一个点是否已经访问),拿到堆顶元素1-7:0.19
再将与1相连的所有边加入堆中,,,,,
当将与2相连的所有边加入堆中后,会发现1-2、2-7这两条边并不是横切边
这就是Lazy的部分,不是横切边,但仍然放在堆中
当堆顶元素就是这个不是横切边的边时,就判断边的两个端点是否在同一个切分中,如果是则在堆中将其删除,再拿到堆顶元素,做同样的判断,直到堆顶元素是横切边
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 #pragma once #include <iostream> #include <vector> #include <cassert> #include "Edge.h" #include "MinHeap.h" using namespace std ;template <typename Graph , typename Weight>class LazyPrimMST {private : Graph& g; MinHeap<Edge<Weight>> mp; bool * marked; vector <Edge<Weight>> mst; Weight minWt; void visit (int v) { assert(!marked[v]); marked[v] = true ; typename Graph::adjIterator adj (g, v) ; for (Edge<Weight>* i = adj.begin (); !adj.end (); i = adj.next()) if (!marked[i->other(v)]) mp.insert(*i); } public : LazyPrimMST(Graph& g) :g(g), mp(MinHeap<Edge<Weight>>(g.getE())) { marked = new bool [g.getV()]; for (int i = 0 ; i < g.getV(); i++) { marked[i] = false ; } mst.clear (); visit(0 ); while (!mp.isEmpty()) { Edge<Weight> e = mp.extractMin(); if (marked[e.getv()] == marked[e.getw()]) continue ; mst.push_back(e); if (!marked[e.getv()]) visit(e.getv()); else visit(e.getw()); } minWt = mst[0 ].getWt(); for (int i = 1 ; i < mst.size (); i++) { minWt += mst[i].getWt(); } } ~LazyPrimMST() { delete [] marked; } vector <Edge<Weight>> getMST () { return mst; } Weight getMinWt () { return minWt; } };
Prim
时间复杂度O(ElogV) 考察了每条边(E), 最小索引堆相较LazyPrim变小了,最多存储顶点个数(logV)
利用最小索引堆来维护权值
最小索引堆中data[i]存储的是到达 i 这个点最短的边(已经访问的边中到达该点最短的边)的权值,
从0开始,将0的所有边加入最小索引堆,此时堆顶就是横切边中最短的边,即0-7:0.16
再考察与7相连的所有边,判断边的另一端的点是否已访问,若已访问则不加入,若未访问则需要判断是否已经有边到达该点,若没有则直接插入,若已经有则判断当前的边的权值是否小于已经有的到达该点的边的权值,若小于则覆盖,否则不修改
对于0-7,因为0已经访问不加入
对于2-7,已经有边0-2:0.26到达2, 2-7:0.34的权值大于0-2:0.26 所以data[2]的值保持不变
对于4-7,已经有边0-4:0.38到达4, 4-7:0.37的权值小于0-4:0.38 所以data[4]的修改为0.37
按以上规则考察与7相连的所有边后,取出堆顶元素:7-1:0.19
再考察与1相连的所有边,,,,,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 #pragma once #include <iostream> #include <vector> #include <cassert> #include "Edge.h" #include "IndexMinHeap.h" using namespace std ;template <typename Graph, typename Weight>class PrimMST {private : Graph& g; IndexMinHeap<Weight> imp; vector <Edge<Weight>*> edgeTo; bool * marked; vector <Edge<Weight>> mst; Weight minWt; void visit (int v) { assert(!marked[v]); marked[v] = true ; typename Graph::adjIterator adj (g, v) ; for (Edge<Weight>* i = adj.begin (); !adj.end (); i = adj.next()) { int w = i->other(v); if (!marked[w]) { if (!edgeTo[w]) { imp.insert(w, i->getWt()); edgeTo[w] = i; } else if (i->getWt() < edgeTo[w]->getWt()) { imp.change(w, i->getWt()); edgeTo[w] = i; } } } } public : PrimMST(Graph& g) :g(g), imp(IndexMinHeap<Weight>(g.getV())) { marked = new bool [g.getV()]; for (int i = 0 ; i < g.getV(); i++) { marked[i] = false ; edgeTo.push_back(NULL ); } mst.clear (); visit(0 ); while (!imp.isEmpty()) { int v = imp.extracIndexMin(); assert(edgeTo[v]); mst.push_back(*edgeTo[v]); visit(v); } minWt = mst[0 ].getWt(); for (int i = 1 ; i < mst.size (); i++) { minWt += mst[i].getWt(); } } ~PrimMST() { delete [] marked; } vector <Edge<Weight>> getMST () { return mst; } Weight getMinWt () { return minWt; } };
Kruskal算法
效率: LazyPrim < kruskal算法 < Prim算法
时间复杂度O(ElogE)
先将所有的边排序 时间复杂度O(ElogE) ,用最小堆排序,便于拿到每个点
插入和获取堆顶元素时间复杂度都是O(logE), 遍历每条边时间复杂度O(E)
使用基于find优化的路径压缩的并查集,非递归版
从最小边开始考察,若这条边加入不使得形成环,则这条边一定是最小生成树的一条边
用并查集来判断边的加入是否形成环,若边的两点是可达的,则说明这条边的加入会形成环
并查集的时间复杂度为O(logV), 每条边都要考察,所以总时间复杂度O(ElogV)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 #pragma once #include <iostream> #include <vector> #include "UF.h" #include "Edge.h" #include "MinHeap.h" using namespace std ;template <typename Graph, typename Weight>class KruskalMST {private : vector <Edge<Weight>> mst; Weight minWt; public : KruskalMST(Graph& g) { MinHeap<Edge<Weight>> mp (g.getE()) ; for (int i = 0 ; i < g.getV(); i++) { typename Graph::adjIterator adj (g, i) ; for (Edge<Weight>* v = adj.begin (); !adj.end (); v = adj.next()) { if (v->getv() < v->getw()) mp.insert(*v); } } UnionFind uf (g.getV()) ; while (!mp.isEmpty() && mst.size () < g.getV()-1 ) { Edge<Weight> e = mp.extractMin(); if (!uf.isConnected(e.getv(), e.getw())) { mst.push_back(e); uf.unionElements(e.getv(), e.getw()); } } minWt = 0 ; for (int i = 0 ; i < mst.size (); i++) { minWt += mst[i].getWt(); } } ~KruskalMST() {} vector <Edge<Weight>> getMST () { return mst; } Weight getMinWt () { return minWt; } };
最短路径
单源最短路径: 某一个点到图中任意一点的最短路径
本部分辅助类沿用最小生成树部分的辅助类
dijkstra算法
dijkstra算法: 解决单源最短路径
前提: 图中不能有负权边
时间复杂度O(E log(V) )
使用最小索引堆选取下一个访问节点,最小索引堆存储已经访问的到达每个点的最短路径,堆顶元素的索引即是下一个要访问的点
1 2 3 4 Weight* distTo; bool * marked; vector <Edge<Weight>*> from; IndexMinHeap<Weight> imh (g.getV()) ;
从0开始,将0标记为已访问,将所有与0相连的边的权值加入堆中,并将1、2、3的前驱标记为0,并记录到达1、2、3的最短路径值分别为5、2、6,取出堆顶元素的索引2,即下一个访问的节点
从2开始
将2标记为已访问
考察(2,1)边,因为(0,2)+(2,1)边的权值小于distTo[1], 所以有如下操作,若没有小于则不操作
因为distTo[1] = 5,值不为零,说明已经插入过堆中,所以使用最小堆的change方法,修改到达1的最短路径为3
distTo[1]更新为3
1的前驱节点更新为2
考察(2,4)边,
因为distTo[4] = 0,值为零,说明之前没有插入过堆中,直接插入
distTo[4]更新为7
4的前驱节点更新为2
,,,,,
关键步骤是松弛操作
只有当前点未被访问才进行松弛操作
当前点未被访问,且之前没有赋值,则直接插入堆中
当前点未被访问,且之前有赋值,则比较当前值是否比以前小,如果小则更新堆中的数据
为什么松弛操作可以找到更短的路径
为什么每次选堆顶元素,也就是路径最短的那个点作为下一个扩展点就是可以最终获得最短路劲
因为:如上图0到1的权值为5,而可以到1的不止0,还有2,也就是说存在一种可能:从0出发到2再到1可能权值更小,同理如果还有多个点也可以到1,则可能存在从0到这些点再到1路径更短,而应该选择哪个点到1最有可能路径最短呢?那一定是与1相连的点中路径最短的那个点也就是堆顶的那个点 ,由此反推,从0出发时,每次选路径最短的那个点扩展
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 #pragma once #include <iostream> #include <cassert> #include <vector> #include <stack> #include "IndexMinHeap.h" #include "Edge.h" using namespace std ;template <typename Graph, typename Weight>class Dijkstra {private : Graph& g; int s; Weight* distTo; bool * marked; vector <Edge<Weight>* > from; public : Dijkstra(Graph& g, int s) :g(g) { assert(s >= 0 && s < g.getV()); this ->s = s; distTo = new Weight[g.getV()]; marked = new bool [g.getV()]; for (int i = 0 ; i < g.getV(); i++) { distTo[i] = Weight(); marked[i] = false ; from.push_back(NULL ); } IndexMinHeap<Weight> imh (g.getV()) ; imh.insert(s, distTo[s]); while (!imh.isEmpty()) { int cur_id = imh.extracIndexMin(); typename Graph::adjIterator adj (g, cur_id) ; marked[cur_id] = true ; for (Edge<Weight>* e = adj.begin (); !adj.end (); e = adj.next()) { int v = e->other(cur_id); Weight wt = e->getWt(); if (!marked[v]) { if (from[v] == NULL ) { distTo[v] = distTo[cur_id] + wt; from[v] = e; imh.insert(v, distTo[v]); } if (distTo[cur_id] + wt < distTo[v]) { distTo[v] = distTo[cur_id] + wt; from[v] = e; imh.change(v, distTo[v]); } } } } } ~Dijkstra() { delete [] distTo; delete [] marked; } Weight shortestPathTo (int w) { assert(w >= 0 && w < g.getV()); assert(hasPathTo(w)); return distTo[w]; } bool hasPathTo (int w) { assert(w >= 0 && w < g.getV()); return marked[w]; } void shortestPath (int w, vector <Edge<Weight>>& vec) { assert(w >= 0 && w < g.getV()); assert(hasPathTo(w)); stack <Edge<Weight>*> s; while (from[w]) { s.push(from[w]); w = from[w]->other(w); } while (!s.empty()) { vec.push_back(*s.top()); s.pop(); } } void showPath (int w) { assert(w >= 0 && w < g.getV()); assert(hasPathTo(w)); vector <Edge<Weight>> vec; shortestPath(w, vec); for (int i = 0 ; i < vec.size (); i++) { cout << vec[i].getv() << "-->" ; if (i == vec.size () - 1 ) cout << vec[i].getw() << endl ; } } };
Bellman-Ford算法
Bellman-Ford算法:解决有负权边的单源最短路径
前提:图中不能有负权环
时间复杂度:O(EV)
一般用于处理有向图,因为无向图中只要存在一条负权边就一定形成了负权环
负权环
0–1–2 三点之间就构成了负权环(这三个点构成环,且环的所有边的权值之和为负值),当有负权环时,则不存在最短路径,因为只要一直走这个环,路径会越来越小,直至负无穷
1–2 两点之间也够成了负权环
0–2 两点之间也够成了负权环
判断图中是否有负权环
假设一个图没有负权环
从一点到另外一点的最短路径,最多经过所有的V个顶点,有V-1条边
否则(经过了大于v个顶点),存在顶点经过了两次,既存在负权环,与假设矛盾
所以只要经过的点大于v个顶点则判断图中有负权环,不可能存在最短路径,直接结束查找
松弛操作
核心思想:从一个点出发,通过另外一个点到达本来要到达的点的路径可能比直接到达更短
前提思考
对一个点的一次松弛操作,就是找到经过这个点的另外一条路径,多一条边,权值更小。
如果一个图没有负权环,从一点到另外一点的最短路径,最多经过所有的V个顶线,有V-1条边
对所有的点进行V-1次松弛操作
Bellman-Ford算法思想
对所有的点进行V-1次松弛操作,理论上就找到了从源点到其他所有点的最短路径。
如果还可以继续松弛(V次及V次以上),说明原图中有负权环。
算法核心步骤如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 bool detectNegativeCycle () { for (int i = 0 ; i < g.getV(); i++) { typename Graph::adjIterator adj (g, i) ; for (Edge<Weight>* e = adj.begin (); !adj.end (); e = adj.next()) { if (from[e->getw()] == NULL || distTo[e->getv()] + e->getWt() < distTo[e->getw()]) { return true ; } } } return false ; } for (int pass = 1 ; pass < g.getV(); pass++) { for (int i = 0 ; i < g.getV(); i++) { typename Graph::adjIterator adj (g, i) ; for (Edge<Weight>* e = adj.begin (); !adj.end (); e = adj.next()) { if (from[e->getv()] && (from[e->getw()] == NULL || distTo[e->getv()] + e->getWt() < distTo[e->getw()])) { distTo[e->getw()] = distTo[e->getv()] + e->getWt(); from[e->getw()] = e; } } } } hasNegativeCycle = detectNegativeCycle();
可以理解为从源点出发的层序遍历,第一次松弛操作只更新了距离源点为1的点,第二次更新了距离源点为2的点,,,,
下图是特殊情况,因为源点正好是0, 而编程中也是选择的从序号最小的点开始扩展,所以等到扩展序号大的点时,一定保证了他的前继节点已经更新了到源点的最短距离,所以一轮松弛操作就可以得到最终结果
对每个点都需要进行v-1次松弛操作的原因:源点可能不是正好是0,如果是比较大的点,按编程中的的扩展顺序,当扩展0的时候0的前继节点可能还没更新,所以0在第一次松弛操作时就没有得到扩展,同理,只要比源点小的点都存在这个问题
为什么第v次松弛操作时有点没有更新(from[e->getw()] == NULL
)也可以判定有负权环:因为有负权环的存在,会导致每次松弛操作一直在环中进行,不会继续扩展到下一个节点,所以到v次扩展仍然有点没有访问到,反过来就可以判断有负权环
1 2 3 4 5 6 7 8 9 10 11 12 上图的执行结果 bellmanford: 0 -> 0 shortestPathTo:0->0 : 0 0 -> 1 shortestPathTo:0->1 : 5 0 -> 1 -> 2 shortestPathTo:0->2 : 1 0 -> 1 -> 2 -> 4 -> 3 shortestPathTo:0->3 : 3 0 -> 1 -> 2 -> 4 shortestPathTo:0->4 : 6
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 #pragma once #include <iostream> #include <vector> #include <stack> #include "Edge.h" using namespace std ;template <typename Graph, typename Weight>class BellmanFord {private : Graph& g; int s; Weight* distTo; vector <Edge<Weight>*> from; bool hasNegativeCycle; bool detectNegativeCycle () { for (int i = 0 ; i < g.getV(); i++) { typename Graph::adjIterator adj (g, i) ; for (Edge<Weight>* e = adj.begin (); !adj.end (); e = adj.next()) { if (from[e->getw()] == NULL || distTo[e->getv()] + e->getWt() < distTo[e->getw()]) { return true ; } } } return false ; } public : BellmanFord(Graph& g, int s) :g(g) { this ->s = s; distTo = new Weight[g.getV()]; for (int i = 0 ; i < g.getV(); i++) { from.push_back(NULL ); } distTo[s] = Weight(); from[s] = new Edge<Weight>(s, s, Weight()); for (int pass = 1 ; pass < g.getV(); pass++) { for (int i = 0 ; i < g.getV(); i++) { typename Graph::adjIterator adj (g, i) ; for (Edge<Weight>* e = adj.begin (); !adj.end (); e = adj.next()) { if (from[e->getv()] && (from[e->getw()] == NULL || distTo[e->getv()] + e->getWt() < distTo[e->getw()])) { distTo[e->getw()] = distTo[e->getv()] + e->getWt(); from[e->getw()] = e; } } } } hasNegativeCycle = detectNegativeCycle(); } ~BellmanFord() { delete [] distTo; delete from[s]; } bool negativeCycle () { return hasNegativeCycle; } Weight shortestPathTo (int w) { assert(w >= 0 && w < g.getV()); assert(!hasNegativeCycle); assert(hasPathTo(w)); return distTo[w]; } bool hasPathTo (int w) { assert(w >= 0 && w < g.getV()); return from[w] != NULL ; } void shortestPath (int w, vector <Edge<Weight>>& vec) { assert(w >= 0 && w < g.getV()); assert(!hasNegativeCycle); assert(hasPathTo(w)); stack <Edge<Weight>*> s; Edge<Weight>* e = from[w]; while (e->getv() != this ->s) { s.push(e); e = from[e->getv()]; } s.push(e); while (!s.empty()) { e = s.top(); vec.push_back(*e); s.pop(); } } void showPath (int w) { assert(w >= 0 && w < g.getV()); assert(!hasNegativeCycle); assert(hasPathTo(w)); vector <Edge<Weight>> vec; shortestPath(w, vec); for (int i = 0 ; i < vec.size (); i++) { cout << vec[i].getv() << " -> " ; if (i == vec.size () - 1 ) cout << vec[i].getw() << endl ; } } };
拓展 单源最短路径算法:
所有对最短路径算法:
计算任意两个点之间的最短路径
Floyed算法,处理无负权环的图, 时间复杂度O( V^3 ),动态规划思想
最长路径算法
最长路径问题不能有正权环。
无权图的最长路径问题是指数级难度的。
对于有权图,不能使用Dijkstra求最长路径问题。可以使用Bellman-Ford算法(把权值取负就可以直接使用该算法了)。
字典树(Trie树)
https://www.cnblogs.com/inchbyinch/p/12588323.html
208 实现 Trie (前缀树)
二 对一-组数据进行排序 这组数据有什么样的特征?
有没有可能包含有大量重复的元素?
如果有这种可能的话,三路快排是更好地选择。
这组数据有什么样的特征?
是否大部分数据距离它正确的位置很近?是否近乎有序?
如果是这样的话,插入排序是更好地选择。
这组数据有什么样的特征?
是否数据的取值范围非常有限?比如对学生成绩排序。
如果是这样的话,计数排序是更好地选择。
对排序有什么额外的要求?
是否需要稳定排序?
如果是的话,归并排序是更好地选择。
数据的存储状况是怎样的?
是否是使用链表存储的?
如果是的话,归并排序是更好地选择。
数据的存储状况是怎样的?
数据的大小是否可以装载在内存里?
数据量很大,或者内存很小,不足以装载在内存里,需要使用外排序算法。
有没有可能包含有大量重复的元素? 是否大部分数据距离它正确的位置很近?是否近乎有序? 是否数据的取值范围非常有限?比如对学生成绩排序。 是否需要稳定排序? 是否是使用链表存储的? 数据的大小是否可以装载在内存里?
各种排序算法 基础数据结构和算法的实现:如堆、二叉树、图… 基础数据结构的使用:如链表、栈、队列、哈希表、图、Trie、 并查集… 基础算法:深度优先、广度优先、二分查找、递…. 基本算法思想:递归、分治、回溯搜索、贪心、动态规划…
优化算法 遍历常见的算法思路 遍历常见的数据结构 空间和时间的交换(哈 希表) 预处理信息(排序) 在瓶颈处寻找答案: O(nlogn) + O(n^2) ; O(n^3)
动态规划
它将一个问题分解为互相重叠 的子问题,通过反复求解子问题,来解决原来的问题
动态规划 VS 分而治之
子问题是相互重叠的(例:斐波拉切数列) 则是动态规划
子问题是相互独立的(例:翻转二叉树 )则是分而治之
先画出树状图,看是否有重叠子问题,可以用递归的方式先思考,再换成动态规划的方式
可以不用数值记录动态过程,直接用三个值记录并更新即可
专题
侯卫东三道例题:https://blog.csdn.net/weixin_44550963/article/details/107282087
动态规划题目特点
计数
有多少种方式走到右下角
有多少种方法选出k个数使得和是Sum
求最大最小值
从左上角走到右下角路径的最大数字和
最长上升子序列长度
求存在性
取石子游戏,先手是否必胜
能不能选出k个数使得和是Sum
四个组成部分
坐标型动态规划
加分项:打印路径 滚动数组
拆分为上下左右四个方向单独考虑,每个方向采用动态规划计算
up[i][j] = up[i-1][j] + 1 (a[i][j] = E) || up[i-1][j](a[i][j] = 0) || 0 (a[i][j] = W)
同理定义down、right、left
遍历若a[i][j] = 0 则a[i][j] = up[i][j] + down[i][j] + right[i][j] + left[i][j]
并更新最大值,最后返回
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 int f (vector <vector <char >> a) { int m = a.size (); int n = a[0 ].size (); vector <vector <int >> up (m, vector <int >(n, 0 )) ; vector <vector <int >> left (m, vector <int >(n, 0 )) ; vector <vector <int >> right (m, vector <int >(n, 0 )) ; vector <vector <int >> down (m, vector <int >(n, 0 )) ; for (int i = 0 ; i < m; ++i){ for (int j = 0 ; j < n; ++j){ if (a[i][j] == 'w' ){ continue ; } if (a[i][j] == 'E' ){ up[i][j] = 1 ; left[i][j] =1 ; } if (i > 0 ){ up[i][j] = up[i-1 ][j] + 1 ; } if (j > 0 ){ left[i][j] = left[i][j-1 ] + 1 ; } } } for (int i = m-1 ; i >= 0 ; --i){ for (int j = n-1 ; j >= 0 ; --j){ if (a[i][j] == 'w' ){ continue ; } if (a[i][j] == 'E' ){ down[i][j] = 1 ; right[i][j] = 1 ; } if (i < m-1 ){ down[i][j] = down[i+1 ][j] + 1 ; } if (j < n-1 ){ right[i][j] = right[i][j+1 ] + 1 ; } } } int res = 0 ; for (int i = 0 ; i < m; ++i){ for (int j = 0 ; j < n; ++j){ if (a[i][j] == '0' ){ int t = up[i][j] + down[i][j] + right[i][j] + left[i][j]; res = max (res, t); } } } return res; }
位操作型动态规划
1 2 3 4 5 6 7 8 vector <int > countBits (int n) { vector <int > res (n+1 ) ; res[0 ] = 0 ; for (int i = 1 ; i <=n; ++i){ res[i] = res[i>>1 ] + (1 %2 ); } return res; }
序列型动态规划
序列+状态
Lintcode 516 https://www.lintcode.com/problem/516/
f[i][j] = min{f[i-1][k] (k != j)} + cost[i][j]
时间复杂度:O(N*k*K)
优化:
循环j求f[i][j]时,每个j都要计算一次min{f[i-1][k] (k != j)}时
时间复杂度:O(K*K)
,但其实有重复计算
一次性计算出f[i-1][k]
的最小值和次小值,当j不等于最小值的k时,最小值就是最小值,当j等于最小值的k时,最小值就是次小值
假如最小值是f[i-1][a]
, 次小值是f[i-1][b]
f[i][j] = f[i-1][a] + cost[i][j] j!=a
f[i][j] = f[i-1][b] + cost[i][j] j==a
时间复杂度降为:O(N*K)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 int minCost11 (vector <vector <int >>& cost) { int n = cost.size (); int k = cost[0 ].size (); vector <vector <int >> f (n,vector <int >(k)) ; for (int j = 0 ;j<k;++j){ f[0 ][j] = cost[0 ][j]; } for (int i=1 ; i<n; ++i){ int first = INT32_MAX, second = INT32_MAX; int a , b; for (int j = 0 ; j < k; ++j){ if (f[i-1 ][j] < first){ second = first; b = a; first = f[i-1 ][j]; a = j; }else if (f[i-1 ][j] < second){ second = f[i-1 ][j]; b = j; } } for (int j=0 ; j < k; ++j){ if (j != a){ f[i][j] = f[i-1 ][a] + cost[i][j]; }else { f[i][j] = f[i-1 ][b] + cost[i][j]; } } } int res = INT32_MAX; for (int j = 0 ; j<k; ++j){ res = min (res, f[n-1 ][j]); } return res; }
最长序列型动态规
划分型动态规划
lint 513
lint 108
每次i, j 都固定后再对[j,i-1]判断回文则会导致时间复杂度为n^3
先记录s中的所有回文串,时间复杂度n*n
从中心点向两边扩展
奇数情况,n个点可以作为中心点
偶数情况,n-1个点可作为中心点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 F[i] = min (F[j] + 1 ) [j,i-1 ]是回文 0 <=j<i F[0 ] = -1 F[i] = min (F[j] + 1 ) isPalin[j,i-1 ]==true 0 <=j<i F[0 ] = -1 用isPalin记录是回文串的区间 bool isHui(string s){ int i = 0 ; int j = s.length()-1 ; while (i < j) { if (s[i] != s[j]) return false ; ++i; --j; } return true ; } void createPalin (string s, vector <vector <bool >>& isPalin) { int n = s.length(); for (int c = 0 ; c < n; ++c){ int i = c; int j = c; while (i>=0 && j < n && s[i] == s[j]) { isPalin[i][j] = true ; --i; ++j; } } for (int c = 0 ; c < n-1 ; ++c){ int i = c; int j = c+1 ; while (i >=0 && j < n && s[i] == s[j]) { isPalin[i][j] = true ; --i; ++j; } } } int lint108 (string s) { int n = s.length(); vector <int > F (n+1 ) ; F[0 ] = -1 ; for (int i = 1 ; i <=n; ++i){ int res = INT32_MAX; for (int j = 0 ; j < i; ++j){ if (isHui(s.substr(j, i-j))) res = min (res, F[j] + 1 ); } F[i] = res; } return F[n]; int n = s.length(); vector <vector <bool >> isPalin (n, vector <bool >(n, false )) ; createPalin(s, isPalin); vector <int > F (n+1 ) ; F[0 ] = -1 ; for (int i = 1 ; i <=n; ++i){ int res = INT32_MAX; for (int j = 0 ; j < i; ++j){ if (isPalin[j][i-j]) res = min (res, F[j] + 1 ); } F[i] = res; } return F[n]; }
lint 437
k>= n时,直接返回数组最大值即可
k < n时, 一个人至少处理一本书
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 k<n时 F[k][i] = min (max (F[k-1 ][j], sum[j, i-1 ])) k-1 <j<i F[0 ][0 ] = 0 F[0 ][1 ---n] = +无穷 F[1 ---k][0 ] = 0 F[k][i] = min (max (F[k-1 ][j], sum[j, i-1 ])) 0 <=j<=i F[0 ][0 ] = 0 F[0 ][1 ---n] = +无穷 F[1 ---k][0 ] = 0 int lint437(vector <int > A, int k){ int n = A.size (); if (k > n){ k = n; } vector <vector <int >> F (k+1 , vector <int >(n+1 )) ; F[0 ][0 ] = 0 ; for (int i = 1 ; i <= n; ++i){ F[0 ][i] = INT32_MAX; } for (int c = 1 ; c <= k; ++c){ F[c][0 ] = 0 ; int sum = 0 ; for (int i = 1 ; i <= n; ++i){ F[k][i] = INT32_MAX; for (int j = i; j > 0 ; --j){ F[k][i] = min (F[k][i], max (F[k-1 ][j], sum)); if (j > 0 ) sum += A[j-1 ]; } } } return F[k][n]; }
博弈型动态规划
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 F[i] = {true , F[i-1 ] = false and F[i-2 ] = false true , F[i-1 ] = true and F[i-2 ] = false true , F[i-1 ] = false and F[i-2 ] = true false , F[i-1 ] = true and F[i-2 ] = true } class Solution { public : bool firstWillWin(int n) { if (n == 0 ) return false ; if (n == 1 ) return true ; bool one, two; one = two = true ; for (int i = 3 ; i <= n; ++i){ bool temp = !one || !two; one = two; two = temp; } return two; } };
背包问题
注意分类
元素可以重复用:零钱问题、lint564、lint440
元素不可重复用:lint92、lint563、lint125
1 2 3 4 5 6 7 * arr: 物品 * arr[i]: i物品重量 * i: 第i个物品 0<=i<arr.size() * w: 背包可容纳的重量 * 诉求:任选物品(不重复)将背包尽可能装满 * i是考虑的对象,最后一个是使用i, 前i-1个是子问题, *背包重量不是考虑对象* * 相对零钱问题,背包问题中物品是不能重复使用的,且不一定可以正好凑到w重量
1 2 3 4 5 6 7 8 9 10 f[i][w] = f[i-1 ][w] || f[i-1 ][w-arr[i-1 ]] f[0 ][0 ] = true ; f[0 ][1 ->w] = false ; f[w] = f[w] || f[w-arr[i-1 ]] f[0 ] = true ; f[1 --w] = false
lint564
记录组合数
元素可重复
一维数组
return f[w]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 题意: 给定N个正整数: A0,A1,...,AN-1 一个正整数Target 求有多少种组合加起来是Target 每个Ai可以用多次 **1 +2 +1 = 4 , 1 +1 +2 = 4 这是算两次的 就是不考虑顺序** solution(vector <int > a, int target){ int n = a.size (); vector <int > f (target+1 , 0 ) ; f[0 ] = 1 ; for (int j = 1 ; j <= target; ++j){ for (int i = 0 ; i < n; ++i){ if (j >= a[i]){ f[j] += f[j-a[i]]; } } } return f[target]; }
区间型动态规划
特点:
lint 667 最长的回文序列
非连续
记录当前区间(s[i….j])最长回文序列长度 f[i][j]记录s[i...j]区间最长回文长度
难度提升:打印出最长回文序列
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 要求S[i..j]的最长回文子串 如果S[i]=S[j],需要知道S[i+1. .j-1 ]的最长回文子串 子问题为去掉回文头尾的子串 如果S[i]/=S[j]答案是S[ i+1. .j]的最长回文子串或S[i..j-1 ]的最长回文子串子问题 子问题为去掉头的子串或去掉尾的子串 状态︰设f[i]j]为S[i..j]的最长回文子串的长度
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 取a[i]时剩余区间 [i+1 ,j], 对手有两种取法, 取a[j]--->[i+1 , j-1 ] 取a[i+1 ]--->[i+2 ,j] f[i][j] = max (a[i]+f[i+2 ][j], a[i]+f[i+1 ][j-1 ], a[j]+f[i][j-2 ], a[j]+f[i+1 ][j-1 ]) f[i][i] = a[i] f[i][i+1 ] = max (a[i],a[i+1 ]) 最后比较先手:f[i][j], 后手:f[i+1 ][j],f[i][j-1 ] 大小即可得答案 长度为1 ,2 时肯定为true 直接返回 [i,j]上取a[i],剩余[i+1 ][j], 而f[i+1 ][j]记录的也是和对手的差值,a[i]-f[i+1 ][j]就是此时选手和对手的差值 f[i][j] = max (a[i]-f[i+1 ][j], a[j]-f[i][j-1 ]) f[i][i] = a[i] f[0 ][n-1 ]大于等于0 则返回true bool solution(vector <int > a){ int n = a.size (); if (n==1 || n==2 ) return true ; vector <vector <int >> f (n,vector <int >(n)) ; for (int i = 0 ; i < n; ++i){ f[i][i] = a[i]; } for (int k = 1 ; k < n; ++k){ for (int i = 0 ; i < n-k; ++i){ int j = i + k; f[i][j] = max ((a[i]-f[i+1 ][j]),(a[j]-f[i][j-1 ])); } } return f[0 ][n-1 ] >= 0 ; }
困难,教程:5: 1.38
时间复杂度:O(n^4), 空间复杂度:O(n^3)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 int n = s1.length();if (n1 != s2.length()) return false ;vector <vector <vector <bool >>> f (n,vector <vector <bool >>(n, vector <bool >(n+1 ,false ))) ;for (int i = 0 ; i < n; ++i){ for (int j = 0 ;j <n; ++j){ f[i][j][1 ] = s1[i] == s2[j]; } } for (int k = 2 ; k<=n; ++k){ for (int i = 0 ; i <= n-k; ++i){ for (int j = 0 ; j <= n-k; ++j){ for (int w= 1 ; w < k ++w){ if ((f[i][j][w] && f[i+w][j+w][k-w]) || (f[i][j+k-w][w] && f[i+w][j][k-w])){ f[i][j][k] = true ; break ; } } } } } return f[0 ][0 ][n];
lint168 戳气球
困难:5:1.45
时间复杂度:O(n^3), 空间复杂度:O(n^2)
特点:消去型
逆向思维,从最后剩下的往回推
每个区间先考虑最后被扎的气球
最后被扎的气球将原区间划分为左右两个子区间
只考虑与之相邻的,最后被扎破,则意味着左右气球都扎破了,此时nums[left] * nums[k] * nums[right] = nums[i]*nums[k]*nums[j]
双序列型动态规划
lint 77 (LCS) 最长公共子序列
非连续 注意子串和子序列的区别 子串连续 子序列非连续
时间复杂度:O(MN) 空间复杂度:O(M) (滚动数组,空间优化)
类比最长回文子序列
lint 29
记录是否 记录bool
初始化不好确定
需要反复思考
lint 119 编辑距离
记录操作次数,比较大小,没有想到比较大小,复习
初始化出错,复习
lint118
记录出现次数
不同情况应该是相加而不是max,复习
初始条件的确定根据需要来
lint 154 正则表达式匹配
下次考虑从s[n-1]作为最后一步考虑,不对正则预处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
Hard
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 题目 给定数组A,每个元素是不超过100的正整数 将A中每个元素修改后形成数组B 要求B中任意两个相邻的元秦的差不能超过Target 求最小修改代价,即|A[0]-B[0]|+...+ |A[n-1]-B[n-1]| 案例 输人:A=[1,4,2,3],Target = 1 输出:2 (B=[2,3,2.3]) 题目分析 可以证明,最优策略中B的每个元素也一定是不超过100的正整数 否则,将B中小于1的数改成1,大于100的数改成100 1 若左右邻点在100内 则拉回到100 一定总修改代价更小 2 若左右邻点都在100外 都拉回到100 则差值为0 总修改代价还是减少了 这就是后续 1<=k<=100的由来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 int solution (vector <int > a) { int n = a.size (); if (n==0 ) return 0 ; vector <vector <int >> f (n+1 , vector <int >(101 )) for (int j = 1 ; j <= 100 ; ++j) { f[1 ][j] = abs (a[0 ]-j); } for (int i = 2 ;i<=n; ++i){ for (int j = 1 ; j <=100 ; ++j){ f[i][j] = INT32_MAX; for (int k = j-target; k <= j+target; ++k){ if (k<0 || k >100 ) contine; f[i][j] = min (f[i][j],f[i-1 ][k])); } f[i][j] += abs (j-a[i-1 ]); } } int res = INT32_MAX; for (int j = 1 ; j<=100 ; ++j){ if (f[n][j] < res) res = f[n][j]; } return res; }
lint89
背包问题
不可重复使用 所以考虑对象是a数组
k,target作为约束
初始化有难度,复习
lint 76 (LIS)
动态规划 O(n*n)
用二分查找可以优化为O(nlogn) 教程:7: 30—48
贪心算法 O(nlogn)
lint623
lint119加强版
直接基于lint119做时间复杂度:O(lnm)
这种方式存在重复计算
例如”abc”和”abd” 前两个字符的计算结果是一样的,应该可以重用才对
基于字母树优化
利用了空间优化的思想,因为递推公式只和当前f[i][j]
和上一层f[i-1][j]
有关系
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 int solution (vector <string > A, int t, int k) { int res = 0 ; int n = t.size (); for (int it = 0 ; it < A.size (); ++it){ string a = A[it]; int m = a.size (); vector <vector <int >> f (n+1 , vector <int >(m+1 , 0 )) ; for (int i = 0 ; i <=n; ++i){ for (int j = 0 ;j <=m; ++j){ if (i == 0 ){ f[i][j] = j; contine; } if (j == 0 ){ f[i][j] = i; contine; } f[i][j] = f[i-1 ][j-1 ]+1 ; f[i][j] = min (f[i][j], f[i][j-1 ]+1 ); f[i][j] = min (f[i][j], f[i-1 ][j]+1 ); if (t[i-1 ] == a[j-1 ]){ f[i][j] = min (f[i][j], f[i-1 ][j-1 ]); } } } if (f[n][m] <= k) ++res; } return res; }
lint622
序列+哈希表
初始化 根据题意和需要
坐标就可以用索引
k的取值范围[1,n-1] 不考虑0,因为不存在这种情况,不可能0步到达下一个点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 unordered_map <int , unordered_set <int >> f; for (auto a: s){ f[a] = {}; } f[s[0 ]] = {0 }; for (auto a : s){ unordered_set <int > nexts = f[a]; for (auto next : nexts){ for (int k = next-1 ; k <= next+1 ; ++k){ if (f.count(a+k) > 0 ){ f[a+k].insert(k); } } } } return f[s[n-1 ]].size () > 0 ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 int getOne (char c) { if (c == '*' ) return 9 ; if (c == '0' ) return 0 ; return 1 ; } int getTwo (cahr f, char l) { if (f == '1' ){ if (l == '*' ){ return 9 ; }else { return 1 ; } } if (f == '2' ){ if (l == '*' ){ return 6 ; }else if (l>= 0 && l<=6 ){ return 1 ; } } if (f == '*' ){ if (l == '*' ){ return 15 ; }else if (l>=0 && l <= 6 ){ return 2 ; }else { return 1 ; } } return 0 ; } int solution (string s) { int n = s.size (); vector <int > f (n+1 , 0 ) ; f[0 ] = 1 ; for (int i = 1 ; i <=n; ++i){ int c = getOne(s[i-1 ]); f[i] += c*f[i-1 ]; if (i > 1 ){ c = getTwo(s[i-2 ], s[i-1 ]); f[i] += c*f[i-2 ]; } } return f[n]; }
lint436 最大正方形
注意f[i][j]
记录的内容,f[i][j]可以为0
递推公式结合图更好理解,复习
L003
记忆化搜索是递归的
动态规划是迭代的 效率更高(不用递归栈,不用重复访问已经计算的结果)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 int fib (int n) { vector <int > memo (n+1 , -1 ) ; memo[0 ] = 0 ; memo[1 ] = 1 ; for (int i = 2 ; i <= n; ++i){ memo[i] = memo[i-1 ] + memo[i-2 ]; } return memo[n]; } class Solution {public : vector <int > memo; int fib (int n) { if (n == 0 ) return 0 ; if (n == 1 ) return 1 ; if (memo[n] == -1 ){ memo[n] = fib(n-1 ) + fib(n-2 ); } return memo[n]; } int fibb (int n) { memo.resize(n+1 , -1 ); return fib(n); } };
基础
70 爬楼梯
f[n] = f[n-1] + f[n-2]
f[0] = 1 需要使用但不能递推 f[2] = f[1] + f[0] = 2
f[1] = 1 事实
120 三角形最小路径和
自己的方案比官答可能更好
f[i,j] = min(f[i+1,j], f[i+1,j+1]) + triangle[i,j]
自底向上,f可以就是triangle节约空间
坐标型
打家劫舍
198 打家劫舍
经典 复习
f[i] = max(f[i-1], f[i-2] + nums[i-1])
f[0] = 0 需要使用,但是不能递推
f[1] = nums[0]
可以直接用nums作为f,节省空间
nums[i] = max(nums[i-1] , nums[i] + nums[i-2]) i >=2
nums[1] = max(nums[0], nums[1])
213 打家劫舍 II
可以不用数值记录动态过程,直接用三个值记录并更新即可
分两种情况统计
nums[0—n-2] 使用0不使用最后一个
nums[1—n-1] 不使用0使用最后一个
最后比较返回这两种情况的中大的那一个
337 打家劫舍 III
零钱类型
322 零钱兑换
记录和为w的最少硬币数
元素可重复
一维数组
return f[w],需要的就是和为w
377 组合总和 Ⅳ
记录和为w的组合个数
元素可重复
一维数组
return f[w],需要的就是和为w
1 2 3 4 5 6 * `F[i] = F[i-nums[0 ]]+ F[i-nums[1 ]] + ..... + F[i-nums[k]]` * `F[0 ] = 1 `
343 整数拆分
待复习
f[i] = max(f[i], max(j*f[i-j], j*(i-j)) 1<=j<i
f[0]=f[1] = 0
特别注意:调f[i-j]的时候是对i-j的继续拆分,不包括i-j本身 所以需要max(j*f[i-j], j*(i-j)
279 完全平方数
上面学习过,查看以前的记录
f[j] = min(f[j-i*i] + 1) 1<=i*i<=j
f[0] = 0
91 解码方法
官答更简洁 学习
f[i] = f[i-1] + f[i-2], s[i-1]-'0' > 0s时可加f[i-1], 9<(s[i-2]-'0')*10+(s[i-1]-'0')<=26时可加f[i-2]
f[0] = 1 需要使用,但是不能递推
f[1] = s[0]-‘0’ > 0 ? 1 : 0
139 单词拆分
记录是否可组合 bool
元素可重复
一维数组
return f[w]
背包问题
问题:
背包问题更多变种
完全背包问题:每个物品可以无限使用
多重背包问题:每个物品不止1个,有num(i)个
多维费用背包问题:要考虑物品的体积和重量两个维度
物品间加入更多约束: 物品间可以互相排斥;也可以互相依赖
416 分割等和子集
记录是否可以 bool
元素不可重复
二维数组
return f[n][w]
, 最后需要知道的就是n个元素种选若干个是否可以组合成w
474 一和零
记录最大子集个数
元素不可重复
三维数组,本身限制就是两维
return f[n][w][k]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
494 目标和
自己的方法
虽然使用了动态规划,但问题过于复杂,没有对问题简化再用动态规划
官方解答
先将问题简化,再使用动态规划 这种方式很重要,以后做题要注意
记数组的元素和为 sum,添加 - 号的元素之和为 neg,则其余添加 + 的元素之和为 sum-neg,得到的表达式的结果为 (sum - neg) - neg = target, 即neg = (sum - target) / 2, 由于数组 nums 中的元素都是非负整数, neg 也必须是非负整数,所以上式成立的前提是 sum−target 是非负偶数。若不符合该条件可直接返回 0
问题就转化为在数组中选取一些元素,使得和为neg,并记录有多少种这样的选择(将这些元素添加 - 号 就可以满足上式)
记录和为neg的组合数
元素不可重复
二维数组
returnf[n][neg]
, 因为要求的就是和为neg
最长子序列LIS
最长非连续递增子序列
300 最长递增子序列
动态规划 O(n*n)
贪心算法 O(nlogn)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 以输入序列 [0 ,8 ,4 ,12 ,2 ][0 , 8 , 4 , 12 , 2 ][0 ,8 ,4 ,12 ,2 ] 为例: 第一步插入 000 ,d=[0 ]d = [0 ]d=[0 ]; 第二步插入 888 ,d=[0 ,8 ]d = [0 , 8 ]d=[0 ,8 ]; 第三步插入 444 ,d=[0 ,4 ]d = [0 , 4 ]d=[0 ,4 ]; 第四步插入 121212 ,d=[0 ,4 ,12 ]d = [0 , 4 , 12 ]d=[0 ,4 ,12 ]; 第五步插入 222 ,d=[0 ,2 ,12 ]d = [0 , 2 , 12 ]d=[0 ,2 ,12 ]。
376 摆动序列
self O(n*n)
官方 O(n)
只考虑前一个元素
有难度,想不到只考虑前一个元素
关键理解点:
当 nums[i]≤nums[i−1]\textit{nums}[i] \leq \textit{nums}[i - 1]nums[i]≤nums[i−1] 时,我们无法选出更长的「上升摆动序列」的方案。因为对于任何以 nums[i]\textit{nums}[i]nums[i] 结尾的「上升摆动序列」,我们都可以将 nums[i]\textit{nums}[i]nums[i] 替换为 nums[i−1]\textit{nums}[i - 1]nums[i−1],使其成为以 nums[i−1]\textit{nums}[i - 1]nums[i−1] 结尾的「上升摆动序列」
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
最长公共子序列/子串
最长回文子序列/子串
5最长回文子串
动态规划,远不如中心扩展法
中心扩展法, 可以去提前结束循环,所以效率更高
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class Solution {public : int longestPalindromeSubseq (string s) { int n = s.size (); vector <vector <int >> f (n, vector <int >(n, 0 )) ; for (int i = 0 ; i < n; ++i){ f[i][i] = 1 ; } for (int i = n- 2 ; i >= 0 ; --i){ for (int j = i + 1 ; j < n; ++j){ if (s[i] == s[j]){ f[i][j] = f[i+1 ][j-1 ] + 2 ; }else { f[i][j] = max (f[i+1 ][j], f[i][j-1 ]); } } } return f[0 ][n-1 ]; } };
编辑距离&正则匹配
股票相关题目
309 最佳买卖股票时机含冷冻期
121 只交易一次
动态规划
方式1:
从0到N-1枚举j,即第几天卖
时刻保存当前为止(即0~j-1天)的最低价格Pi
最大的Pj-Pi即为答案
方式2:
1 2 3 4 5 6 7 // 方式2 // h: 持有 // n: 不持有 // h[i] = max(h[i-1], -prices[i]) // n[i] = max(n[i-1], h[i-1]+prices[i]) // h[0] = -prices[0] // n[0] = 0
122 交易次数不限制
方法1: 动态规划
方法2:贪心 记录所有递增段
只要当前值大于前继值则它们的差值就是获利,累加所有的获利就是最终获利(交易次数不限制)
1 2 3 4 5 6 7 方法1: 动态规划 // h: 持有 // n: 不持有 // h[i] = max(h[i-1], n[i-1]-prices[i]) // n[i] = max(n[i-1], h[i-1]+prices[i]) // h[0] = -prices[0] // n[0] = 0
123 限制两次交易
先写出动态规划递归公式,再对应写出空间优化公式
注意初始化时,不能太在意交易次数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 class Solution {public : int maxProfit (vector <int >& prices) { int n = prices.size (); int buy2 = -prices[0 ]; int buy1 = -prices[0 ]; int sell2 = 0 ; int sell1 = 0 ; for (int i = 1 ; i < n; ++i){ buy2 = max (buy2, sell1 - prices[i]); buy1 = max (buy1, -prices[i]); sell2 = max (sell2, buy2+prices[i]); sell1 = max (sell1, buy1+prices[i]); } return max (sell1, sell2); } };
188 限制k次交易
当k>=N/2时就等于是不限制交易次数,可以直接用122中的方法
因为即使不限制交易次数,最多也就交易N/2次
连续买卖可以合并(1买2卖,2买3卖 可以合并为1买3卖)
先写出动态规划递归公式,再对应写出空间优化公式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 class Solution {public : int maxProfit (int k, vector <int >& prices) { int n = prices.size (); if (k>= n/2 ){ int res = 0 ; for (int i = 1 ; i <n; ++i){ if (prices[i] > prices[i-1 ]){ res += prices[i] - prices[i-1 ]; } } return res; } vector<int> F(k+1), N(k+1); for (int j =0 ; j <=k;++j){ F[j] = -prices[0 ]; N[j] = 0 ; } for (int i = 1 ; i < n; ++i){ for (int j = 1 ; j <=k; ++j){ F[j] = max (F[j], N[j-1 ]-prices[i]); N[j] = max (N[j], F[j]+prices[i]); } } int res = 0 ; for (auto & a : N){ res = max (res, a); } return res; } };
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 class Solution {public : int maxProfit (vector <int >& prices) { int n = prices.size (); int buy = -prices[0 ]; int sell = 0 ; int profit_freeze = 0 ; for (int i = 1 ; i < n; ++i){ int temp = sell ; buy = max (buy, profit_freeze - prices[i]); sell = max (sell, buy + prices[i]); profit_freeze = temp; } return sell; } };
714 有交易费
方法1:记录下所有上升段,上升段差值大于交易非则计入收益
方法2:动态规划
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class Solution {public : int maxProfit (vector <int >& prices, int fee) { int n = prices.size (); int buy = -prices[0 ] - fee; int sell = 0 ; for (int i = 1 ; i < n; ++i){ buy = max (buy, sell - prices[i] - fee); sell = max (sell, buy + prices[i]); } return sell; } };