[TOC]
0 起步 基本数据类型:变量,强制转换,符号常量(define),const
顺序结构:赋值,scanf/printf,getchar/putchar,typedef,math 函数
区分double型的scanf和printf
常用math函数
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 #include <math.h> double fabs (double x) ; double round (double x) ; double floor (double x) ; double ceil (double x) ; double pow (double r, double p) ; double sqrt (double x) ; double log (double x) ; double sin (double x) ; double cos (double x) ; double tan (double x) ; double asin (double x) ;double acos (double x) ;double atan (double x) ;
选择结构:if,switch
循环结构:while,do while,for,break/continue
数组:memset,字符数组,string.h头文件,
1 2 #include <cstring> memset (数组名, 值, sizeof (数组名));
sscanf/sprintf (字符串格式转换)
1 2 3 4 5 6 7 8 9 10 11 int n = 12345 ;char str[6 ];sprintf (str, "%d" , n);char str[6 ] = "12345" ;int n;sscanf (str, "%d" , &n);
函数:嵌套、递归
指针:指针与数组、引用&
引用不产生副本,而是给原变量起别名
指针引用(如int* &r),在需要改指针存储的unsigned int型地址本身时使用
结构体:访问元素./->,构造函数
输入字符串汇总:
1 2 3 4 5 6 7 8 9 10 11 12 13 char str[MAXN];scanf ("%s" , str); while ((str[i]=getchar())!='\n' ) i++; gets(str); cin .getline(str, MAXN); string str2;getline(cin , str2);
文件输入
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 #include <stdio.h> char fileName[10 ] = "tmp.txt" ;char mode[3 ] = "r+" ; FILE *fp = fopen(filename, mode); int fclose ( FILE *fp ) ; while ((c = fgetc(fp)) != EOF) ... char buff[maxn];while (fgets(buf, maxn, fp)) ... int n;while (fscanf (fp, "%d" , &n) != EOF) ... fputc( int c, fp); fputs (str, fp); fprintf (fp, "%s" , str);rewind(fp); int fseek (FILE *stream, long int offset, int whence) ;
C语言字符判断函数
1 2 3 4 5 6 7 8 9 10 11 12 #include <ctype.h> isalpha (c); isdigit (c); isalnum (c); isupper (c); islower (c); isblank(c); isspace (c);
1 算法初步 1.1 排序 c++ sort
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include <algorithm> using namespace std ;sort([首元素地址], [尾元素的下一个地址], [比较函数cmp(非必填)]); sort(indices.begin (), indices.end (), [&](int i, int j){ return tasks[i][0 ] < tasks[j][0 ]; });
c qsort
1 2 3 4 5 6 7 8 #include <stdlib.h> int cmpfunc (const void * a, const void * b) { return ( *([类型]*)a - *([类型]*)b ); } qsort([数组], n, sizeof ([类型]), cmp);
写cmp函数的原则:
希望元素按什么顺序排列,就直接按照大小次序返回即可;
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 void dfs (int index) { if (index == n - 1 ) { for (int i = 0 ; i < n; i++) { printf ("%d" , a[i]); } putchar ('\n' ); } for (int i = 1 ; i <= n; i++) { if (!flag[i]) { a[index] = i; vis[i] = true ; dfs(index + 1 ); vis[i] = false ; } } }
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 void dfs (int index) { if (index == n) { cnt++; for (int i = 0 ; i < n; i++) { printf ("%d" , ans[i]); (i == n - 1 )? putchar ('\n' ) : putchar (' ' ); } return ; } for (int i = 1 ; i <= n; i++) { if (!vis[i]) { bool flag = true ; for (int pre = 0 ; pre < index; pre++) { if (abs (index - pre) == abs (i - ans[pre])) { flag = false ; break ; } } if (flag) { vis[i] = true ; ans[index] = i; dfs(index + 1 ); vis[i] = false ; } } } }
1.3 贪心 局部最优推得全局最优
1.4二分
查找序列是否存在 满足条件的元素(常规二分
1 2 3 4 5 6 7 8 9 10 11 12 13 int solve (int left, int right, int x) { int mid; while (left <= right) { mid = (left + right) / 2 ; if (A[mid] == x) return mid else if (A[mid] > x) right = mid - 1 ; else left = mid + 1 ; } return -1 ; }
查找序列第一个满足 条件的元素
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 int solve (int left, int right) { int mid; while (left < right) { mid = (left + right) / 2 ; if (条件成立) right = mid else left = mid + 1 ; } return left; }
对于左开右闭的区间(left, right],二分条件就是while ((left + 1 < right)
对于左闭右开[left, right),二分条件是while (left < right - 1)
二分拓展
注:任何正整数对1取模一定为0
1.5 Two Pointer Two Pointer即双指针,用两个指针(或下标)一同访问一个序列,加快速度
1.5.1 归并排序 two pointer思想:merge中,两个数组两个指针
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 void merge (int A[], int L1, int R1, int L2, int R2) { int i = L1, j = L2; int temp[MAXN], index = 0 ; while (i <= R1 && j <= R2) { if (A[i] <= A[j]) temp[index++] = A[i++]; else temp[index++] = A[j++]; } while (i <= R1) temp[index++] = A[i++]; while (j <= R2) temp[index++] = A[j++]; for (i = 0 ; i < index; i++) A[L1 + i] = temp[i]; } void mergeSort (int A[], int left, int right) { if (left < right) { int mid = (left + right) / 2 ; mergeSort(A, left, mid); mergeSort(A, mid + 1 , right); merge(A, left, mid, mid + 1 , right); } }
1.5.2快排 two pointer思想:partition中,一个数组、首尾双指针
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 #include <stdlib.h> #include <time.h> int randPartition (int left, int right) { int p = (int )round(1.0 * rand() / RAND_MAX * (right - left) + left); swap(A[left], A[p]); int temp = A[left]; while (left < right) { while (left < right && A[right] > temp) right--; A[left] = A[right]; while (left < right && A[left] <= temp) left++; A[right] = A[left]; } A[left] = temp; return left; } void quickSort (int L, int R) { int pivot; if (L < R) { pivot = randPartition(L, R); quickSort(L, pivot - 1 ); quickSort(pivot + 1 , R); } } int main () { ... srand((unsigned )time(NULL )); ... }
1.6 其他高效技巧
打表
递推(有时思考一下递推式比简单暴力高效很多) “有几个PAT”:计算一个给定字符串包含多少个PAT
暴力计算会超时,但利用组合的思想,计算A左边的P数,乘以A右边的T数,就能得到一个A形成的PAT个数
例如 APPAPT,中间的A左边2个P,右边1个T,总共能形成2*1=2个PAT,将所有A的结果相加得到最终结果
随机选择算法 (求第K大的数) 此处有文字游戏:如1 2 3 4 5 6,有两种理解:1)第一大 1,第二大 2;2)第一大 6,第二大 5
1 2 3 4 5 6 7 8 9 10 11 12 13 void randSelect (int A[], int left, int right, int K) { int p = randPartition(left, right); int M = p - left + 1 ; if (K == M) return A[p]; else if (K < M) return randSelect(A, left, p - 1 , K); else return randSelect(A, p + 1 , right, K - M); }
2 数学问题 简单数学
最大公约数和最小公倍数 1 2 3 4 5 6 int gcd (int a, int b) { if (b == 0 ) return a; else return gcd(b, a % b); }
1 2 3 4 5 6 int lcm (int a, int b) { return a * b / gcd(a, b); }
分数四则运算 素数 素数:不能被其他数整除的数(n%i!=0)
求素数表
大整数运算 由于大整数运算从低位开始,而字符串读入从高位,因此在转换时要记得反置
组合数
C++标准模板库STL
vector
定义
1 2 3 vector <typename > name; vector <vector <typename >> name; vector <typename > arrName[arrSize];
元素访问
使用下标:v[i]
迭代器:
vector<typename>::iterator it:类似指针,用*it访问元素
vi.begin(), vi.end():左闭右开。(只有vector和string可以使用vi.begin()+i的写法
常用函数
1 2 3 4 5 6 7 vi.push_back(x); vi.pop_back(); vi.size (); vi.clear (); vi.insert(it, x); vi.erase(it); vi.erase(first, last)
set 内部自动有序且不含重复元素的集合
string
定义
1 2 3 4 5 6 7 8 string str = "abcd" ;char c[MAXN];... string str (c) ;string str1 = c;
元素访问
直接像字符数组一样访问
直接输入输出,只能用cin和cout
1 2 3 string str;cin >> str;cout << str;
使用printf输出
1 printf ("%s\n" , str.c_str());
迭代器访问
1 2 3 4 for (string ::iterator it = str.begin (); it != str.end (); it++){ printf ("%c" , *it); }
常用函数
operator+=(字符串拼接)
1 string str3 = str1 + str2;
compare operator,按字典序比较
其他
1 2 3 4 5 6 7 8 9 10 11 12 13 str.length(); str.size (); str.insert(pos, str2); str.insert(it, first, last); str.erase(it); str.erase(first, last); str.erase(pos, length); str.clear (); str.substr(pos, len); str.find (str2); str.find (str2, pos); str.replace(pos, len, str2); str.replace(first, last, str2);
map map内的键会按从小到大顺序自动排序(因为是基于红黑树实现),键唯一
注:
map无法按value排序,只能将pair放进vector里,对vector排序
定义
访问
直接使用下标(新值会覆盖旧值)
通过迭代器访问
1 2 3 map <type1, type2>::iterator it;it->first; it->second;
常用函数
1 2 3 4 5 6 7 it = mp.find (key); mp.insert(make_pair(value1, value2)); mp.erase(it); mp.erase(key); mp.erase(first, last); mp.size (); mp.clear ();
拓展
multimap:一个键对多个值
unordered_map:只映射不排序,比纯map快很多
其他
hashmap可当作bool数组使用
map会自动初始化
queue 先进先出
priority_queue 用堆实现,队首元素是当前队列中优先级最高的一个
stack 后进先出
定义
元素访问st.top()
常用函数
1 2 3 4 st.push(x); st.top(); st.pop(); st.empty();
其他
pair 将两个元素合成为一个
定义
1 2 3 4 5 6 7 8 9 10 #include <utility> using namespace std ;pair<type1, type2> name; pair<type1, type2> p(value1, value2); pair<type1, type2>(value1, value2); make_pair(value1, value2);
元素访问
只有两个元素:p.first,p.second
常用函数
比较操作 直接使用比较符号如>=, !=。首先比较first,只有当first相等时才去比较second (可类似priority_queue重写<号)
常见用途 作为map的键值对插入
1 mp.insert(make_pair(value1, value2));s
algorithm头文件下的常用函数 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 #include <algorithm> max (x, y);min (x, y);abs (x); swap(x, y); reverse(it1, it2); typename a[N];do { printf ("%d%d%d..." , a[0 ], a[1 ], a[2 ]...); } while (next_permutation(it1, it2)); fill (it1, it2, value); sort(it1, it2, cmp); lower_bound(it1, it2, val); upper_bound(it1, it2, val);
数据结构专题 链表
定义
1 2 3 4 5 6 7 8 9 10 11 12 13 struct node { typename data; node* next; }; struct node { typename data; int next; };
空间分配
1 2 3 4 5 6 #include <stdlib.h> node* p = (node*)malloc (sizeof (node)); node* p = new node;
(申请较大动态数组时可能会失败)
内存泄漏 :使用malloc或new分配的空间,使用后没有释放,直到程序结束前一直占据。
内存泄漏会导致内存消耗过快而最终五内存可使用
malloc与free成对出现;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 node* create (vector <typename > arr) { node *p, *head; head = new node; for (int i = 0 ; i < arr.length(); i++) { p = new node; p->val = arr[i]; p->next = head->next; head->next = p; } return head; } void insert (node* head, int pos, int val) { node* p = head; int i; for (i = 0 ; i < pos - 1 && p != NULL ; i++, p = p->next); if (p == NULL || i > pos - 1 ) return ; node* q = new node; q->val = val; q->next = p->next; p->next = q; } void deleteNode (node* head, int pos) { node* p = head; node* q = NULL ; for (int i = 0 ; i < pos - 1 ; i++, p = p->next); if (p == NULL || p->next == NULL ) return ; q = p->next; p->next = q->next; delete (q); }
静态链表排序 将有效结点前置,按升序排序。之后即可用数组下标访问链表,数组序即为排序后的链表序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 struct node { int data, addr; int next; int valid; }ls[maxn]; bool cmp (const node& n1, const node& n2) { if (n1.valid && n2.valid) { return n1.data < n2.data; } else { return n1.valid > n2.valid; } } sort(ls, ls+maxn, cmp);
搜索专题 DFS 找“岔路口”与“死胡同”
比如背包问题,对每个物品,有选或不选两种选择即“岔路口”;物品重量总和超过V或已完成n个物品选择,就到达“死胡同”
用递归即可写出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void dfs (int index, int sumW, int sumC) { if (index == n) return ; dfs(index + 1 , sumW, sumC); if (sumW + w[index] <= V) { if (sumC + c[index] > max ) max = sumC + c[index]; dfs(index + 1 , sumW + w[index], sumC + c[index]); } }
BFS 访问完所有邻接点,再往下层
一般用队列实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void bfs (int s) { queue <int > q; q.push(s); while (!q.empty()) { q.pop(); for (当前元素的邻接点x) q.push(x); } }
迷宫问题
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 int X[4 ] = {0 , 0 , 1 , -1 };int Y[4 ] = {1 , -1 , 0 , 0 };int BFS () { q.push(S); inq[S.x][S.y] = true ; while (!q.empty()) { node top = q.front(); q.pop(); if (top.x == T.x && top.y == T.y) { return top.step ; } for (int i = 0 ; i < 4 ; i++) { int newX = top.x + X[i]; int newY = top.y + Y[i]; if (isValid (newX, newY) && !inq[newX][newY]) { Node node (newX, newY) ; node.step = top.step + 1 ; q.push(node); inq[newX][newY] = true ; } } } return -1 ; }
树 一些定义与概念
结点、根结点、叶子结点、子树、边
层次:根节点为第1层,往下增;
深度:即层数
高度:从最底层叶子结点(高度为1)向上逐层累加至该结点
度
结点的度:该结点子树的棵数,叶子结点的度为0
树的度:树中结点的最大的度
祖先、子孙:自己既是自己的祖先结点,也是自己的子孙结点
二叉树 概念
递归定义:要么没有根节点,是一棵空树;要么由根节点、左子树、右子树组成,子树都是二叉树 (与度为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 struct node { typename data; node* lch; node* rch; }; node* root = NULL ; node* newNode (typename x) { node* p = new node; p->data = x; p->lch = NULL ; p->rch = NULL ; return p; } void search (node* p, typename x, typename newData) { if (root == NULL ) return ; if (root->data == x) root->data = newData; search(root->lch, x, newData); search(root->rch, x, newData); } void insert (node* &root, typename x) { if (root == NULL ) { root = newNode(x); return ; } if (xxx) insert(root->lch, x); else insert(root->rch, x); } node* create (typename data[], int n) { node* root = NULL ; for (int i = 0 ; i < n; i++) { insert(root, data[i]); } return root; }
完全二叉树的递归创建
1 2 3 4 5 6 7 8 Node* create (int x) { Node* root = newNode(x); root->lch = create(2 * x); root->rch = create(2 * x + 1 ); return root; }
遍历
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 void preOrder (node* root) { if (root == NULL ) return ; printf ("%d " , root->x); preOrder(root->lch); preOrder(root->rch); } void inOrder (node* root) { if (root == NULL ) return ; inOrder(root->lch); printf ("%d " , root->x); inOrder(root->rch); } void postOrder (node* root) { if (root == NULL ) return ; postOrder(root->lch); postOrder(root->rch); printf ("%d " , root->x); } void layerOrder (node* root) { queue <node*> q; q.push(root); while (!q.empty()) { node* now = q.front(); q.pop(); printf ("%d " , root->x); if (now->lch != NULL ) q.push(now->lch); if (now->rch != NULL ) q.push(now->rch); } }
二叉树静态实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 struct node { typename data; int lch; int rch; }Node[maxn]; int root = -1 ;int index = 0 ;int newNode (int v) { Node[index].data = v; Node[index].lch = Node[index].rch = -1 ; return index++; }
注:
有些题按层序标了号,要求计算一些特性,此时可能无需建树(只需假想一棵树),利用下标计算: 当前结点: x,左子: 2x ,右子: 2x+1 , 判断超出范围:2x > n
一般的树 尽量考虑静态写法,maxn为结点数上限
1 2 3 4 5 sreuct node { typename data; vector <int > child; } Node[maxn];
从树的遍历看DFS与BFS
DFS:合法的DFS都能画出树的形式,“死胡同”等价于叶子结点;“岔路口”等价于非叶子结点,DFS遍历过程即树的先序遍历 过程 从树的角度引入对状态的剪枝
BFS:即对树的层序遍历
需要再看看的题目
codeup_611_b 子结点计算
codeup_611_d 树重建(主要看指针方式存储树的创建)
二叉查找树 BST 也叫二叉排序树
递归定义:
要么是一棵空树
要么由根节点、左右子树构成,左子树上的结点小于根结点,右子树上的结点大于根节点
性质:
编码
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 bool search (node* root, int x) { if (root == NULL ) return false ; if (x == root->data) return true ; else if (x < root->data) return search(root->lch, x); else return search(root->rch, x); } void insert (node* &root, int x) { if (root == NULL ) { root = newNode(x); return ; } if (x == root->data) return ; else if (x < root->data) insert(root->lch, x); else insert(root-rch, x); } node* create (int data[], int n) { node* root = NULL ; for (int i = 0 ; i < n; i++) insert(root, data[i]); return root; }
删除比较难,单独记录一下:
删除结点v的基本思路是:使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 node* findMax (node* root) { while (root->rch != NULL ) root = root->rch; return root; } node* findMin (node* root) { while (root->lch != NULL ) root = root->lch; return root; } void deleteNode (node* root, int x) { if (root == NULL ) return ; if (x == root->data) { if (root->lch == NULL && root->rch == NULL ) root = NULL ; else if (root->lch != NULL ) { node* pre = findMax(root->lch); root->data = pre->data; deleteNode(root->lch, pre->data); } else { node* next = findMin(root->rch); root->data = next->data; deleteNode(root->rch, next->data); } } else if (x < root->data) deleteNode(root->lch, x); else deleteNode(root->rch, x); }
注:一直删除前驱或后继会导致树退化成一条链,解决办法有:
交替删除前驱或后继
记录高度,总是优先在高度较高的一棵子树里删除结点
平衡二叉树 AVL树 使树高在每次插入后保持O(logn)级别
平衡因子 平衡因子 :左子树与右子树的高度差(ps:高度从叶子开始计算,叶子为1)
为记录平衡因子,需要在树的结构中增加变量height:
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 struct Node { int v; int height ; Node* lch, rch; }; Node* newNode (int x) { Node* node = new Node; node->height = 1 ; return node; } int getHeight (Node* root) { if (root == NULL ) return 0 ; return root->height ; } int getBalanceFactor (Node* root) { return (getHeight(root->lch) - getHeight(root->rch)); } void updateHeight (Node* root) { root->height = max (getHeight(root->lch), getHeight(root->rch)) + 1 ; }
AVL树的重点是插入,需要考虑平衡的调整:
旋转
左旋:
使B的左子树♦成为A的右子树
使A成为B的左子树、更新高度
根节点设置为B
1 2 3 4 5 6 7 8 9 10 void leftRotation (Node* &root) { Node* temp = root->rch; root->rch = temp->lch; temp->lch = root; updateHeight(root); updateHeight(temp); root = temp; }
右旋
使A的右子树成为B的左子树
使B成为A的右子树、更新高度
根节点设置为A
1 2 3 4 5 6 7 8 9 10 11 void rightRotation (Node* &root) { Node* temp = root->lch; root->lch = temp->rch; temp->rch = root; updateHeight(root); updateHeight(temp); root = temp; }
插入 AVL插入即BST插入+平衡调整
平衡调整:将最靠近插入结点的失衡结点调整到正常
四种情况(左插LL、LR,右插RR、RL):
树形
判定条件
调整方法
示图
LL
BF(root) = 2, BF(root->lch) = 1
对root右旋
LR
BF(root) = 2, BF(root->lch) = -1
对root->lch左旋,再对root右旋
RR
BF(root) = -2, BF(root->rch) = -1
对root左旋
RL
BF(root) = -2, BF(root->rch) = 1
对root->rch右旋,再对root左旋
编码
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 void insert (Node* root, int x) { if (root == NULL ) { root = newNode(x); return ; } if (x < root->data) { insert(root->lch, x); updateHeight(root); if (getBalanceFactor(root) == 2 ) { if (getBalanceFactor(root->lch) == 1 ) { rightRotation(root); } else if (getBalanceFactor(root->lch) == -1 ) { leftRotation(root->lch); rightRotation(root); } } } else { insert(root->rch, x); updateHeight(root); if (getBalanceFactor(root) == -2 ) { if (getBalanceFactor(root->rch) == -1 ) { leftRotation(root); } else if (getBalanceFactor(root->rch) == 1 ) { rightRotation(root->rch); leftRotation(root); } } } }
知道如何插入后,AVL树的建立与查找就和BST无异了:
1 2 3 4 5 6 7 8 9 Node* create (int data[], int n) { Node* root = NULL ; for (int i = 0 ; i < n; i++) { insert(root, data[i]); } return root; }
注意易错点:
search不要想复杂了,查找树按照大小选择子树查找,如果找不到,就是查找失败,无需考虑其他树上有没有该结点
涉及对root本身的修改要记得用指针引用Node* &root,这里有leftRotation, rightRotation和insert
insert之后要记得updateHeight
哈夫曼树 结点的路径长度:从根结点到该结点所经过的边数
叶子的带权路径长度 :叶子的权值乘以路径长度
树的带权路径长度:所有叶子结点的带权路径长度之和
哈夫曼树 :树的带权路径最小的二叉树(最优二叉树)
构建过程:
初始n个结点,视为n棵只有1个结点的树
合并其中权值最小 的两棵树,生成两棵子树的父结点,权值为两个根结点之和
重复步骤2,直到只剩下一棵树为止,这棵树即哈夫曼树,根结点的权值为最小带权路径长度
哈夫曼树的构建思想核心为:反复选择两个最小的元素 ,合并,直到剩下一个元素。有时无需构建树,只需通过合并,得到最终带权路径长度即可。
哈夫曼编码:
对任何一个叶子结点,其编号一定不会成为其他任何结点编号的前缀
左0右1
按频次作为权值,字符串编码为01串的长度即树的带权路径长度
并查集 定义
性质:
并查集产生的每个集合都是一棵树(因为只对不同集合进行合并,集合内不会有环)
实现:
用数组father[i]记录结点i的父节点;若i=father[i],说明i是根节点。一个集合只有一个根结点,其是集合的标志
编码:
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 for (int i = 1 ; i <= N; i++){ fa[i] = i; } int findFather (int x) { int root = x; while (root != fa[root]) root = fa[root]; while (x != fa[x]) { int tmp = x; fa[tmp] = root; x = fa[x]; } return root; } void union (int a, int b) { int faA = findFather(a); int faB = findFather(b); if (faA != faB) fa[faA] = faB; } int count () { int cnt = 0 ; for (int i = 1 ; i <= n; i++) { if (fa[i] == i) cnt++; } return cnt; }
补充路径压缩的示意图:
并查集模型的应用:
输入一条边的两个点(双向边),使用union就能将两个点并在一个集合下,进而计数集合的个数或集合内元素个数
堆 定义
堆是一棵完全二叉树
大顶堆:树中每个结点的值都不小于孩子
小顶堆:不大于
实现方式:用数组来存储完全二叉树,这样结点就按层序 存储在数组中。第一个结点存于下标1,第i号的左孩子是2i ,右孩子是2i+1 ,父亲是i/2
ps:如果不是强调自己实现堆,在可用STL的情况下,尽量用优先队列,不要造轮子
1 2 3 4 5 #include <queue> using namespace std ;priority_queue<int > pq; priority_queue< int , vector <int >, greater<int > > pq_s;
向下调整
当前结点与其孩子比较,将其中权值大的孩子与V交换
继续让V与孩子比较,直到孩子的权值都比V小或没有孩子结点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 void downAdjust (int low, int high) { int i = low, j = 2 * i; while (j <= high) { if (j + 1 <= hight && heap[j+1 ] > heap[j]) j = j + 1 ; if (heap[j] > heap[i]) { swap(heap[j], heap[i]); i = j; j = 2 * i; } else break ; } }
建堆 完全二叉树的叶子结点个数为$\lceil \frac{n}{2} \rceil$,因此在$[1, \lfloor \frac{n}{2} \rfloor]$范围内的都是非叶子结点,
n个结点的完全二叉树有性质
1)n0+n1+n2=n(其中n0表示度为0的结点) 2)n0=n2+1
3)n1 = 0 或 1
综上可得n0 = $\lceil \frac{n}{2} \rceil$
从$\lfloor \frac{n}{2} \rfloor$倒着枚举,对遍历到的结点$i$进行$[i, n]$范围的调整。
倒着调整是因为,每次调整一个结点后,当前子树中权值最大的结点就会处于子树的根结点位置(这正是向下调整的逻辑),轮到父节点调整时,又可以直接使用这一结果,保证了每个结点都是以其为根结点的子树的权值最大点
记住结论即可:
1 2 3 4 5 6 7 8 void createHeap () { for (int i = n / 2 ; i >= 1 ; i--) { downAdjust(i, n); } }
删除堆顶 思路:用最后一个结点覆盖堆顶结点(根结点),然后对根结点向下调整
1 2 3 4 5 6 void deleteTop () { heap[1 ] = heap[n--]; downAdjust(1 , 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 void upAdjust (int low, int high) { int i = high, j = i / 2 ; while (j >= low) { if (heap[j] < heap[i]) { swap(heap[j], heap[i]); i = j; j = i / 2 ; } else break ; } } void insert (int x) { heap[++n] = x; upAdjust(1 , n); }
堆排序 思路:取出堆顶元素,然后将堆的最后一个元素替换至堆顶,然后对堆顶向下调整
实际上为了节省空间,用原来存储heap的数组来存排序后的序列,所以用倒着遍历heap数组实现堆排序
i从n开始遍历,直到堆只1个元素
访问到i,将i与堆顶交换
在[1, i-1]范围对堆顶进行向下调整
1 2 3 4 5 6 7 8 9 10 11 12 13 void heapSort () { createHeap(); for (int i = n; i > 1 ; i--) { swap(heap[1 ], heap[i]); downAdjust(1 , i - 1 ); } }
总结一下
倒着遍历:
建堆:数组中已有数据,从$\lceil \frac{n}{2} \rceil$开始倒着遍历,对每个结点向下调整,建立起堆结构
堆排序:从结点n开始倒着遍历,和堆顶交换,对堆顶向下遍历,已访问的结点固定不再动
调整
向下调整:用于建堆、删除堆顶、堆排序。后二者是对堆顶向下调整,调整后,子树根结点就存放着最大元素
向上调整:用于插入时,将尾巴插入的结点调整到它的位置
图 定义:
图的存储 邻接矩阵:
一般适用于顶点数目不大(不超过1000)的题目
邻接表
Adj\[i\]存放顶点i的所有出边 组成的列表
1 2 3 4 5 6 7 struct Node { int v, w; Node(int _v, int _w) : v(_v), w(_w) {} }; vector <Node> Adj[N];
使用构造函数实现加边
1 Adj[i].push_back(Node(3 , 4 ));
图的遍历 对无向图:
连通 :两个顶点之间相互可达
连通图 :图G(V, E)中任意两个顶点连通
连通分量 :非连通图中的极大连通子图
对有向图:
强连通 :两个顶点各有一条有向路径通向另一个顶点(即可达)
强连通图 :图G(V, E)中任意两个顶点强连通
强连通分量 :非强连通图中的极大强连通子图
DFS 邻接表版:
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 vector <int > adj[MAXV];bool vis[MAXV] = {false };void DFS (int u, int depth) { vis[u] = true ; for (int i = 0 ; i < adj[u].size (); i++) { int v = adj[u][i]; if (!vis[v]) DFS(v, depth + 1 ); } } void DFSTravel () { for (int u = 0 ; u < n; u++) { if (!vis[u]) DFS(u, 1 ); } }
无向图、邻接矩阵存储,涉及边权的累计:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 void DFS (int u, int & totalW) { vis[u] = true ; for (int v = 0 ; v <nump; v++) { if (G[u][v] > 0 ) { totalW += G[u][v]; G[u][v] = G[v][u] = 0 ; if (!vis[v]) DFS(v, totalW); } } }
BFS 之前介绍过了,这里只放图的代码
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 int n, G[MAXV][MAXV];bool inq[MAXV] = {false }; void BFS (int u) { queue <int > q; q.push(u); inq[u] = true ; while (!q.empty()) { int now = q.front(); q.pop(); for (int v = 0 ; v < n; v++) { if (!inq[v] && G[u][v] != INT_MAX) { q.push(v); inq[v] = true ; } } } }
注意:清空inq数组时,要对整个MAXV长度清空,否则后面来的如果访问顶点数大于前者
最短路径 Dijkstra 算法流程:
从未访问过的点集(V-S)中找到一个与起点距离最小的点u
u作为中介点,对起点s 与 所有从u能到达的未访问顶点 进行relaxing(松弛)操作
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 const int INF = 1e9 ;int G[MAXV][MAXV];int d[MAXV];int pre[MAXV];bool vis[MAXV] = {false };void Dijkstra (int s) { fill (d, d+MAXV, INF); d[s] = 0 ; for (int i = 0 ; i < n; i++) { int u = -1 , minD = INF; for (int j = 0 ; j < n; j++) { if (!vis[j] && d[j] < MIN) { u = j; minD = d[j]; } } if (u == -1 ) return ; vis[u] = true ; for (int v = 0 ; v < n; v++) { if (!vis[v] && G[u][v] != INF && d[u] + G[u][v] < d[v]) { d[v] = d[u] + G[u][v]; pre[v] = u; } } } } void dfs (int s, int v) { if (v == s) { printf ("%d " , s); return ; } dfs(s, pre[v]); printf ("%d " , v); }
三种可能的第二标尺(多条最短路径时的其他衡量标准):
只需在relaxing操作中进行修改
新增边权(如花费) 初始化合数组d相同
1 2 3 4 5 6 7 8 9 10 11 12 13 for (int v = 0 ; v < n; v++){ if (!vis[v] && G[u][v] != INF) { if (d[u] + G[u][v] < d[v]) { d[v] = d[u] + G[u][v]; c[v] = c[u] + cost[u][v]; } else if (d[u] + G[u][v] == d[v] && c[u] + cost[u][v] < c[v]) c[v] = c[u] + cost[u][v]; } }
新增点权(如每个点的资源)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 for (int v = 0 ; v < n; v++){ if (!vis[v] && G[u][v] != INF) { if (d[u] + G[u][v] < d[v]) { d[v] = d[u] + G[u][v]; w[v] = w[u] + weight[v]; } else if (d[u] + G[u][v] == d[v] && w[u] + weight[v] > w[v]) w[v] = w[u] + weight[v]; } }
求最短路径条数
1 2 3 4 5 6 7 8 9 10 11 12 13 for (int v = 0 ; v < n; v++){ if (!vis[v] && G[u][v] != INF) { if (d[u] + G[u][v] < d[v]) { d[v] = d[u] + G[u][v]; num[v] = num[u]; } else if (d[u] + G[u][v] == d[v]) num[v] += num[u]; } }
使用Dij+dfs简化逻辑:
使用dij记录下所有最短路径
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 vector <int > pre[MAXV];for (int v = 0 ; v < n; v++){ if (!vis[v] && G[u][v] != INF) { if (d[u] + G[u][v] < d[v]) { d[v] = d[u] + G[u][v]; pre[v].clear (); pre[v].push_back(u); } else if (d[u] + G[u][v] == d[v]) pre[v].push_back(u); } }
遍历所有最短路径,找出第二标尺最优的路径 遍历过程相当于对终点为根结点,起点为叶子结点(可重复)的递归树的搜索
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 vector <int > pre[MAXV];vector <int > path, tempPath;int optValue;void dfs (int v, int curValue) { if (v == st) { if (curValue 优于 optValue) { optValue = curValue; path = tempPath; path.push_back(v); } return ; } tempPath.push_back(v); for (int i = 0 ; i < pre[v].size (); i++) { int u = pre[v][i]; dfs(u, curValue + value[u][v]); } tempPath.pop_back(); }
注意:
INF要用const int INF = 1e9定义,不要用#define,后者值为0,原因不明
所有的min都要记得初始化
Bellman-Ford与SPFA Bellman-Ford:
邻接表实现为$O(VE)$,思路如下:
进行V-1轮操作,每轮对所有边松弛
最后一轮再对所有边松弛,如果仍能松弛,说明有原点可达的负环,返回false;否则返回true;
1 2 3 4 5 6 7 8 9 10 11 12 13 d[s] = 0 ; for (int i = 0 ; i < n - 1 ; i++){ for (each edge e[u][v]) if (d[u] + e[u][v] < d[v]) { d[v] = d[u] + e[u][v]; } } for (each edge e[u][v]) if (d[u] + e[u][v] < d[v]) return false ;
SPFA:
邻接表实现为$O(kE)$,从B-F算法优化而来,思路如下:
只有当某个顶点d[u]的值改变时,从它出发的邻接点v的d[v]才有可能改变
因此建立一个队列,每次松弛后,如果顶点不在队中,将其入队。若入队次数超过V-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 vector <Node> adj[MAXV];int d[MAXV], num[MAXV];bool inq[MAXV];bool SPFA (int s) { fill (d, d + MAXV, INF); queue <int > q; q.push(s); inq[s] = true ; num[s]++; d[s] = 0 ; while (!q.empty()) { int u = q.front(); q.pop(); inq[u] = false ; for (int j = 0 ; j < adj[u].size (); j++) { int v = adj[u][v].v, dis = adj[u][v].dis; if (d[u] + dis < d[v]) { d[v] = d[u] + dis; if (!inq[v]) { q.push(v); inq[v] = true ; num[v]++; if (num[v] >= n) return false ; } } } } return true ; }
可以使用优先队列priority_queue进行优化
Floyd 全源最短路径算法,$O(n^3)$,n应限制在200以内
基本思想:枚举中介点k,使用k任意两个顶点进行松弛操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 int dis[MAXV][MAXV];void floyd () { fill (dis[0 ], dis[0 ] + MAXV * MAXV, INF); for (int k = 0 ; k < n; k++) { for (int i = 0 ; i < n; i++); { for (int j = 0 ; j < n; j++) { if (dis[i][k] != INF && dis[k][j] != INF && dis[i][k] + dis[k][j] < dis[i][j]) dis[i][j] = dis[i][k] + dis[k][j]; } } } }
最小生成树MST MST(Minimum Spanning Tree)
在给定无向图G中求一棵树T,T包含G中所有顶点,且边都来自G,满足整棵树边权之和最小
作为树,MST有:边数=顶点数-1
MST不唯一,但边权之和一定为1
一般给定一个根结点求最小生成树
Prim 复杂度$O(V^2)$,优先队列优化能达到$O(VlogV)$
基本思想:
集合S存放已访问结点,每次从V-S中选择与S距离最小的点u,将该距离最小的边加入MST,并将u加入S
令顶点u为中介,优化 所有u的邻接点 与 集合S 之间的最短距离
循环n次后,即得到MST
(Prim与Dij的思想几乎相同,区别只有:Prim松弛的是邻接点到集合S的距离,Dij松弛的是邻接点到起点之间的距离;在代码中体现为数组d[i]的意义)
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 int G[MAXV][MAXV];int d[MAXV];bool vis[MAXV] = {false };int prime (int s) { int ans = 0 ; fill (d, d + MAXV, INF); d[s] = 0 ; for (int i = 0 ; i < n; i++) { int u = -1 , minD = INF; for (int j = 0 ; j < n; j++) { if (!vis[j] && d[j] < minD) { u = j; minD = d[j]; } } if (u == -1 ) return ; vis[u] = true ; ans += d[u]; for (int v = 0 ; v < n; v++) { if (!vis[v] && G[u][v] != INF) { if (G[u][v] < d[v]) { d[v] = G[u][v]; } } } } return ans; }
Kruskal 复杂度$O(ElogE)$,开销来自于边排序
基本思想:边贪心策略
每次选择图中最小边权的边,如果加入不成环(边两端顶点在不同连通块中),就把边加入MST中
实现:使用并查集判断是否成环
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 struct Edge { int u, v; int cost; } E[MAXE]; bool cmp (edge a, edge b) { return a.cost < b.cost; } int fa[MAXV];int findFather (int x) { } int kruskal (int n, int m) { int ans = 0 , nEdges = 0 ; for (int i = 1 ; i <= n; i++) fa[i] = i; sort(E, E + m, cmp); for (int i = 0 ; i < m; i++) { int faU = findFather(E[i].u); int faV = findFather(E[i].v); if (faU != faV) { fa[faU] = faV; ans += E[i].cost; nEdges++; if (nEdges == n - 1 ) break ; } } if (nEdges != n - 1 ) return -1 ; else return ans; }
总结
prim和dij思想相近,都是不断优化d[],复杂度与顶点数有关,适用于稠密图(边多点少)
kruskal使用边贪心策略,并查集判断是否成环,复杂度与边数有关,适用于稀疏图(边少点多)
拓扑排序 有向无环图(Directed Acyclic Graph, DAG ):有向、无环(任意点都无法通过边回到自身)
拓扑排序:将DAG图的所有顶点排成一个序列,如果存在边u->v,则序列中u一定在v之前
算法步骤:
定义队列Q,将所有入度为0的结点入队
取队首结点输出,删去所有从它出发的边,并令边到达的结点入度-1
重复直到队空。如果入过队的结点数目为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 vector <int > G[MAXV];int inDegree[MAXV]; bool toplogicalSort () { int num = 0 ; queue <int > q; for (int i = 0 ; i < n; i++) { if (inDegree[i] == 0 ) q.push(i); } while (!q.empty()) { int u = q.front(); q.pop(); printf ("%d " , u); num++; for (int i = 0 ; i < G[u].size (); i++) { int v = G[u][i]; inDegree[v]--; if (inDegree[v] == 0 ) q.push(v); } G[u].clear (); } return (num == n); }
关键路径 AOV(Activity On Vertex)网:顶点活动网,用顶点表示活动,边集表示活动间优先关系的有向图
AOE(Activity On Edge)网:边活动网,带权边集表示活动,顶点表示事件的有向图。边权表示活动所需时间
源点:入度为0的点,表示起始事件;
汇点:出度为0的点,表示终止事件
关键路径 :AOE网中的最长路径 ,关键路径上的活动称为关键活动,关键路径的长度等于工程最短完成时间
最长?从路径长度上看,选择不能拖延的活动,组成的就是最长的路径,比较对象是选择其他路径的方案
最短?从时间角度看,不能拖延的活动按照时间完成就是最短的时间,比较对象是活动中有拖延的方案
AOE网实际是DAG图
求解关键路径的问题即 求解DAG图中最长路径
四个关键的数组:
数据
定义
计算
图示
e[r]
活动$a_r$的最早开始时间
e[r] = ve[i]
l[r]
活动$a_r$的最迟开始时间
l[r] = vl[j] - length[r]
(活动r最迟开始时间+活动时间=事件j最迟发生时间)
ve[i]
事件$i$的最早发生时间
ve[j] = max{ve[ip] + length[rp]}
(所有前驱事件发生、活动完成之后,Vj才能被激活,最后完成的是max)
vl[i]
事件$i$的最迟发生时间
vl[i] = min{vl[jk] - length[rk]}
(图中用了逆拓扑展示) (事件i的最迟发生,加上活动时间,不能影响到任何后继事件,最先影响到的是min)
编码:
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 stack <int > topOrder; bool toplogicalSort () { queue <int > q; for (int i = 0 ; i < n; i++) { if (inDegree[i] == 0 ) q.push(i); } while (!q.empty()) { int u = q.front(); q.pop(); topOrder.push(u); for (int i = 0 ; i < G[u].size (); i++) { int v = G[u][i].v; inDegree[v]--; if (inDegree[v] == 0 ) q.push(v); if (ve[u] + G[u][i].w > ve[v]) ve[v] = ve[u] + G[u][i].w; } } return (topOrder.size () == n); } int criticalPath () { memset (ve, 0 , sizeof (ve)); if (!toplogicalSort()) return -1 ; int maxLength = 0 ; for (int i = 0 ; i < n; i++) { if (ve[i] > maxLength) maxLength = ve[i]; } fill (vl, vl + n, maxLength); while (!topOrder.empty()) { int u = topOrder.top(); topOrder.pop(); for (int i = 0 ; i < G[u].size (); i++) { int v = G[u][i].v; if (vl[v] - G[u][v].w < vl[u]) vl[u] = vl[v] - G[u][v].w; } } for (int u = 0 ; u < n; u++) { for (int i = 0 ; i < G[u].size (); i++) { int v = G[u][i].v, w = G[u][i].w; int e = ve[u], l = vl[v] - w; if (e == l) nextPath[u] = v; } } int s; for (int i = 0 ; i < n; i++) { if (ve[i] == 0 ) { s = i; break ; } } while (nextPath[s] != s) { printf ("(%c,%c) " , vertices[s], vertices[nextPath[s]]); s = nextPath[s]; } return maxLength; }
动态规划 DP
状态:记录当前信息;状态转移方程:根据之前的状态计算当前状态值 如何设计二者是DP的核心
最优子结构:问题最优解可以由子问题的最优解构造出来
状态的无后效性:当前状态一旦确定,不再改变。只有当前状态能影响下一状态,历史信息只能通过已有状态影响决策,而不直接参与决策
分治与DP
同:都分解为子问题
异:分治的子问题不重叠,DP的重叠
贪心与DP
同:都要求最优子结构
异:贪心壮士断腕,直接选择一个子问题往下求解;DP总会考虑所有子问题,选择继承能得到最优解的一个
最大连续子序列 给定数字序列$A_1, …, A_n$,求$i, j$使$A_1 + … + A_j$最大,输出这个和
暴力:$O(n^2)$,DP:$O(n)$
状态:dp[i]表示以A[i]为结尾的连续序列最大和
状态转移方程:dp[i] = max(A[i], dp[i-1] + A[i])
一个元素:A[i]
多个元素:前面最大和dp[i-1]加上当前A[i]
1 2 3 4 dp[0 ] = A[0 ]; for (int i = 1 ; i < n; i++) dp[i] = max (A[i], dp[i-1 ] + A[i]);
最长不下降子序列 LIS LIS(Longest Increasing Sequence)
给定一个数字序列,子u最长的非下降子序列长度(可以不连续)
暴力:$O(2^n)$, DP:$O(n^2)$
状态:dp[i] 表示以A[i]结尾的LIS长度
状态转移方程:dp[i] = max{1, dp[j] + 1}(j=1, 2,…, i-1 && A[j] <= A[i])
A[i]跟在A[j]后面,LIS长度+1
A[i]自己成为一条新的LIS,长度为1
1 2 3 4 5 6 7 8 9 10 11 for (int i = 0 ; i < n; i++) dp[i] = 1 ; for (int i = 0 ; i < n; i++){ for (int j = 0 ; j < i; j++) { if (A[j] <= A[i] && dp[j] + 1 > dp[i]) dp[i] = dp[j] + 1 ; } }
最长公共子序列 LCS 给定两个字符串A和B,求字符串s,使s使A和B的最长公共部分(可以不连续)
复杂度:$O(nm)$
状态:dp[i][j] 表示A的i号位和B的j号位之前的LCS长度
状态转移方程:
如果A[i]==B[i],LCS长度为增加1位
否则,继承dp[i-1][j]和dp[i][j-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 char A[MAXN], B[MAXN];int dp[MAXN][MAXN], p[MAXN][MAXN];const int LU = 0 , U = 1 , L = 2 ;int LCS () { gets(A + 1 ); gets(B + 1 ); int lenA = strlen (A + 1 ); int lenB = strlen (B + 1 ); for (int i = 0 ; i <= lenA; i++) dp[i][0 ] = 0 ; for (int j = 0 ; j <= lenB; j++) dp[0 ][j] = 0 ; for (int i = 1 ; i <= lenA; i++) { for (int j = 1 ; j <= lenB; j++) { if (A[i] == B[j]) { dp[i][j] = dp[i - 1 ][j - 1 ] + 1 ; p[i][j] = LU; } else if (dp[i - 1 ][j] > dp[i][j - 1 ]) { dp[i][j] = dp[i - 1 ][j]; p[i][j] = U; } else { dp[i][j] = dp[i][j - 1 ]; p[i][j] = L; } } } return dp[lenA][lenB]; } void printLCS (int i, int j) { if (p[i][j] == LU) { printLCS(i - 1 , j - 1 ); putchar (A[i]); } else if (p[i][j] == U) printLCS(i - 1 , j); else printLCS(i, j - 1 ); }
最长回文串 复杂度:$O(n^2)$
状态:dp[i][j]表示S[i]到S[j]是否是回文串,是为1,不是为2
状态转移方程:
若S[i]==S[j],则只要S[i+1]至S[j-1]是回文串,S[i]至S[j]就是,因此继承dp[i+1][j-1]
若S[i]!=S[j],肯定不是回文串,dp[i][j]=0
采用区间dp 实现,即两层循环,外层枚举子串长度L,内层枚举初始位置(左端点)i,右端点=i+L-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 int ans = 0 ; for (int i = 0 ; i < len; i++){ dp[i][i] = 1 ; if (i < len - 1 && S[i] == S[i+1 ]) { dp[i][i+1 ] = 1 ; ans = 2 ; } } for (int L = 3 ; L <= len; L++){ for (int i = 0 ; i + L - 1 < len; i++) { int j = i + L - 1 ; if (S[i] == S[j] && dp[i+1 ][j-1 ] == 1 ) { dp[i][j] = 1 ; ans = L; } } } return ans;
DAG最长路 也即求关键路径,两个问题:
整个DAG中的最长路径
固定终点,求DAG的最长路径
整个DAG中的最长路径 状态:dp[i]表示从i号结点出发能获得的最长路径长度
状态转换方程:dp[i] = max{dp[j] + length[i->j]} (i,j)∈E
因为是用后继结点j更新结点i,使用递归,代替逆拓扑序列求解
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 DP (int i) { if (dp[i] > 0 ) return dp[i]; for (int j = 0 ; j < n; j++) { if (G[i][j] != INF) { int temp = DP(j) + G[i][j]; if (temp > dp[i]) { dp[i] = temp; pathNext[i] = j; } } } return dp[i]; } void criticalPath () { for (int i = 0 ; i < n; i++) { dp[i] = 0 ; pathNext[i] = i; } int ans = 0 , s; for (int i = 0 ; i < n; i++) { if (DP(i) > ans) { ans = dp[i]; s = i; } } printf ("%d" , s); while (pathNext[s] != s) { s = pathNext[s]; printf ("->%d" , s); } }
上述代码还自动实现了取字典序最小的路径(因为是按结点从小到大遍历)
固定终点的DAG最长路径 状态:dp[i]表示从i出发到终点T的最长路径长度
状态转移方程:dp[i] = max{dp[j] + length[i->j]} (i,j)∈E
与前一个问题的方程相同,区别在对边界的定义:初始化所有dp[i]=-INF来表达不可达,dp[T]=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 int DP (int i) { if (vis[i]) return dp[i]; vis[i] = true ; for (int j = 0 ; j < n; j++) { if (G[i][j] != INF) { dp[i] = DP(j) + G[i][j]; } } return dp[i]; } int criticalPath (int S, int T) { for (int i = 0 ; i < n; i++) { dp[i] = -INF; } dp[T] = 0 ; return DP(S); }
path选择以及最小字典序方案和上一个问题相同
总结:
背包问题 多阶段动态规划问题::arrow_right:问题可以描述成若干有序阶段,每个阶段(多个状态)只与上个阶段的状态有关
01背包就是多阶段DP问题
01背包 DP:$O(nV)$
n件物品,容量V的背包,每件物品重w[i],价值c[i],问如何选取物品使背包内物品总价值最大(每个物品1件)
状态与最优子结构 dp[i][v]表示选择前i项物品装入容量v的背包的最大价值
不放第i件物品:dp[i][v]=dp[i-1][v]
放第i件物品:dp[i][v] = dp[i-1][v-w[i]]+c[i]
状态转移方程
自底向上 边界dp[0][v]=0(0件物品放入任何背包容量的价值都为0) 枚举i<-1 to n,v<-w[i] to V
编码:
1 2 3 4 5 for (int i = 1 ; i <= n; i++){ for (int v = w[i]; v <= V; v++) dp[i][v] = max (dp[i - 1 ][v], dp[i - 1 ][v - w[i]] + c[i]); }
优化空间复杂度:
上一阶段的状态,用于计算当前阶段的状态后,就不再使用,因此可以用滚动数组,化为一阶数组表示当前状态
注意,自底向上的遍历必须逆序 ,因为如果正序遍历,dp[v]使用的dp[v-w[i]]是i-1阶段的状态,会造成错误。逆序保证了计算第i阶段的状态时使用的都是i-1阶段的状态
1 2 3 4 5 for (int i = 1 ; i <= n; i++){ for (int v = V; v >=w[i]; v--) dp[v] = max (dp[v], dp[v-w[i]] + c[i]); }
总结:
如果能划分阶段,可以尝试将阶段作为状态,将数组降一维
如果设计的状态不满足无后效性,可以尝试将状态升维 (多个阶段,每个阶段多个状态)来表达相应信息
完全背包问题 与01背包的区别在于,每个物品有无穷件(可选择多次)
状态与最优子结构 dp[i][v]表示选择前i项物品装入容量v的背包的最大价值
不放第i件物品:dp[i][v]=dp[i-1][v]
放第i件物品:dp[i][v] = dp[i][v-w[i]]+c[i] 注意此处转移到了第i阶段而非i-1阶段,因为放了第i件物品后还可以继续放
状态转移方程 | 优化版:
自底向上 对于优化版:边界:初始化所有dp[v]为0(即表示dp[0][v]=0)正向 枚举v<-w[i] to V,因为求解dp[i][v]总是需要它左边的dp[i][v-w[i]],而上方的dp[i-1][v]在计算完后才会覆盖
1 2 3 4 5 for (int i = 1 ; i <= n; i++){ for (int v = w[i]; v <= V; v++) dp[v] = max (dp[v], dp[v - w[i]] + c[i]); }
总结
当题目与序列或字符串A有关时,可以考虑状态设计:
令dp[i]表示以A[i]结尾(或开头)的xxx
令dp[i]表示以A[i]至A[j]区间的xxx
大多数情况都可以把DP问题看作一个有向无环图(DAG),结点是状态,边是状态转移方向,辅助理解
KMP算法 给出两个字符串text和pattern,判断pattern是否是text的子串
暴力法枚举起始点$O(nm)$,使用KMP能达到$O(n+m)$
求解next数组
KMP算法
令i指向text的欲比较位,j指向pattern已被比较的最后一位
若text[i] == pattern[j+1],说明匹配成功,i、j自增,继续比较
若text[i] != pattern[j+1],匹配失败,需要考虑j回退到哪,位置要满足以下条件:
1)text[i] == pattern[j' + 1]成立
2)pattern[0..j']仍然与text相应位置处于匹配状态,即pattern[0..j']是text[i-1-j..i-1]的后缀,由于之前匹配的结果有text[i-1-j..i-1] == pattern[0..j],因此只需满足pattern[0..j']是pattern[0..j]的后缀
3)pattern[0..j']是pattern[0..j]的前缀(自动满足,因为是回退)
4)j'尽可能大
条件2)3)4)正是数组next[j]的定义,因此只需不断令j'=next[j],直到满足条件1)或j'=-1为止。(逻辑与next数组的求解几乎一致)
从这个角度来看,next数组的含义就是当j+1位失配时,j应该回到的位置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 bool KMP (char text [], char pattern[]) { int n = strlen (text ), m = strlen (pattern); getNext(pattern, m); int j = -1 ; for (int i = 0 ; i < n; i++) { while (j != -1 && text [i] != pattern[j+1 ]) j = next[j]; if (text [i] == pattern[j+1 ]) j++; if (j == m - 1 ) return true ; } return false ; }
统计文本串text中pattern的出现次数(不是子串个数,如text=”ababab”中,pattern=”abab”出现了2次)
思路:
完全匹配一次pattern后,j需回退一定距离,使得不漏解且效率最高。由于next[j]的定义是最长前后缀匹配的前缀最后一位,完全匹配时text[i-j..i] == pattern[0..j], j=m-1,所以令j=next[j] ,在next[j]之前的位置都已完成匹配,省去了许多无意义的比较
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 int KMP (char text [], char pattern[]) { int n = strlen (text ), m = strlen (pattern); getNext(pattern, m); int j = -1 ; ans = 0 ; for (int i = 0 ; i < len; i++) { while (j != -1 && text [i] != pattern[j+1 ]) j = next[j]; if (text [i] == pattern[j+1 ]) j++; if (j == m - 1 ) { ans++; j = next[j]; } } return ans; }
总结
计算next数组实际上是pattern自我匹配的过程
KMP的关键是j回退的位置j=next[j],实质就是回到一个已知匹配的长度,从而省去从头开始比较的开销。容易模糊的地方是如何证明这个回去的位置是有效而且最优的,可以多看看上面的解释,思路就是递推求解代替逐项匹配
$O(n+m)$的开销:i和j的变化是$O(n)$的,计算next数组的开销是$O(m)$
其他 中缀表达式计算 1)中缀转换为逆波兰(后缀表达式):
(使用队列存储)
如果是数字,直接入队
如果是运算符op:如果运算符栈空,压入运算符栈,否则与栈顶比较优先级(用map记录优先级):只有在遇到” ) “的情况下我们才弹出” ( “,其他情况我们都不会弹出” ( “
1 2 3 4 5 6 while (栈非空 && 栈顶 != '(' && op小于或等于栈顶){ }
如果遇到一个右括号,则将栈元素弹出(入队),将弹出的操作符输出直到遇到左括号为止。注意,左括号只弹出并不输出。
表达式扫描结束后,若运算符栈非空,从栈顶依次入队
2)逆波兰表达式运算:
(由队首依次出队)
如果是数字,压栈
如果是运算符,取栈顶两个数字,运算,将结果压栈
队列扫描结束后,栈顶数字即最终结果
3)中缀表达式直接计算: (使用数字栈和运算符栈)
如果是数字,压栈
如果是运算符,同1),不过弹栈时,取数字栈顶两个元素运算后压栈,而非入队
同1),弹栈时运算而非入队
C语言变长参数函数写法 使用头文件<stdarg.h> 实现,下面为用到的四个宏:
va_list:声明指向变长参数的指针ap
va_start(ap, arg):用于初始化变长参数指针ap的位置,arg为最后一个固定参数
va_arg(ap, type):ap传入变长参数指针,type为参数类型。函数返回ap当前指向的类型为type的参数,并将ap指向参数列表的下一个参数
va_end(ap):置ap为空指针
变长参数函数的参数中,先填写若干个固定参数,然后在最后一个固定参数的逗号后方添加3个点,表示变长参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include <stdarg.h> #include <cstdio> void test (int num, ...) { va_list ap; va_start(ap, num); char * str; for (int i = 0 ; i < num; i++) { str = va_arg(ap, char *); printf ("%s\n" , str); } va_end(ap); } int main () { test(3 , "hello" , "fxxking" , "world" ); }
Debug思路
注意考虑边界情况 ,如0、有质数的题考虑1
没有编码错误的情况下,找逻辑错误 (代码是否和题目逻辑一致),着重检查循环里的数组下标i,j,k
注意浮点数精度:float能保证6位,double能保证到15位。有时float错了可能double能过
可能没有考虑多组输入
格式问题,有些题目可能输出点个数为0时需输出空行
无法调试可能的原因:
声明const char []型数据时,一定要检查后面有没有添加'\0' ,否则可能会出现本地没问题但交上去WA的情况。比如:const char ONE[2] = {'1', '\0'};能过,但const char ONE[1] = {'1'};在Linux环境下会出现怪数,但windows下结果正常
同阶时间复杂度 的算法,实际表现可能有很大差异。比如O(nlogn)和O(2nlog(2n))。当RE时,可能其他同阶算法能过
sort的cmp函数 中如果使用return a >= b可能会有段错误(原因不明),改用return a > b,不影响语义
多组输入,记得对每个用到的数据结构进行初始化
杂
字符转数字
1 2 3 num = 0 ; for (int i = 0 ; i < len; i++) num = num * 10 + str[i] - '0' ;
注意先找规律,n很大的不要一上来就递归DP,很容易超时。有时可能往简单想能做出来
int的范围[-2147483648, 2147483647],10个0就无法支持了,一般取INF=1e9
ceil():向上取整;floor():向下取整;round():四舍五入;三者都返回double值
万能库:include <bits/stdc++.h>
long和int都是4字节,long long才是8字节
21!就超过了long long的范围
cin和cout远比 scanf和printf花费时间多,能不用尽量不用
运行时限为1s,算法复杂度不能超过百万级,也即不能超过一千万 比如$O(n^2)$,n应<=3000
cin的多组输入:while(cin >> x),cin在读入EOF时返回0
浮点数比较,在经过大量计算后应考虑误差
1 2 3 4 #include <math.h> const double eps = 1e-8 #define Equ(a, b) (fabs ((a)-(b))<(eps))
大于、小于、大于等于,以及sqrt、asin、cos等也需要考虑
C的空指针为NULL,c++11引入nullptr
scanf("%c")会可以读入空格,需要考虑
下标的计算(i、j的加加减减之类的)从实体加减考虑,不要考虑加减位置
C语言中整型最大值和最小值的宏
1 2 3 4 #include <limits.h> printf ("%d " , INT_MAX); printf ("%d " , INT_MIN);
命令行输入
1 2 3 4 5 6 int main ( int argc, char *argv[ ]) { int i; for (i=1 ; i<argc; i++) printf (“%s%c”, argv[i], (i< argc-1 )? ' ' : '\n' ); }