-
Notifications
You must be signed in to change notification settings - Fork 0
/
search.json
1 lines (1 loc) · 56.1 KB
/
search.json
1
[{"title":"字符串专题","url":"/2020/03/20/String/","content":"\nKMP喜提我狗命\n\n<!-- more -->\n## 字符串hash初步\n```cpp\n/*字符串hash是指将一个字符串S映射为一个整数,使得该整数可以尽可能唯一地代表字符串S。\n例如:假设字符串均由大写字母A-Z构成。A-Z表示0-25,二十六进制,构成的最大整数26^len - 1\n*/\nint hashFunc(char S[], int len) {\n\tint id = 0;\n\tfor(int i = 0; i < len; i++) {\n\t\tid = id * 26 + (S[i] - 'A');\n\t}\n\treturn id;\n}\n/*同理,如果字符串中出现了小写字母,那么把A-Z视为0-25,而把a-z作为26-51,五十二进制\n*/\nint hashFunc(char S[], int len) {\n\tint id = 0;\n\tfor(int i = 0; i < len; i++) {\n\t\tif(S[i] >= 'A' && S[i] <= 'Z') {\n\t\t\tid = id * 52 + (S[i] - 'A');\n\t\t}else if(S[i] >= 'a' && S[i] <= 'z') {\n\t\t\tid = id * 52 + (S[i] - 'a') + 26;\n\t\t}\n\t}\n\treturn id;\n}\n/*如果出现数字\n① 按照瞎写字母的方式,增大进制数至62。\n② 如果字符串的末尾是数字,那么把前面英文字母部分转换成整数,再将末尾的数字直接拼接上去。例如\"BCD4\"\n*/\nint hashFunc(char S[], int len) {\n\tint id = 0;\n\tfor(int i = 0; i < len - 1; i++) {\n\t\tid = id * 26 + (S[i] - 'A');\n\t}\n\tid = id * 10 + (S[len - 1] - '0');\n}\n\n//question: 给出N个字符串(由恰好三位大写字母组成),再给出M个查询字符串,问每个查询字符串在N个字符串中出现的次数。\n```\n## 字符串hash进阶\n```cpp\n//H[i] = (H[i-1] × p + index(str[i])) % mod\n//其中p = 10000019,mod = 1000000007\n\n//questin: 给出N个只有小写字母的字符串,求其中不同的字符串个数。\n//solution: 把每个字符串转化成整数,然后去重。(可以用set和map,但比字符串hash慢一点点)unordered_set呢?\n#include <iostream>\n#include <string>\n#include <vector>\n#include <algorithm>\nusing namespace std;\nconst int MOD = 1000000007;\nconst int P = 10000019;\nvector<int> ans;\n\nlong long hashFunc(string str) {\n\tlong long H = 0;\n\tfor(int i = 0; i <str.size(); i++) {\n\t\tH = (H * P + (str[i] - 'a')) % MOD;\n\t}\n\treturn H;\n}\nint main() {\n\tstring str;\n\twhile(getline(cin, str), str != \"#\") {\n\t\tlong long id = hashFunc(str);\n\t\tans.push_back(id);\n\t}\n\tsort(ans.begin(), ans.end());\n\tint count = 0;\n\tfor(int i = 0; i < ans.size(); i++) {\n\t\tif(i == 0 || ans[i] != ans[i-1]) {\n\t\t\tcount++;\n\t\t}\n\t}\n\tcout << count << endl;\n\treturn 0;\n}\n\n//question: 输入两个长度均不超过1000的字符串,求他们的最长公共子串的长度。\n//solution: 分别对两个字符串的每个子串求出hash值,然后找出两堆子串对应的hash值中相等的那些,便可以找到最大长度。\n//T(n) = O(n² + m²)\n//变式: 输出最大公共子串本身codeup2432\n\n//question: 最长回文子串\n//solution: 字符串hash + 二分\n//T(n) = O(nlogn)\n```\n## KMP 算法\n```cpp\n//KMP算法由Knuth、Morris、Pratt共同发现的。\n\n//求解next数组:\nvoid getNext(char s[], int len) {\n\tnext[0] = -1;\n\tint j = next[0];\n\tfor(int i = 1; i < len; i++) {\n\t\twhile(j != -1 && s[i] != s[j + 1])\n\t\t\tj = next[j];\n\t\tif(s[i] == s[j + 1]) j++;\n\t\tnext[i] = j;\n\t}\n}\n\n/*算法思想:\n① 初始化j = -1,表示pattern当前已被匹配的最后位。\n② 让i遍历文本串text,对每个i,执行③④来试图匹配text[i]和pattern[j+1]。\n③ 不断令j = next[j],直到j回退为-1,或是text[i] == pattern[j+1]成立。\n④ 如果text[i] == pattern[j+1],则令j++。如果j达到m-1,说明pattern是text的子串,返回true。\n*/\nbool KMP(char text[], char pattern[]) {\n\tint n = strlen(text), m = strlen(pattern);\n\tgetNext(pattern, m);\n\tint j = -1;\n\tfor(int i = 0; i < n; i++) {\n\t\twhile(j != -1 && text[i] != pattern[j + 1])\n\t\t\tj = next[j];\n\t\tif(text[i] == pattern[j + 1]) j++;\n\t\tif(j == m - 1) return true;\n\t}\n\treturn false;\n}\n\n//统计模式串pattern出现次数的KMP算法\nint KMP(char text[], char pattern[]) {\n\tint n = strlen(text), m = strlen(pattern);\n\tgetNext(pattern, m);\n\tint j = -1, ans = 0;\n\tfor(int i = 0; i < n; i++) {\n\t\twhile(j != -1 && text[i] != pattern[j + 1])\n\t\t\tj = next[j];\n\t\tif(text[i] == pattern[j + 1]) j++;\n\t\tif(j == m - 1) {\n\t\t\tans++;\n\t\t\tj = next[j];\n\t\t}\n\t}\n\treturn ans;\n}\n\n//优化next数组: nextval[]\nvoid getNextval(char s[], int len) {\n\tint j = -1;\n\tnetxval[0] = -1;\n\tfor(int i = 1; i < len; i++) {\n\t\twhile(j != -1 && s[i] != s[j + 1])\n\t\t\tj = nextval[j];\n\t\tif(s[i] == s[j + 1]) j++;\n\t\tif(j == -1 || s[i + 1] != s[j + 1])\n\t\t\tnextval[i] = j;\n\t\telse\n\t\t\tnextval[i] = nextval[j];\n\t}\n}\n\n//从有限自动机的角度看待KMP算法\n```\n\n","tags":["字符串"],"categories":["algorithm"]},{"title":"动态规划专题","url":"/2020/03/18/DP/","content":"\nDynamic Programming\n\n<!-- more -->\n\t* 动态规划是一种用来解决一类最优化问题的算法思想\n\t* 动态规划将一个复杂问题分解成若干子问题,通过综合子问题的最优解来求得原问题最优解\n\t* 动态规划会将每个求解过的子问题的解记录下来,这样当下一次碰到同样子问题时,就可以直接使用之前记录的结果,而不是重复计算\n\t* 状态的无后效性:当前状态记录了历史信息,一旦当前状态确定,就不会改变,且未来的决策只能在已有的一个或若干个状态的基础上进行,\n\t 历史信息只能通过已有的状态去影响未来的决策。\n\t* 一个问题必须拥有\"重叠子问题\"和\"最优子结构\",才能用动态规划解决。\n\t* 重叠子问题:如果一个问题可以被分解为若干个子问题,且这些子问题会重复出现。\n\t* 最优子结构:如果一个问题的最优解可以由其子问题的最优解有效地构造出来,那么称为这个问题拥有的最优子结构。\n\t 最优子结构保证了动态规划中的原问题的最优解可以由子问题的最优解推导而来。\n\t* 分治与动态规划:分治和动态规划都是将问题分解为子问题,然后合并子问题的解得到原问题的解,\n\t 但是不同的是,分治法分解出的子问题时不重叠的,因此分治法解决的问题不拥有重叠子结构,而动态规划解决的问题拥有重叠子结构。\n\t* 贪心与动态规划:贪心和动态规划都要求原问题必须拥有最优子结构。二者的区别在于,\n\t 贪心直接选择一个子问题去求解,会抛弃一些子问题,这种选择的正确性需要用归纳法证明,而动态规划会考虑所有的子问题。\n\t* 动态规划的核心是\"如何设计状态和状态转移方程\"。\n## 递归法和递推法\n```cpp\n递归写法(记忆化搜索)\n斐波那契(Fibonacci)数列\n//T(n) = O(2^n)\nint F(int n) {\n\tif(n == 0 || n == 1) return 1;\n\telse return F(n-1) + F(n-2);\n}\n\ndp[n]记录F[n]的结果,并用dp[n] = -1表示F[n]当前还没有被计算过。\n//T(n) = O(n)\nint dp[MAXN];\nint F(int n) {\n\tif(n == 0 || n == 1) return 1;\n\tif(dp[n] != -1) return dp[n];\n\telse {\n\t\tdp[n] = F(n-1) + F(n-2);\n\t\treturn dp[n];\n\t}\n}\n```\n```cpp\n递推写法\n数塔问题\n\n令dp[i][j]表示从第i行第j个数字除法的到达最底层的所有路径中能得到的最大和。\n则状态转移方程:\n\tdp[i][j] = max{dp[i+1][j],d[i+1][j+1]} + f[i][j]\n\n#include <cstdio>\n#include <algorithm>\nuisng namespace std;\nconst int maxn = 1000;\nint f[maxn][maxn], dp[maxn][maxn];\nint main() {\n\tint n;\n\tscanf(\"%d\", &n);\n\tfor(int i = 1; i <= n; i++) {\n\t\tfor(int j = 1; j <= i j++) {\n\t\t\tscanf(\"%d\", &f[i][j]);\n\t\t}\n\t}\n\t//边界(第n层)\n\tfor(int j = 1; j <= n; j++) {\n\t\tdp[n][j] = f[n][j];\n\t}\n\t//从第n-1层不断往上计算出dp[i][j]\n\tfor(int i = n - 1; i >= 1; i--) {\n\t\tfor(int j = 1; j <= i; j++) {\n\t\t\tdp[i][j] = max{dp[i+1][j],d[i+1][j+1]} + f[i][j];\n\t\t}\n\t}\n\tprintf(\"%d\\n\", dp[1][1]); //dp[1][1]即为所需要的答案\n\treturn 0;\n}\n```\n## 最大连续子序列和\n```cpp\n给定一个数字序列A1,A2,···,An,求i,j(1≤i≤j≤n),使得Ai+···+Aj最大,输出这个最大和\n样例:-2 11 -4 13 -5 -2\n则最大和为20(11-4+13)\n\n步骤1:令状态dp[i]表示以A[i]作为末尾的连续序列的最大和,以样例为例:序列-2 11 -4 13 -5 -2,下标分别为0,1,2,3,4,5,那么\ndp[0] = -2\ndp[1] = 11\ndp[2] = 7\ndp[3] = 20\ndp[4] = 15\ndp[5] = 13\n步骤2:作如下考虑:因为dp[i]要求是必须以A[i]结尾的连续序列,那么只有两种情况:\n① 这个最大和的连续序列只有一个元素,即以A[i]开始,以A[i]结尾\n② 这个最大和的连续序列有多个元素,即以前面某处A[p]开始(p<i),一直到A[i]结尾\n对第一种情况,最大和就是A[i]本身\n对第二种情况,最大和是dp[i-1]+A[i],即A[p]+···+A[i-1]+A[i] = dp[i-1]+A[i]\n由于只有两种情况,于是得到状态转移方程:\n\t\t\t\tdp[i] = max{A[i], dp[i-1]+A[i]}\n\n//T(n) = O(n)\n#include <cstdio>\n#include <algorithm>\nusing namespace std;\nconst int maxn = 10010;\nint A[maxn], dp[maxn];\nint main() {\n\tint n;\n\tscanf(\"%d\", &n);\n\tfor(int i = 0; i < n; i++) {\n\t\tscanf(\"%d\", A + i);\n\t}\n\tint sum = dp[0] = A[0];\n\tfor(int i = 1; i < n; i++) {\n\t\tdp[i] = max(A[i], dp[i-1] + A[i]);\n\t\tsum = max(sum, dp[i]);\n\t}\n\tprintf(\"%d\\n\", sum);\n}\n```\n## 最长不下降子序列\n```cpp\n在一个数字序列中,找到一个最长的子序列(可以不连续),使得这个子序列是不下降(非递减)的。\n例如,现有序列A={1,2,3,-1,-2,7,9}(下标从1开始),它的最长不下降子序列是{1,2,3,7,9},长度为5\n\n令dp[i]表示以A[i]结尾的最长不下降子序列长度,这样对A[i]来说有两种可能:\n① 如果存在A[i]之前的元素A[j](j<i)使得A[j]≤A[i]且dp[j] + 1 > dp[i](即\n把A[i]跟在以A[j]结尾的LIS后面时能比当前以A[i]结尾的LIS长度更长),那么就把A[i]跟在A[j]结尾的LIS后面,\n形成一条更长的不下降子序列(令 dp[i] = d[j] + 1)\n② 如果A[i]之前的元素都比,那么A[i]就只好自己形成一条LIS,但是长度为1,即这个子序列里面只有一个A[i]。\n最后以A[i]结尾的LIS长度就是①②中能形成的最大长度\n由此可以写出状态转移方程:\n\t\t\tdp[i] = max{1, dp[j]+1}\n\t (j=1,2,···,i-1 && A[j]<A[i])\n\n#include <cstdio>\n#include <algorithm>\nusing namespace std;\nconst int N = 100;\nint A[N], dp[N];\nint main() {\n\tint n;\n\tscanf(\"%d\", &n);\n\tfor(int i = 1;i <= n; i++) {\n\t\tscanf(\"%d\", A + i);\n\t}\n\tint ans = -1; //记录最大的dp[i]\n\tfor(int i = 1; i <= n; i++) {\n\t\tdp[i] = 1; //边界初始条件(即先假设每个元素自成一个序列)\n\t\tfor(int j = 1; j < i; j++) {\n\t\t\tif(A[i] >= A[j] && (dp[j] + 1 > dp[i])) {\n\t\t\t\tdp[i] = dp[j] + 1; //状态转移方程,用以更新dp[i]\n\t\t\t}\n\t\t}\n\t\tans = max(ans, dp[i]);\n\t}\n\tprintf(\"%d\", ans);\n\treturn 0;\n}\n```\n## 最长公共子序列(LCS)\n```cpp\n给定两个字符串(或数字序列)A 和 B,求一个字符串,使得这个字符串是A和B的最长公共部分(子序列可以不连续)\n样例:字符串\"sadstory\"和\"adminsorry\"的最长公共子序列为\"adsory\"长度为6\n\n令dp[i][j]表示字符串A的i号位和字符串B的j号位之前的LCS长度(下标从1开始),如dp[4][5]表示\"sads\"与\"admin\"的LCS长度。那么可以根据A[i]和B[j]的情况,分为两种决策:\n① 若A[i] == B[j],则字符串A与字符串B的LCS增加了1位,即有dp[i][j] = dp[i-1][j-1] + 1。\n② 若A[i] ≠ B[j],则字符串A的i号位和字符串B的j号位之前的LCS无法1延长,因此dp[i][j]将会继承dp[i-1][j]和dp[i][j-1]中的较大值,即有dp[i][j] = max{dp[i-1][j],dp[i]][j-1]}。\n由此得到状态转移方程:\n\t\tdp[i][j] = dp[i-1][j-1], if A[i] == B[i]\n\t\tdp[i][j] = max{dp[i-1][j], dp[i][j-1]}, if A[i] ≠ B[i]\n边界:dp[i][0] = dp[0][j] = 0 (0≤i≤n, 0≤j≤m)\n最终dp[n][m]即为答案\n\n//T(n) = O(nm);\n#include <cstdio>\n#include <cstring>\n#include <algorithm>\nusing namespace std;\nconst int N = 100;\nchar A[N], B[N];\nint dp[N][N] = {0};\nint main() {\n\tint n;\n\tgets(A + 1); gets(B + 1);\n\t/*\n\tfor(int i = 0; i <= strlen(A + 1); i++) {\n\t\tdp[i][0] = 0;\n\t}\n\tfor(int j = 0; j <= strlen(A + 1); j++) {\n\t\tdp[0][j] = 0;\n\t}\n\t*/\n\tfor(int i = 1; i <= strlen(A + 1); i++) {\n\t\tfor(int j = 1; j <= strlen(B + 1); j++) {\n\t\t\tif(A[i] == B[j]) {\n\t\t\t\tdp[i][j] = dp[i-1][j-1] + 1;\n\t\t\t}else {\n\t\t\t\tdp[i][j] = max(dp[i-1][j], dp[i][j-1]);\n\t\t\t}\n\t\t}\n\t}\n\tprintf(\"%d\\n\", dp[strlen(A + 1)][strlen(B + 1)]);\n\treturn 0;\n}\n```\n## 最长回文子串\n```cpp\n给出一个字符串s,求S的最长回文子串的长度。\n样例:字符串\"PATZJUJZTACCBCC\"的最长回文子串是\"ATZJUJZTA\",长度为9。\n\n令dp[i][j]表示S[i]至S[j]所表示的子串是否是回文子串,是则为1,不是为0。\n这样根据S[i]是否等于S[j],可以把转移情况分为两类:\n① 若S[i] == S[j],那么只要S[i+1]至S[j-1]是回文子串,S[i]至S[j]就是回文子串;\n如果S[i+1]至S[j-1]不是回文子串。则S[i]至S[j]一定不是回文子串。\n② 若S[i] ≠ S[j],那么S[i]至S[j]一定不是回文子串。\n由此可以写出状态转移方程:\n\t\tdp[i][j] = dp[i+1][j-1], if S[i] == S[j]\n\t\tdp[i][j] = 0, if S[i] ≠ S[j]\n边界:dp[i][i] = 1, dp[i][i+1] = (S[i] == S[i+!]) ? 1 : 0\n\n//T(n) = O(n²)\n#include <iostream>\n#include <cstdio>\n#include <cstring>\nusing namespace std;\nconst int maxn = 1010;\nchar S[maxn];\nint dp[maxn][maxn];\nint main() {\n\tcin.getline(S, maxn);\n\tint len = strlen(S), ans = -1;\n\tmemset(dp, 0, sizeof(dp));\n\t//边界\n\tfor(int i = 0; i < len; i++) {\n\t\tdp[i][i] = 1;\n\t\tif(i < len - 1) {\n\t\t\tif(S[i] == S[i+1]) {\n\t\t\t\tdp[i][i+1] = 1;\n\t\t\t\tans = 2; //当前最长回文子串长度\n\t\t\t}\n\t\t}\n\t}\n\t//状态转移方程\n\tfor(int L = 3; L <= len; L++) {\n\t\tfor(int i = 0; i + L -1 < len; i++) {\n\t\t\tint j = i + L - 1; //子串右端点\n\t\t\tif(S[i] == S[j] && dp[i+1][j-1] == 1) {\n\t\t\t\tdp[i][j] = 1;\n\t\t\t\tans = L;\n\t\t\t}\n\t\t}\n\t}\n\tprintf(\"%d\\n\", ans);\n\treturn 0;\n}\n\n二分+字符串hash T(n) = O(nlogn)\nManacher算法 T(n) = O(n)\n```\n## DAG最长路\n```cpp\n解决两个问题\n① 求整个DAG中的最长路径(不固定起点和终点)\n② 固定终点,求DAG的最长路径\n\n第一个问题:给定一个有向无环图,怎样求解整个图的所有路径中权值之和最大的那条。\n令dp[i]表示从i号顶点除法能获得的最长路径长度,这样所有dp[i]的最大值就是整个DAG的最长路径长度。\n如果从i号顶点出发能直接到达顶点j1,j2,···,jk,而dp[j1], dp[j2], ···,dp[jk]均已知,\n那么就有dp[i] = max{dp[j] + length[i->j],(i,j)∈E}\n根据上面思路可以按照逆拓扑序列的顺序来求解dp数组\n所以我选择递归(。・ω・。)\n\nint DP(int i) {\n\tif(dp[i] > 0) return dp[i];\n\tfor(int j = 0; j < n; j++) {\n\t\tif(G[i][j] != INF) {\n\t\t\tdp[i] = max(dp[i], DP[j] + G[i][j]);\n\t\t}\n\t}\n\treturn dp[i];\n}\n\n开一个int型choice数组记录最长路径的后继结点,\n如果最终有多条路径,将choice数组改为vector类型的数组(Dijkstra算法中有多条路径时的做法),记得去实现它哦。\n还有如何求解最长路径条数。\n\nint DP(int i) {\n\tif(dp[i] > 0) return dp[i];\n\tfor(int j = 0; j < n; j++) { //遍历i的所有出边\n\t\tif(G[i][j] != INF) {\n\t\t\tint temp = DP[i] + G[i][j];\n\t\t\tif(temp > dp[i]) {\n\t\t\t\tdp[i] = temp;\n\t\t\t\tchoice[i] = j; //i号顶点的后继顶点是j\n\t\t\t}\n\t\t}\n\t}\n\treturn dp[i]; //返回计算完的dp[i]\n}\n调用printPath前需要先得到最大的dp[i],然后将i作为路径起点传入\nvoid printPath(int i) {\n\tprintf(\"%d\", i);\n\twhile(choice[i] != -1) { //choice数组初始化为-1\n\t\ti = choice[i];\n\t\tprintf(\"->%d\", i);\n\t}\n}\n\n第二个问题:固定终点,求DAG的最长路径\n令dp[i]表示从i号顶点出发到达终点(假设终点为T)能获得的最长路径长度,\n同理,如果从i号顶点出发能直接到达顶点j1,j2,···,jk,而dp[j1], dp[j2], ···,dp[jk]均已知,\n那么就有dp[i] = max{dp[j] + length[i->j],(i,j)∈E}\n边界:dp[T] = 0, so problem is coming, 数组不能初始化为0了\n合适做法:初始化dp数组为一个负的大数来保证\"无法到达终点\"的含义得以表达,然后设置一个vis数组表示顶点是否已经被计算。\n\nint DP(int i) {\n\tif(vis[i] == true) return dp[i]; //dp[i]已计算得到直接返回,减少重复计算\n\tvis[i] = true;\n\tfor(int j = 0; j < n; j++) {\n\t\tif(G[i][j] != INF) {\n\t\t\tdp[i] = max(dp[i], DP[j] + G[i][j]);\n\t\t}\n\t}\n\treturn dp[i];\n}\n\n经典问题:矩形嵌套问题\n```\n## 背包问题\n```cpp\n01背包问题:\n有n件物品,每件物品的重量为w[i],价值为c[i]。现有一个容量为V的背包,\n问如何选取物品放入背包,使得背包内物品的总价值最大。其中每件物品都只有1件。\n样例:\n5 8 // n == 5,v == 8\n3 5 1 2 2 // w[i]\n4 5 2 1 3 // c[i]\nT(n) = O(nV)\n\n令dp[i][j]表示前i件物品(1 ≤ i ≤ n,0 ≤ j ≤ v)恰好装入容量为j的背包中所能获得的最大价值。然后求解dp[i][j]:\n考虑对第i件物品的选择策略,有两种:\n① 不放第i件物品,那么问题转化为前i-1件物品恰好装入容量为j的背包中所能获得的最大价值。也即dp[i-1][j]。\n② 放第i件物品,那么问题转化为前i-1件物品恰好装入容量为j-w[i]的背包中所能获得的最大价值。也即dp[i-1][j-w[i]] + c[i]。\n因此状态转移方程为:\n\tdp[i][j] = max{dp[i-1][j], dp[i-1][j-w[i]] + c[i]}\n\t(1 ≤ i ≤ n,w[i] ≤ j ≤ v)\n边界: dp[0][j] = 0(0 ≤ j ≤ v)\n答案: dp[n][v]\n空间复杂度: O(nv)\nfor(int i = 1; i <= n; i++) {\n\tfor(int j = 1; j <= v; j++) {\n\t\tif(j < w[i]) { //鲲之大,一包装不下\n\t\t\tdp[i][j] = dp[i-1][j];\n\t\t}else {\n\t\t\tdp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]] + c[i]);\n\t\t}\n\t}\n}\n\n优化: 一维: \n\tdp[j] = max{dp[j], dp[j-w[i]] + c[i]}\n\t(1 ≤ i ≤ n,w[i] ≤ j ≤ v)\n边界: dp[j] = 0, 0 ≤ j ≤ v\n答案: dp[v]\n空间复杂度: O(v)\nfor(int i = 1; i <= n; i++) {\n\tfor(int j = v; j >= w[i]; j--) {\n\t\tdp[j] = max(dp[j], dp[j-w[i]] + c[i]);\n\t}\n}\n优化版完整代码:\n#include <cstdio>\n#include <algorithm>\nusing namespace std;\nconst int maxn = 100;\nconst int maxv = 1000;\nint w[maxn], c[maxn], dp[maxn] = {0};\nint main() {\n\tint n, v;\n\tscanf(\"%d%d\", &n, &v);\n\tfor(int i = 0; i < n; i++) {\n\t\tscanf(\"%d\", w + i);\n\t}\n\tfor(int i = 0; i < n; i++) {\n\t\tscanf(\"%d\", c + i);\n\t}\n\tfor(int i = 1; i <= n; i++) {\n\t\tfor(int j = v; j >= w[i]; j--) {\n\t\t\tdp[j] = max(dp[j], dp[j-w[i]] + c[i]);\n\t\t}\n\t}\n\tprintf(\"%d\\n\",dp[v]);\n\treturn 0;\n}\n\n如果需要知道是哪些物品被放入了背包,可用\"回溯算法\"(针对未优化版本)\n算法思想:\n从表的右下角dp[n][v]开始回溯,\n如果发现前n个物品最佳组合的价值和前n-1个物品的最佳组合的价值一样,说明第n个物品没有被装入。\n否则,第n个物品被装入。\nsoluton: 增加一个object[], object[i] = true表示第i个物品被装入, false表示没装入。\nbool object[maxn] = {false};\nvoid Find(int i, int j) {\n\tif(i == 0) {\n\t\tfor(int i = 0; i < n; i++) {\n\t\t\tif(i) printf(\" \");\n\t\t\tprintf(\"%d\", object[i]);\n\t\t}\n\t\treturn;\n\t}\n\tif(dp[i][j] == dp[i-1][j]) { //第i个物品没有被装入\n\t\tobject[i] = false;\n\t\tFind(i-1, j);\n\t}else { //第i个物品被装入\n\t\tobject[i] = true;\n\t\tFind(i-1,j-w[i]);\n\t}\n}\n```\n## 总结\n```cpp\n(1)最大连续子序列和\n令dp[i]表示以A[i]作为末尾的连续序列的最大和\n(2)最长不下降子序列(LIS)\n令dp[i]表示以A[i]结尾的最长不下降子序列长度\n(3)最长公共子序列(LCS)\n令dp[i][j]表示字符串A的i号位和字符串B的j号位之前的LCS长度\n(4)最长回文子串\n令dp[i][j]表示S[i]至S[j]所表示的子串是否是回文子串\n(5)数塔DP\n令dp[i][j]表示从第i行第j个数字除法的到达最底层的所有路径上所能得到的最大和\n(6)DAG最长路\n令dp[i]表示从i号顶点出发能获得的最长路径长度\n(7)01背包\n令dp[i][v]表示前i件物品恰好装入容量为v的背包中能获得的最大价值。\n(8)完全背包\n令dp[i][v]表示前i件物品恰好装入容量为v的背包中能获得的最大价值。\n\n(1)-(4): \n当题目与序列或字符串(记为A)有关时,可以考虑把状态设计成下面两种形式,然后根据端点特点去考虑状态转移方程。\n\t① 令dp[i]表示以A[i]结尾(或开头)的XXX\n\t② 令dp[i][j]表示A[i]至A[j]区间的XXX\n其中XXX均为原问题的表述。\n\n(5)-(8):\n状态设计都包含某种\"方向\"的意思。\n分析题目中的状态需要几维来表示,然后对其中的每一维采取下面的某一个表述:\n① 恰好为i。\n② 前i。\n在每一维的含义设置完毕之后,dp数组的含义可以设置成\"令dp数组表示恰好为i(或前i)、恰好为j(或前j)······的XXX\",\n其中XXX为原问题的描述。接下来通过端点的特点去考虑状态转移方程。\n\n注:在大多数情况下,都可以把动态规划可解的问题看做一个DAG,图中的结点就是状态,边就是状态转移的方向,\n求解问题的顺序就是按照DAG的拓扑序列求解。\n```","tags":["动态规划"],"categories":["algorithm"]},{"title":"树算法","url":"/2020/03/13/Tree/","content":"早起的虫儿被鸟吃\n<!-- more -->\n\n\t* 二叉树的存储:链式存储和数组。\n\t* 二叉树的遍历:先序、中序、后序、层序。\n# 二叉树的遍历\n## 层序遍历\n```cpp\n//算法思想:\n//①将根节点root加入队列q。\n//②取出队首结点,访问之。\n//③如果该结点有左孩子,将左孩子入队。\n//④如果该结点有有孩子,将有孩子入队。\n//⑤返回②,直到队列为空。\n\nvoid LayerOrder(node* root) { //带层次\n\tqueue<node*> q;\n\troot->layer = 1;\n\tq.push(root);\n\twhile(!q.empty()){\n\t\tnode* p = q.front();\n\t\tq.pop();\n\t\tprintf(\"%d \", p->data);\n\t\tif(p->left != NULL) {\n\t\t\tp->left->layer = p->layer + 1;\n\t\t\tq.push(p->left);\n\t\t}\n\t\tif(p->right != NULL) {\n\t\t\tp->right->layer = p->layer + 1;\n\t\t\tq.push(p->right);\n\t\t}\n\t}\n}\n```\n\n# 建立二叉树\n## 已知先序中序\n```cpp\n//算法思想:\n//①先序数组的第零个结点为根结点。\n//②遍历中序数组找出根结点位置并记录。\n//③计算中序数组左子树个数\n//④递归建立左右子树。\n\n//其他情况算法类似,重点是确定根结点和数组区间范围。\n\nnode* create(int preL, int preR, int inL, int inR) {\n\tif(preL > preR) return NULL;\n\tnode* root = new node;\n\troot->data = pre[preL];\n\tint k = inL;\n\tfor(; k < inR; k++)\n\t\tif(in[k] == root->data) break;\n\tint numLeft = k - inL;\n\troot->left = create(preL + 1, preL + numLeft, inL, k - 1);\n\troot->right = create(preL + numLeft + 1, preR, k + 1, inR);\n\treturn root;\n}\n```\n\n# 二叉查找树(BST)\n\t* 左边小右边大\n\t* 对二叉查找树进行\"中序\"遍历,遍历结果是有序的\n\t* 如果给定序列是有序的或者排序后有序,可以中序递归建立BST\n## 查找操作\n```cpp\nvoid search(node* root, const int x) {\n\tif(root == NULL) {\n\t\tprintf(\"search failed!\\n\");\n\t\treturn;\n\t}\n\tif(x == root->data)\n\t\tprintf(\"%d\\n\", root->data);\n\telse if(x < root->data)\n\t\tsearch(root->left, x);\n\telse\n\t\tsearch(root->right, x);\n}\n```\n## 插入操作\n```cpp\nvoid insert(node* &root, const int x) {\n\tif(root == NULL) { //插入位置\n\t\troot = new node;\n\t\troot->data = x;\n\t\troot->left = root->right = NULL;\n\t\treturn;\n\t}\n\tif(x == root->data) return; //结点已存在 直接返回\n\telse if(x < root->data)\n\t\tinsert(root->left, x);\n\telse\n\t\tinsert(root->right, x);\n}\n```\n## 二叉查找树的建立\n```cpp\nnode* create(int data[]) {\n\tnode* root = NULL;\n\tfor(const auto val : data) insert(root, val);\n\treturn root;\n}\n```\n## 二叉查找树的删除\n```cpp\n//寻找以root为根结点的树中的最大权值结点\nnode* findMAX(node* root) {\n\twhile(root->right != NULL)\n\t\troot = root->right;\n\treturn root;\n}\n//寻找以root为根结点的树中的最小权值结点\nnode* findMin(node* root) {\n\twhile(root->left != NULL)\n\t\troot = root->left;\n\treturn root;\n}\n```\n```cpp\n//算法思想:\n//①如果当前结点root为空,说明不存在权值为给定权值 直接返回。\n//②如果当前结点root的权值恰为给定权值x,进入删除处理\n//a)如果当前结点root不存在左右孩子,说明叶子结点,直接删除。\n//b)如果当前结点root存在左孩子,那么在左子树中寻找结点前驱pre,然//后让pre的数据覆盖root,接着在左子树中删除结点pre。\n//c)如果当前结点root存在右孩子,那么在右子树中寻找结点后继next,//然后让next的数据覆盖root,接着在右子树中删除结点next。\n//③如果给定的权值x小于当前结点的权值,则在左子树中递归删除权值为x//的结点。\n//④如果给定的权值x大于当前结点的权值,则在右子树中递归删除权值为x//的结点。\n\nvoid deleteNode(node* root, int x) {\n\tif(root == NULL) return; //不存在权值为x的结点\n\tif(x == root->data) {//找到欲删除结点\n\t\t//delete(root);\n\t\tif(root->left == NULL && root->right == NULL)\n\t\t\troot = NULL;\n\t\telse if(root->left != NULL) {\n\t\t\tnode* pre = findMAX(root->left, x);\n\t\t\troot->data = pre->data;\n\t\t\tdeleteNode(root->left, pre->data);\n\t\t}\n\t\telse {\n\t\t\tnode* next = findMin(root->right);\n\t\t\troot->data = next->data;\n\t\t\tdeleteNode(root->right, next->data);\n\t\t}\n\t}\n\telse if(x < root->data)\n\t\tdeleteNode(root->left, x);\n\telse\n\t\tdeleteNode(root->right, x);\n}\n```\n# 平衡二叉树(AVL树)\n\t* AVL树是一棵二叉查找树\n\t* 任意结点的左右子树高度之差(平衡因子)的绝对值不超过1\n```cpp\nstruct node {\n\tint v, height;\n\tnode *left, *right;\n};\n\nint getHeight(node* root) {\n\tif(root == NULL) return 0;\n\telse return root->height;\n}\n\nint getBalanceFactor(node* root) {\n\treturn getHeight(root->left) - getHeight(root->right);\n}\n\nvoid updateHeight(node* root) {\n\troot->height = max(getHeight(root->left), getHeight(root->right)) + 1;\n}\n```\n## 查找操作\n```cpp\nvoid search(node* root, const int x) {\n\tif(root == NULL) {\n\t\tprintf(\"search failed!\\n\");\n\t\treturn;\n\t}\n\tif(x == root->data)\n\t\tprintf(\"%d\\n\", root->data);\n\telse if(x < root->data)\n\t\tsearch(root->left, x);\n\telse\n\t\tsearch(root->right, x);\n}\n```\n## 插入操作\n```cpp\n//旋转问题:\n//左旋(Left Rotation)算法思想:\n//①让B的左子树◆成为A的右子树\n//②让A成为B的左子树\n//③将根结点设定为结点B\n\nvoid L(node* &root) {\n\tnode* p = root->right;\n\troot->right = p->left;\n\tp->left = root;\n\tupdateHeight(root);\n\tupdateHeight(p);\n\troot = p;\n}\n\n//右旋(Right Rotation)算法思想:\n//①让A的右子树◆成为B的左子树\n//②让B成为A的右子树\n//③将根结点设定为结点A\n\nvoid R(node* &root) {\n\tnode* p = root->left;\n\troot->left = p->right;\n\tp->right = root;\n\tupdateHeight(root);\n\tupdateHeight(p);\n\troot = p;\n}\n```\n```cpp\n//只要把最靠近插入结点的失衡结点调整到正常,路径上的所有结点就都会正常(可证明)\n//分LL型、LR型、RR型、RL型\n//LR -> LL | RL -> RR\nvoid insert(node* &root, const int v) {\n\tif(root == NULL) { //插入位置\n\t\troot = new node;\n\t\troot->v = v;\n\t\troot->height = 1;\n\t\troot->left = root->right = NULL;\n\t\treturn;\n\t}\n\tif(v < root->v) {\n\t\tinsert(root->left, v);\n\t\tupdateHeight(root);\n\t\tif(getBalanceFactor(root) == 2) {\n\t\t\tif(getBalanceFactor(root->left) == 1) //LL型\n\t\t\t\tR(root);\n\t\t\telse if(getBalanceFactor(root->left) == -1) { //LR型\n\t\t\t\tL(root->left);\n\t\t\t\tR(root);\n\t\t\t}\n\t\t}\n\t}\n\telse {\n\t\tinsert(root->right, v);\n\t\tupdateHeight(root);\n\t\tif(getBalanceFactor(root) == -2) {\n\t\t\tif(getBalanceFactor(root->right) == -1) //RR型\n\t\t\t\tL(root);\n\t\t\telse if(getBalanceFactor(root->left) == 1) { //RL型\n\t\t\t\tR(root->right);\n\t\t\t\tL(root);\n\t\t\t}\n\t\t}\n\t}\n}\n```\n## AVL树的建立\n```cpp\nnode* create(int data[]) {\n\tnode* root = NULL;\n\tfor(const auto v : data) insert(root, v);\n\treturn root;\n}\n```\n\n# 并查集\n\t* 合并:合并两个集合\n\t* 查找:判断两个元素是否在一个集合\n\t* 并查集产生的每一个集合都是一棵树\n\t* int father[N]; //并查集数组\n## 初始化\n```cpp\nfor(int i = 1; i <= N; i++)\n\tfather[i] = i;\n```\n## 查找\n```cpp\n//iterator\nint findFather(int x){\n\twhile(x != father[x]) x = father[x];\n\treturn x;\n}\n//recursion\nint findFather(int x) {\n\tif(x == father[x]) return x;\n\telse return findFather(father[x]);\n}\n```\n## 合并\n```cpp\nvoid Union(int a, int b) {\n\tint faA = findFather(a);\n\tint faB = findFather(b);\n\tif(faA != faB)\n\t\tfather[faA] = faB;\n}\n```\n## 路径压缩\n```cpp\n//算法思想:\n//①按原来的写法获得v的根结点root\n//②重新从v开始走一遍寻找根结点的过程,把路径上经过的所有结点的父亲//全部改为根结点\nint findFather(int v) { //T(n) = O(1)\n\tif(v == father[v]) return v; // 找到根结点\n\telse {\n\t\t//让当前结点v的父亲全部改为根结点\n\t\tint root = findFather(father[v]);\n\t\tfather[v] = root;\n\t\treturn root;\n\t}\n}\n```\n# 堆与堆排序\n\t* 堆是一个完全二叉树\n\t* 大顶堆 和 小顶堆\n\t* 堆可以用来实现优先队列\n\t* int heap[maxn], n; // n为元素个数\n## 向下调整\n```cpp\n//向下调整 T(n) = O(logn)\n//其中low为欲调整的数组下表,high一般为堆的最后一个元素的数组下标\nvoid downAdjust(int low, int high) {\n\tint i = low, j = i * 2;\n\twhile(j <= high) {\n\t\tif(j + 1 <= high && heap[j+1] > heap[j])\n\t\t\tj = j + 1;\n\t\tif(heap[j] > heap[i]) {\n\t\t\tswap(heap[j], heap[i]);\n\t\t\ti = j;\n\t\t\tj = i * 2;\n\t\t}\n\t\telse break;\n\t}\n}\n```\n## 建堆\n```cpp\n//从最后一名同学位置开始,从下往上 从右往左\n//左轮·D·普拉西多\n// T(n) = O(n)\nvoid createHeap() {\n\tfor (int i = n / 2; i >= 1; i--) {\n\t\tdownAdjust(i, n);\n\t}\n}\n```\n## 删除堆顶\n```cpp\n// T(n) = O(n)\nvoid deleteTop(){\n\theap[1] = heap[n--]; //用最后一个元素覆盖堆顶元素,并让元素个数减1\n\tdownAdjust(1,n);\n}\n```\n## 向上调整\n```cpp\n// T(n) = O(logn)\nvoid upAdjust(int low, int high) {\n\tint i = high, j = i / 2;\n\twhile(j >= low){\n\t\tif(heap[i] > heap[j]){\n\t\t\tswap(heap[i], heap[j]);\n\t\t\ti = j;\n\t\t\tj = i / 2;\n\t\t}\n\t\telse break;\n\t}\n}\n```\n\n## 堆的插入\n```cpp\nvoid insert(node* root, int x) {\n\theap[++n] = x; // 加入到堆最后一个元素后面,并让元素个数加1\n\tupAdjust(1, n);\n}\n```\n## 堆排序(heap sort)\n```cpp\nvoid heapSort() {\n\tcreateHeap();\n\tfor (int i = n; i > 1; i--){\n\t\tswap(heap[1], heap[i]);\n\t\tdownAdjust(1,i - 1);\n\t}\n}\n```\n# 哈夫曼树\n```cpp\n//算法思想:\n//反复选择两个最小的元素,合并,直到只剩下一个元素\n#include <cstdio>\n#include <vector>\n#include <queue>\nusing namespace std;\n\npriority_queue<long long, vector<long long>, greater<long long> > q;\n\nint main() {\n\tint n;\n\tlong long temp, x, y, ans = 0;\n\tscanf(\"%d\", &n);\n\tfor (int i = 0; i < n; i++){\n\t\tscanf(\"%lld\", &temp);\n\t\tq.push(temp);\n\t}\n\twhile(q.size() > 1) {\n\t\tx = q.top();\n\t\tq.pop();\n\t\ty = q.top();\n\t\tq.pop();\n\t\tq.push(x + y);\n\t\tans += x + y;\n\t}\n\tprintf(\"%lld\\n\", ans);\n\treturn 0;\n}\n```","tags":["树"],"categories":["algorithm"]},{"title":"Be a magical developer","url":"/2019/11/19/ambition/","content":"《一百种自杀方法》最后一页写着活下去。\n<!-- more -->\n\n* [费曼学习法](https://www.choupangxia.com/2019/09/26/%E4%B8%96%E7%95%8C%E4%B8%8A%E6%9C%80%E5%A5%BD%E7%9A%84%E5%AD%A6%E4%B9%A0%E6%B3%95%EF%BC%9A%E8%B4%B9%E6%9B%BC%E5%AD%A6%E4%B9%A0%E6%B3%95/) \n* 康奈尔笔记 \n* 数据结构与算法(不C9 无算法 一直都坚信它是程序的灵魂 加油)([PKU](https://www.bilibili.com/video/av7134874)+[THU](https://www.bilibili.com/video/av49361421)+[MIT](https://www.bilibili.com/video/av48922404?p=21)+算法导论英文第三版) \n* 计算机组成原理(秃头程序员的进阶感觉可还行)([HIT](https://www.bilibili.com/video/av15123338)) \n* 操作系统(听说过bug10吧) ([PKU](https://www.bilibili.com/video/av6538245)) \n* 计算机网络(学好卖网线)(自顶而下英文原版)\n* 编译原理([HIT](https://www.bilibili.com/video/av17649289))\n* 嵌入式系统([学堂在线](https://next.xuetangx.com/))\n* 信息论与编码([THU](https://www.bilibili.com/video/av28661250)+[西电](https://www.bilibili.com/video/av26735580))\n* [ML](https://www.bilibili.com/video/av9912938) \n* 前端 \n* 数据库 \n* 健身 \n* 个人札记 \n 1. 学点经济学\n\t2. 为竞赛而生 \n\t3. 多看英文原版书 \n\t4. 多线程+设计模式 \n\t5. 多看项目源码 \n\t6. 脚踏实地的理想主义者,既要有高远的理想也要有脚踏实地的精神 \n\t7. 他们只是起点比我们高 -- 2018-10-3 打卡南京大学鼓楼校区 \n\t8. 见见有趣的人,读读有趣的书,讲讲有趣的故事。 \n\t9. 有时候我们会觉得自己后知后觉,那是因为学习的太少,了解的太少,很多问题前人已经总结好了现成的方法和方案,我们却不知道,还在自己探索,当然行动缓慢,后知后觉了。只有站在巨人的肩膀上才能看得更远。 \n\t10. 2019-10-26打卡东南大学四牌楼校区图书馆 \n\t11. [C语言笔记](https://app.yinxiang.com/shard/s40/nl/24222849/82d590e6-3c3b-4af1-87c2-8964f41501e9/) \n","tags":["成长"],"categories":["techs"]},{"title":"大学阅读记录","url":"/2019/11/07/book/","content":"\n![一起努力(劝退)](./20191120135008.jpg)\n\n<!-- more -->\n\n## 算法类\n花费我时间最长的东西 不放在第一位都觉得对不起自己. \n* Introduction to Algorithms \n* 大话数据结构 \n* 算法笔记 \n* 算法笔记上机训练实战指南 \n* 数学之美\n* 算法之美 \n* 初等数论 \n* 啊哈算法 \n* 算法竞赛入门经典(第2版) \n* 算法竞赛入门经典训练指南 \n* 大学生程序设计课程与竞赛训练教材-算法设计编程实验 \n## 语言类杂书\n同类书籍看了一堆又一堆 就觉得这几本有点意思(索然无味). \n* C Traps and Pitfalls \n* Expert C Programming \n* C Pointer \n* C++ STL \n* C++ 11 \n* Thinking in C++ \n* Effective C++ \n* More Effective C++ \n* Inside The C++ Object Model \n* The C++ Standard Library \n* Computer Network A Top-Down Approach \n## Python小爬虫\n骚不动系列,只看了一本两个晚上93分过期末。。。 \n* Python编程: 从入门到实践 \n## 硬件之路\n* Computer System A Programmer's Perspective \n* Computer Organization And Design the hardware/software interface \n* Computer Architecture A Quantitative Approach \n* ARM Cortex-M0 and Cortex-M0+ Processors \n* See MIPS Run","tags":["阅读"],"categories":["books"]},{"title":"图论算法","url":"/2019/11/01/Graph/","content":"\n早起的鸟儿有虫吃\n\n<!-- more -->\n# 图的遍历\n * 有两种存储方式:邻接矩阵和邻接表\n * 在一些顶点数目比较大(一般顶点个数在1000以上)的情况下,使用邻接表而 \n 不是邻接矩阵来存储图。如果是稀疏图,用邻接表,如果是稠密图,用邻接矩阵。\n\n## 深度优先搜索dfs遍历图\n```cpp\n//按深度优先的方式访问所有未被访问的结点,在结点被访问过后标记为已访问\ndfs(u) {\n vis[u] = true;\n for(从u出发到能到达的所有顶点v)\n if(vis[v] == false)\n dfs(v);\n}\ndfsTravel(G) {\n for(G的所有顶点u)\n if(vis[u] == false)\n dfs(u); //访问u所在连通块\n}\n```\n\n```cpp \n//邻接矩阵版 DFS\n\nint n, G[maxV][maxV];//n顶点数 maxV最大顶点数\nbool vis[maxV] = {false};//记录顶点是否被访问过\n\nvoid dfs(int u, int depth) {\n vis[u] = true;\n for(int v = 0; v < n; v++)//遍历u可以到达的顶点v\n if(G[u][v] != INF && vis[v] == false)\n dfs(v, depth + 1);//访问v 深度+1\n}\n\nvoid travelDFS() {\n for(int u = 0; u < n; u++)\n if(vis[u] == false)\n dfs(u, 1);//访问u和u所在的连通块\n}```\n```cpp\n//邻接表版 DFS\n\nvector<int> arr[maxn];\n\nvoid dfs(int u, int depth) {\n vis[u] = true;\n for(int i = 0; i < arr[u].size(); i++){\n\tint v = arr[u][i];\n\tif(vis[i] == false)\n \tdfs(v, depth + 1);\n }\n}\n\nvoid dfsTrave() {\n for(int u = 0; u < n; u++) {\n if(vis[u] == false)\n dfs(u, 1);\n }\n}\n```\n## 广度优先搜索bfs遍历图\n```cpp\n//建立一个队列,把初始定点加入队列,然后每次都取出队首元素进行访问,并把该定点 \n//除法可以到达的未曾加入过队列(而不是未访问)的顶点全部加入队列,直到队列为空。\nbfs(u) {\n queue q;\n 将u入队\n inq[u] = true;\n while(q非空) {\n for(从u出发到可到达的所有顶点v) {\n if(inq[v] == false)\n 将v入队\n inq[v] = true;\n }\n }\n}\n\nbfsTrave(G) {\n for(G的所有顶点u) {\n if(inq[u] == false)\n bfs(u);\n }\n}\n```\n\n```cpp\n//邻接矩阵版 BFS\nint n, G[maxV][maxV];//n顶点数 maxV最大顶点数\nbool inq[maxV] = {false};//记录顶点的是否入队\n\nvoid bfs(int u) {//u顶点编号\n\tqueue<int> q;\n\tq.push(u);//入队\n\tinq[u] = true;\n\twhile(!q.empty()) {\n\t\tint u = q.front();\n\t\tq.pop();\n\t\tfor(int v = 0; v < n; v++)//遍历u所在连通块(未入队)并入队\n\t\t\tif(G[u][v] != INF && inq[v] == false){\n\t\t\t\tq.push(v);、\n\t\t\t\tinq[v] = true;\n\t\t\t}\n\t}\n}\n\n/*邻接表:\nfor(int i = 0; i < arr[u].size(); i++) {\n int v= arr[u][i];\n if(inq[v] == false) {\n q.push(v);\n inq[v] = true;\n }\n}\n*/\n\nvoid travelBFS() {\n\tfor(int v = 0; v < n; v++)\n\t\tif (inq[v] == false)\n\t\t\tbfs(v);//遍历v及其所在连通块\n}```\n\n```cpp\n//带层数的 邻接表\nstruct node {\n int v;//顶点编号\n int layer;//顶点层号\n};\nnext.layer = curNode.layer + 1;\n//邻接表中的类型是node,而不是int\nvector<node> Adj[N];\n```\n# 最短路径\n\t* 单源最短路径:计算源点到其他各顶点的最短路径的长度\n\t* 全局最短路径:图中任意两点的最短路径\n\t* Dijkstra、Bellman-Ford、SPFA求单源最短路径\n\t* Floyd-Warshall可以求全局最短路径,但是效率比较低\n\t* SPFA算法是Bellman-Ford算法的队列优化\n\t* Dijkstra算法不能求带负权边的最短路径,而SPFA算法、Bellman-Ford算法、Floyd-Warshall可以求带负权边的最短路径\n\t* Bellman-Ford算法的核心代码只有4行,Floyd-Warshall算法的核心代码只有5行\n\t* 深度优先遍历可以求一个点到另一个点的最短路径的长度\n## 单源最短路径\n\t思想来源:图的广度优先遍历bfs\n### Dijkstra 算法\n```cpp\n//Dijkstra 算法:\n//对图G(V,E)设置集合S,存放已被访问的顶点,\n//然后每次从集合V-S中选择与起点s的最短距离最小的一个顶点(记为u),访问并加入集合S,\n//之后,令顶点u为中介点,优化起点s与所有从u能到达的顶点v的最短距离,\n//这样的操作执行n次,直到集合S已包含所有顶点。\n\nint G[maxSize][maxSize], n;\nint d[maxSize];\nint pre[maxSize];\nbool vis[maxSize] = {false};\n//邻接矩阵\nvoid Dijkstra(int s) { //T(n) = O(V^2)\n\tfill(d, d + maxSize, INF);\n\td[s] = 0;\n\tfor (int i = 0; i < n; i++){\n\t\tint u = -1, min = INF;//u 保存最短路径顶点,min保存最短距离\n\t\tfor (int j = 0; j < n; j++)\n\t\t\tif(vis[j] == false && d[j] < min) {\n\t\t\t\tu = j;\n\t\t\t\tmin = d[j];\n\t\t\t}\n\t\t//找不到小于INF的d[u],说明剩下的顶点与起点s不连通\n\t\tif(u == -1) return;\n\t\tvis[u] = true;\n\t\tfor (int v = 0; v < n; v++) {\n\t\t\t//以u为中介点使可以使d[v]更优\n\t\t\tif(G[u][v] != INF && vis[v] == false && d[u] + G[u][v] < d[v]) {\n\t\t\t\td[v] = d[u] + G[u][v];\n\t\t\t\tpre[v] = u; //记录v的前驱结点u\n\t\t\t}\n\t\t}\n\t}\n}\n```\n```cpp\n//邻接表\nstruct Node {\n\tint v, dis; //v为边的目标顶点,dis为边权\n};\n\nvector<Node> Adj[MAXV];\n\nfor(int j = 0; j < Adj[u].size(); j++) {\n\tint v = Adj[u][j].v; //通过邻接表直接获得u能到达的顶点v\n\tif(vis[v] == false && d[u] + Adj[u][v].dis < d[v]) {\n\t\td[v] = d[u] + Adj[u][v].dis;\n\t} \n}\n\n\n```\n\n```cpp\n//无向图解决方案: 把无向边当成指向相反的有向边\n\nvoid DFS(int s, int v) {//s起点编号,v是当前访问的顶点编号(从终点开始递归)\n\tif(v == s) { //如果当前已经打扫起点s,则输出起点并返回\n\t\tprintf(\"%d\\n\", s);\n\t\treturn;\n\t}\n\tDFS(s, pre[v]); //递归访问v的前驱结点pre[v]\n\tprintf(\"%d\\n\", v);//从最深处return回来之后,输出每一层的顶点号\n}\n```\n\t附加考法:第一标尺是距离,第二标尺常见有三种\n\t①新增边权:给每条边再增加一个边权(比如花费),然后要求在最短路径有多条时要求路径上的花费之和最小(如果边权是其它含义,也可以是最大)。\n```cpp\n//c[] 从起点s到达顶点u的最少花费c[u]\n//cost[u][v]表示u->v的花费\n//初始化时只有c[s] = 0 其余均为INF\nfor(int v = 0; v < n; v++) {\n\tif(G[u][v] != INF && vis[v] == false) {\n\t\tif(d[u] + G[u][v] < d[v]) {\n\t\t\td[v] = d[u] + G[u][v];\n\t\t\tc[v] = c[u] +cost[u][v];\n\t\t}\n\t\telse if(d[u] + G[u][v] == d[v] && c[u] +cost[u][v] < c[v]){\n\t\t\tc[v] = c[u] +cost[u][v];//最短距离相同时,看c[v]能否更优\t\n\t\t}\n\t}\n}\n```\n\t②新增点权:给每个点增加一个点权(例如每个城市能收集到的物资),然后在最短路径\n\t有多条时要求路径上的点权之和最大(如果点权是其它含义也可以是最小)。\n```cpp\n//w[]从起点s到顶点u能收集的最大物资w[u]\n//weight[u]表示城市u中的物资数目\n//初始化时只有w[s]为weight[s] 其余均为0\nfor(int v = 0; v < n; v++) {\n\tif(G[u][v] != INF && vis[v] == false) {\n\t\tif(d[u] + G[u][v] < d[v]) {\n\t\t\td[v] = d[u] + G[u][v];\n\t\t\tw[v] = w[u] + weight[v];\n\t\t}\n\t\telse if(d[u] + G[u][v] == d[v] && w[u] + weight[v] > w[v]){\n\t\t\tw[v] = w[u] + weight[v];\n\t\t}\n\t}\n}\n```\n\t③直接问有多少条最短路径\n```cpp\n//num[]从起点s到达顶点u的最短路径条数为num[u]\n//初始化只有num[s] = 1 其余均为0\nfor(int v = 0; v < n; v++) {\n\tif(G[u][v] != INF && vis[v] == false) {\n\t\tif(d[u] + G[u][v] < d[v]) {\n\t\t\td[v] = d[u] + G[u][v];\n\t\t\tnum[v] = num[u];\n\t\t}\n\t\telse if(d[u] + G[u][v] == d[v]){\n\t\t\tnum[v] += num[u];//最短距离相同时累加num\n\t\t}\n\t}\n}\n```\n### Bellman-Ford 算法\n```cpp\n// 零环、正环、负环\n//零环和正环不会影响最短路径求解(因为不能使最短路径更短)\n//负环 从源点可以到达,会影响最短路径求解(无法从源点除法到达 不会影响)\n//算法思想:\n//需要对图中的边进行V - 1轮操作,每轮都遍历图中的所有边:对每条边u->v, \n//如果以u为中介点可以使d[v]更小,即d[u] + length[u->v] < d[v]成立时,\n//就用d[u] + length[u->v] 更新d[v]。\n//T(n) = O(VE)\n//第k轮得到从0号顶点\"最多通过k条边到达其余各顶点的最短路径长度\"\nstruct Node {\n\tint v, dis; //v为邻接边的目标顶点, dis为邻接边的边权\n};\nvector<Node> Adj[MAXV]; //邻接表\nint n, d[MAXV]; //n顶点数,d[]用来存放从源点s到达各个顶点的最短距离\n\nbool Bellman(int s) {\n\tfill(s, d + MAXV, INF);\n\td[s] = 0;\n\tfor(int i = 0; i < n - 1; i++) {\n\t\tfor(u = 0; u < n; u++){//遍历所有边\n\t\t\tfor(int j = 0; j < Adj[u].size(); j++) {\n\t\t\t\tint v = Adj[u][j].v;\n\t\t\t\tint dis = Adj[u][j].dis;\n\t\t\t\tif(d[u] + dis < d[v]) {\n\t\t\t\t\td[v] = d[u] + dis; //松弛操作\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\t//判断负环代码\n\tfor(int u = 0; u < n; u++) {\n\t\tfor(int j = 0; j < Adj[u].size(); j++) {\n\t\t\tint v = Adj[u][j].v;\n\t\t\tint dis = Adj[u][j].dis;\n\t\t\tif(d[u] + dis < d[v]) {\n\t\t\t\treturn false; //说明图中有从源点可达的负环\n\t\t\t}\n\t\t}\n\t}\n\treturn true; //数组d的所有值都已经达到最优\n}\n\n```\n### SPFA 算法\n```cpp\n//Shortest Path Faster Algorithm\n//T(n) = O(kE) , k <= 2\n//理解SPFA的关键是理解它是如何从Bellman-Ford算法优化得来的\nqueue<int> q;\n源点s入队\nwhile(队列非空) {\n\t取出队首元素\n\tfor(u的所有邻接边u->v) {\n\t\tif(d[u] + dis < d[v]) {\n\t\t\td[v] = d[u] + dis;\n\t\t\tif(v当前不在队列) {\n\t\t\t\tv入队;\n\t\t\t\tif(v入队次数大于n - 1) {\n\t\t\t\t\t说明有可达负环,return;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n```\n## 全源最短路径\n### Floyd 算法\n```cpp\n//Floyd算法\n枚举顶点 k ∈ [1,n]\n\t以顶点 k 作为中介点,枚举所有顶点对i和j (i,j∈ [1,n])\n\t\t如果dis[i][k] + dis[k][j] < dis[i][j]成立\n\t\t\t赋值dis[i][j] = dis[i][k] + dis[k][j]\n```\n```cpp\nint n, m; //n顶点数, m为边数\nint dis[MAXV][MAXV]; //dis[i][j]表示顶点i和j的最短距离\n\nvoid Floyd(){\n\tfor(int k = 0; k < n; k++) {\n\t\tfor(int i = 0; i < n; i++) {\n\t\t\tfor(int j = 0; j < n; j++) {\n\t\t\t\tif(dis[i][k] != INF && dis[k][j] != INF && dis[i][k] + dis[k][j] < dis[i][j])\n\t\t\t\t\tdis[i][j] = dis[i][k] + dis[k][j];\n\t\t\t}\n\t\t}\n\t}\n}\n```\n\n# 最小生成树\n * 如果是稠密图(边多) prim算法\n * 如果是稀疏图(边少) kruskal算法\n## prim 算法\n```cpp\n//prim算法:\n//对图G(V,E)设置集合S,存放已被访问的顶点,\n//然后每次从集合V-S中选择与集合S的最短距离最小的一个顶点(记为u)访问并加入集合S。\n//之后,令顶点u为中介点,优化所有从u能到达的顶点v与集合S之间的最短距离。\n//这样执行操作n次(n为顶点个数),直到集合S已包含所有顶点。\n//prim算法和Dijkstra算法只有优化d[v]部分不同\n//prim算法和Dijkstra算法思路完全相同,只不过是数组d[]含义不同\nPrim(G, d[]){\n\t初始化\n\tfor(循环n次){\n\t\tu = 使d[u]最小的还未被访问的顶点的标号\n\t\tfor(从u除法能到达的所有顶点){\n\t\t\tif(v未被访问&& 以u为中介点使得v与集合S的最短距离d[v]更优){\n\t\t\t\t将G[u][v]赋值给v与集合S的最短距离d[v]\n\t\t\t}\n\t\t}\n\t}\n}\n```\n```cpp\n//邻接矩阵\nint n, G[MAXV][MAXV]; //n为顶点数\nint d[MAXV]; //顶点与集合S的最短距离\nbool vis[MAXV] = {false}; //标记数组,vis[i] == true表示已访问。初值均为false\n\nint prim(){//默认0号为初始点\n\tfill(d, d + MAXV, INF);\n\td[0] = 0;\n\tint ans = 0; //存放最小生成树的边权之和\n\tfor(int i = 0; i < n; i++) {\n\t\tint u = -1, min = INF; //u使d[u]最小,min存放该最小的d[u]\n\t\tfor(int j = 0; j < n; j++) {\n\t\t\tif(vis[j] == false && d[j] < min) {\n\t\t\t\tu = j;\n\t\t\t\tmin = d[j];\n\t\t\t}\n\t\t}\n\t\t//找不到小于INF的d[u],则剩下的顶点和集合S不连通\n\t\tif(u == -1) return -1;\n\t\tvis[u] = true;\n\t\tans += d[u];\n\t\tfor(int v = 0; v < n; v++) {\n\t\t\tif(G[u][v] != INF && vis[v] ==false && G[u][v] < d[v]){\n\t\t\t\td[v] = G[u][v];\n\t\t\t}\n\t\t}\n\t}\n\treturn ans;\n}\n```\n```cpp\n//邻接表\nstruct Node {\n\tint v, dis; //v为边的目标顶点,dis为边权\n};\n\nvector<Node> Adj[MAXV]\n\nfor(int j = 0; j < Adj[u].size(); j++) {\n\tint v = Adj[u][j].v; //通过邻接表直接获得u能到达的顶点v\n\tif(vis[v] == false && Adj[u][j].dis < d[v]) {\n\t\td[v] = Adj[u][v].dis;\n\t} \n}\n```\n\n## kruskal 算法\n\n```cpp\n//kruskal算法采用\"边贪心\"策略,步骤:\n//①对所有边按边权从小到大进行排序。\n//②按边权从小到大测试所有边,如果当前测试所连接的两个顶点不在同一//个连通块中,\n//则把这条测试边加入当前最小生成树中;否则,将边舍弃\n//③执行步骤②,直到最小生成树中的边数等于总顶点数-1或是测试完所有边//时结束。\n//而当结束时如果最小生成树的边数小于总顶点数-1,说明该图不连通\n\nstruct edge {\n\tint u, v;//边的两个端点编号\n\tint cost;//边权\n}E[MAXE];\n\nint kruskal(){\n\t令最小生成树的边权之和为ans,最小生成树的当前边数为Num_Edge;\n\t将所有边按边权从小到大排序;\n\tfor(从小到大枚举所有边){\n\t\tif(当前测试边的两个端点在不同的连通块中) {\n\t\t\t将该测试边加入最小生成树中;\n\t\t\tans += 测试边边权;\n\t\t\t最小生成树的当前边数Num_Edge 加 1;\n\t\t\t当边数Num_Edge等于顶点数减1时结束循环;\n\t\t}\n\t}\n\treturn ans;\n}\n```\n```cpp\nint father[N];//并查集数组\nint findFather(int x){\n\twhile(x != father[x]) x = father[x];\n\treturn x;\n}\n\nbool cmp(edge a, edge b) {\n\treturn a.cost < b.cost;\n}\n\nint kruskal(int n, int m){ //n顶点数 m边数\n\tint ans = 0,Num_Edge = 0;\n\tfor(int i = 1; i <= n; i++) {\n\t\tfather[i] = i;\n\t}\n\tsort(E, E + m, cmp); //所有边按边权从小到大排序\n\tfor(int i = 0; i < m; i++) { //枚举所有边\n\t\tint faU = findFather(E[i].u);\n\t\tint faV = findFather(E[i].v);\n\t\tif(faU != faV) { //如果不在一个集合中\n\t\t\tfather[faU] = faV;\n\t\t\tans += E[i].cost;\n\t\t\tNum_Edge++;\n\t\t\tif(Num_Edge == n - 1) break;//边数等于顶点数减1时结束算法\n\t\t}\n\t}\n\tif(Num_Edge != n - 1) return -1; //无法连通时返回-1\n\telse return ans;\n}\n```\n# 拓扑排序\n```cpp\n//算法思想:\n//①定义一个队列Q,并把所有入度为0的结点加入队列\n//②当队列不空时,取队首结点,输出然后删去所有从它出发的边,并令这些边到达的顶点的入度减1,\n//如果某个顶点的入度减为0,则将其加入队列\n//③反复进行②操作,直到队列为空。\n//如果队列为空时入过队的结点数目恰好为N,说明拓扑排序成功,图G为有向无环图;否则,拓扑排序失败,图G中有环\n\n//拓扑排序可以判断一个给定的图是否是\"有向无环图\"\n//如果有多个入度为0的顶点,选择编号最小的顶点,那么把queue改成priority_queue,\n//并保持堆顶元素是优先队列中的最小元素即可。(set也行)\n\nvector<int> G[MAXV]; //邻接表\nint n, inDegree[MAXV];\nbool topologicalSort() {\n\tqueue<int> q;\n\tint num = 0; //记录加入拓扑排序的顶点数\n\tfor(int i = 0; i < n; i++) {\n\t\tif(inDegree[i] == 0) {\n\t\t\tq.push(i);\n\t\t}\n\t}\n\twhile(!q.empty()) {\n\t\tint u = q.front();\n\t\tq.pop();\n\t\t//printf(\"%d \", u); //此处可输出顶点u,作为拓扑序列中的顶点\n\t\tfor(int i = 0; i < G[u].size(); i++) {\n\t\t\tint v = G[u][i]; //u的后继v\n\t\t\tinDegree[v]--;\n\t\t\tif(inDegree[v] == 0) {\n\t\t\t\tq.push(v);\n\t\t\t}\n\t\t}\n\t\tG[u].clear(); //清空顶点u的所有出边(可写可不写)\n\t\tnum++; //加入拓扑排序的顶点数加1\n\t}\n\tif(num == n) return true;\n\telse return false;\n}\n```\n# 关键路径\n## AOV网 和 AOE网\n```cpp\n//AOV: Activity On Vertex\n//AOE: Activity On Edge\n//AOV, AOE不应当有环\n/*\n *AOV网只有一个源点(入度为0)和一个汇点(出度为0),若有多个源点和多*个汇点,也可转化为一个源点和一个汇点的情况\n *即添加一个\"超级源点\"和一个\"超级汇点\",\n *方法:从超级源点出发,连接所有入度为0的点;从所有出度为0的点出**发,连接超级汇点;添加的有向边的边权均为0\n */\n/*\n *如果给定AOV网中各顶点所需要的时间,那么可以将AOV网转化成AOE网\n *方法:将AOV网中每个顶点都拆成两个顶点。分别表示活动的起点和终点,\n *而两个顶点之间用有向边连接,该有向边表示原顶点的活动,边权给定;\n *原AOV网中的边全部视为空活动,边权为0\n */\n/*\n *AOE网需要着重解决两个问题:\n *a. 工程起始到终止至少需要多少时间;\n *b. 那条(些)路径上的活动是影响整个工程进度的关键\n */\n//AOE网中的\"最长路径\"称为\"关键路径\",关键路径上的活动称为关键活动\n```\n## 最长路径算法\n```cpp\n//算法思想:\n//把所有边的边权乘以-1,然后使用 Bellman-Ford算法 或 SPFA算法 求最长路径长度,将所得结果取反。\n//注意:此处不能用Dijkstra算法,原因是Dijkstra算法不能处理负权边的情况。\n\n//最长路径问题,即 Longest Path Problem,寻求的是图中的\"最长简单路径\"\n//如果图中有正环,则最长路径不存在。但最长简单路径存在,但你用Bellman-Ford算法求不出来(你说气不气)\n//但我后面有简单方法鸭(*^▽^*) (〃'▽'〃)哪呢\n```\n## 关键路径算法\n```cpp\nstruct node {\n\tint v, w;\n};\nvector<node> G[MAXV]; //邻接表\n\n//先求点,再夹边\n//ve[] 事件最早发生时间 //取最大\nstack<int> topOrder; //留着求vl[], 不然我吃饱了撑的没事干急的啊\nbool topologicalSort() {\n\tqueue<int> q;\n\tfor(int i= 0; i < n; i++) {\n\t\tif(inDegree[i] == 0) {\n\t\t\tq.push(i);\n\t\t}\n\t}\n\twhile(!q.empty()) {\n\t\tint u = q.front();\n\t\tq.pop();\n\t\ttopOrder.push(u);\n\t\tfor(int i = 0; i < G[u].size(); i++) {\n\t\t\tint v = G[u][i].v; //u的i号后继结点编号为v\n\t\t\tinDegree[v]--;\n\t\t\tif(inDegree[v] == 0) {\n\t\t\t\tq.push(v);\n\t\t\t}\n\t\t\tif(ve[u] + G[u][v].w > ve[v]) {\n\t\t\t\t//用ve[u]来更新u的所有后继结点v\n\t\t\t\tve[v] = ve[u] + G[u][v].w;\n\t\t\t}\n\t\t}\n\t}\n\tif(topOrder.size() == n) return true;\n\telse return false;\n}\n\n//vl[] 事件最晚发生时间 //取最小\nfill(vl,vl + n, ve[n - 1]); //vl数组初始化,初始值为终点的ve值\nwhile(!topOrder.empty()) {\n\tint u = topOrder.top();\n\ttopOrder.pop();\n\tfor(int i = 0; i < G[u].size(); i++) {\n\t\tint v = G[u][i].v;\n\t\tif(vl[v] - G[u][i].w < vl[u]) {\n\t\t\t//用u的所有后继结点v的vl值来更新vl[u]\n\t\t\tvl[u] = vl[v] - G[u][i].w;\n\t\t}\n\t}\n}\n```\n```cpp\n//下面代码中未保存活动的最早开始时间e和最迟开始时间l\n//原因是e 和 l只用来判断当前活动是否为关键路径\n//如果需要保存则在结构体node中添加域e 和 l\nint criticalPath() {\n\tmenset(ve, 0, sizeof(ve)); //ve[]初始化\n\tif(topologicalSort() == false) {\n\t\treturn -1; //不是有向无环图 返回-1\n\t}\n\n\t/*\n\t如果事先不知道汇点编号,可以取ve[]的最大值来得到关键路径长度\n\tint maxLength = 0;\n\tfor(int i = 0; i < n; i++) {\n\t\tif(maxLength < ve[i]) {\n\t\t\tmaxLength = ve[i];\n\t\t}\n\t}\n\tfill(vl, vl + n, maxLength);\n\t别忘了替换函数返回值\n\t*/\n\n\tfill(vl,vl + n, ve[n - 1]);\n\twhile(!topOrder.empty()) {\n\t\tint u = topOrder.top();\n\t\ttopOrder.pop();\n\t\tfor(int i = 0; i < G[u].size(); i++) {\n\t\t\tint v = G[u][i].v;\n\t\t\tif(vl[v] - G[u][i].w < vl[u]) {\n\t\t\t\t//用u的所有后继结点v的vl值来更新vl[u]\n\t\t\t\tvl[u] = vl[v] - G[u][i].w;\n\t\t\t}\n\t\t}\n\t}\n\t//遍历邻接表的所有边,计算活动的最早开始时间e和最迟开始时间l\n\tfor(int u = 0; u < n; u++) {\n\t\tfor(int i = 0; i < G[u][i].size(); i++) {\n\t\t\tint v = G[u][i].v, w = G[u][i].w;\n\t\t\t//活动的最早开始时间e和最迟开始时间l\n\t\t\tint e = ve[u], l = vl[v] - w;\n\t\t\t//如果e == l,说明活动u->v是关键活动\n\t\t\tif(e == l) {\n\t\t\t\tprintf(\"%d->%d\\n\", u, v); //输出关键活动\n\t\t\t}\n\t\t}\n\t}\n\treturn ve[n - 1]; //返回关键路径长度\n}\n```","tags":["图论"],"categories":["algorithm"]},{"title":"debug","url":"/2019/10/24/warings/","content":"\ndebug 之于 coder\n\n<!-- more -->\n一定一定要会debug!debug!debug!血的教训,从来都不debug的我,上课看老师debuging都不以为意,大佬们给的建议也是左耳进右耳出。花了一下午和一个晚上的时间终于把b站图标给改出来了。以后工作了面对着几千行几万行代码你能一行一行查吗? \n还有能用框架就用框架!那效率真的不是一般的高!别傻乎乎地一行一行地去敲。\n# 发此博客引以为戒!!!","tags":["警示"],"categories":["techs"]}]