courses = umap[cur]; //获取这门课指向的课程,也就是这么课的后续课
49 | if (courses.size()) { // 有后续课
50 | for (int i = 0; i < courses.size(); i++) {
51 | inDegree[courses[i]]--; // 它的后续课的入度-1
52 | if (inDegree[courses[i]] == 0) que.push(courses[i]); // 如果入度为0,加入队列
53 | }
54 | }
55 | }
56 | if (count == numCourses) return true;
57 | return false;
58 |
59 | }
60 | };
61 | ```
62 |
63 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/problems/0210.课程表II.md:
--------------------------------------------------------------------------------
1 |
2 | ```CPP
3 | class Solution {
4 | public:
5 | vector findOrder(int numCourses, vector>& prerequisites) {
6 | vector inDegree(numCourses, 0);
7 | vector result;
8 | unordered_map> umap;
9 | for (int i = 0; i < prerequisites.size(); i++) {
10 |
11 | // prerequisites[i][0] 是 课程入度,prerequisites[i][1] 是课程出度
12 | // 即: 上课prerequisites[i][0] 之前,必须先上课prerequisites[i][1]
13 | // prerequisites[i][1] -> prerequisites[i][0]
14 | inDegree[prerequisites[i][0]]++;//当前课程入度值+1
15 | umap[prerequisites[i][1]].push_back(prerequisites[i][0]); // 添加 prerequisites[i][1] 指向的课程
16 | }
17 | queue que;
18 | for (int i = 0; i < numCourses; i++) {
19 | if (inDegree[i] == 0) que.push(i); // 所有入度为0,即为 开头课程 加入队列
20 | }
21 | int count = 0;
22 | while (que.size()) {
23 | int cur = que.front(); //当前选的课
24 | que.pop();
25 | count++; // 选课数+1
26 | result.push_back(cur);
27 | vector courses = umap[cur]; //获取这门课指向的课程,也就是这么课的后续课
28 | if (courses.size()) { // 有后续课
29 | for (int i = 0; i < courses.size(); i++) {
30 | inDegree[courses[i]]--; // 它的后续课的入度-1
31 | if (inDegree[courses[i]] == 0) que.push(courses[i]); // 如果入度为0,加入队列
32 | }
33 | }
34 | }
35 | if (count == numCourses) return result;
36 | else return vector();
37 | }
38 | };
39 | ```
40 |
--------------------------------------------------------------------------------
/problems/1334.阈值距离内邻居最少的城市.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!
6 |
7 | floyd
8 |
9 |
10 | class Solution {
11 | public:
12 | int findTheCity(int n, vector>& edges, int distanceThreshold) {
13 | vector> grid(n, vector(n, 10005)); // 因为边的最大距离是10^4
14 |
15 | // 节点到自己的距离为0
16 | for (int i = 0; i < n; i++) grid[i][i] = 0;
17 | // 构造邻接矩阵
18 | for (const vector& e : edges) {
19 | int from = e[0];
20 | int to = e[1];
21 | int val = e[2];
22 | grid[from][to] = val;
23 | grid[to][from] = val; // 注意这里是双向图
24 | }
25 |
26 | // 开始 floyd
27 | // 思考 为什么 p 要放在最外面一层
28 | for (int p = 0; p < n; p++) {
29 | for (int i = 0; i < n; i++) {
30 | for (int j = 0; j < n; j++) {
31 | grid[i][j] = min(grid[i][j], grid[i][p] + grid[p][j]);
32 | }
33 | }
34 | }
35 |
36 | int result = 0;
37 | int count = n + 10; // 记录所有城市在范围内连接的最小城市数量
38 | for (int i = 0; i < n; i++) {
39 | int curCount = 0; // 统计一个城市在范围内可以连接几个城市
40 | for (int j = 0; j < n; j++) {
41 | if (i != j && grid[i][j] <= distanceThreshold) curCount++;
42 | // cout << "i:" << i << ", j:" << j << ", val: " << grid[i][j] << endl;
43 | }
44 | if (curCount <= count) { // 注意这里是 <=
45 | count = curCount;
46 | result = i;
47 | }
48 | }
49 | return result;
50 | }
51 | };
52 |
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/problems/1791.找出星型图的中心节点.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!
6 |
7 | # 1791.找出星型图的中心节点
8 |
9 | [题目链接](https://leetcode.cn/problems/find-center-of-star-graph/)
10 |
11 | 本题思路就是统计各个节点的度(这里没有区别入度和出度),如果某个节点的度等于这个图边的数量。 那么这个节点一定是中心节点。
12 |
13 | 什么是度,可以理解为,链接节点的边的数量。 题目中度如图所示:
14 |
15 | 
16 |
17 | 至于出度和入度,那就是在有向图里的概念了,本题是无向图。
18 |
19 | 本题代码如下:
20 |
21 | ```c++
22 |
23 | class Solution {
24 | public:
25 | int findCenter(vector>& edges) {
26 | unordered_map du;
27 | for (int i = 0; i < edges.size(); i++) { // 统计各个节点的度
28 | du[edges[i][1]]++;
29 | du[edges[i][0]]++;
30 | }
31 | unordered_map::iterator iter; // 找出度等于边熟练的节点
32 | for (iter = du.begin(); iter != du.end(); iter++) {
33 | if (iter->second == edges.size()) return iter->first;
34 | }
35 | return -1;
36 | }
37 | };
38 | ```
39 |
40 | 其实可以只记录度不用最后统计,因为题目说了一定是星状图,所以 一旦有 节点的度 大于1,就返回该节点数值就行,只有中心节点的度会大于1。
41 |
42 | 代码如下:
43 |
44 | ```c++
45 | class Solution {
46 | public:
47 | int findCenter(vector>& edges) {
48 | vector du(edges.size() + 2); // edges.size() + 1 为节点数量,下标表示节点数,所以+2
49 | for (int i = 0; i < edges.size(); i++) {
50 | du[edges[i][1]]++;
51 | du[edges[i][0]]++;
52 | if (du[edges[i][1]] > 1) return edges[i][1];
53 | if (du[edges[i][0]] > 1) return edges[i][0];
54 |
55 | }
56 | return -1;
57 | }
58 | };
59 | ```
60 |
61 | 以上代码中没有使用 unordered_map,因为遍历的时候,开辟新空间会浪费时间,而采用 vector,这是 空间换时间的一种策略。
62 |
63 | 代码其实可以再精简:
64 |
65 | ```c++
66 | class Solution {
67 | public:
68 | int findCenter(vector>& edges) {
69 | vector du(edges.size() + 2);
70 | for (int i = 0; i < edges.size(); i++) {
71 | if (++du[edges[i][1]] > 1) return edges[i][1];
72 | if (++du[edges[i][0]] > 1) return edges[i][0];
73 | }
74 | return -1;
75 | }
76 | };
77 | ```
78 |
79 |
80 |
81 |
82 |
83 |
84 |
--------------------------------------------------------------------------------
/problems/kamacoder/0108.冗余连接.md:
--------------------------------------------------------------------------------
1 |
2 |
参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!
3 |
4 | # 108. 冗余连接
5 |
6 | [卡码网题目链接(ACM模式)](https://kamacoder.com/problempage.php?pid=1181)
7 |
8 | 题目描述
9 |
10 | 树可以看成是一个图(拥有 n 个节点和 n - 1 条边的连通无环无向图)。
11 |
12 | 现给定一个拥有 n 个节点(节点编号从 1 到 n)和 n 条边的连通无向图,请找出一条可以删除的边,删除后图可以变成一棵树。
13 |
14 | 输入描述
15 |
16 | 第一行包含一个整数 N,表示图的节点个数和边的个数。
17 |
18 | 后续 N 行,每行包含两个整数 s 和 t,表示图中 s 和 t 之间有一条边。
19 |
20 | 输出描述
21 |
22 | 输出一条可以删除的边。如果有多个答案,请删除标准输入中最后出现的那条边。
23 |
24 | 输入示例
25 |
26 | ```
27 | 3
28 | 1 2
29 | 2 3
30 | 1 3
31 | ```
32 |
33 | 输出示例
34 |
35 | 1 3
36 |
37 | 提示信息
38 |
39 | 
40 |
41 | 图中的 1 2,2 3,1 3 等三条边在删除后都能使原图变为一棵合法的树。但是 1 3 由于是标准输出里最后出现的那条边,所以输出结果为 1 3
42 |
43 | 数据范围:
44 |
45 | 1 <= N <= 1000.
46 |
47 | ## 思路
48 |
49 | 这道题目也是并查集基础题目。
50 |
51 | 这里我依然降调一下,并查集可以解决什么问题:两个节点是否在一个集合,也可以将两个节点添加到一个集合中。
52 |
53 | 如果还不了解并查集,可以看这里:[并查集理论基础](./图论并查集理论基础.md)
54 |
55 | 我们再来看一下这道题目。
56 |
57 | 题目说是无向图,返回一条可以删去的边,使得结果图是一个有着N个节点的树(即:只有一个根节点)。
58 |
59 | 如果有多个答案,则返回二维数组中最后出现的边。
60 |
61 | 那么我们就可以从前向后遍历每一条边(因为优先让前面的边连上),边的两个节点如果不在同一个集合,就加入集合(即:同一个根节点)。
62 |
63 | 如图所示:
64 |
65 | 
66 |
67 | 节点A 和节点 B 不在同一个集合,那么就可以将两个 节点连在一起。
68 |
69 | 如果边的两个节点已经出现在同一个集合里,说明着边的两个节点已经连在一起了,再加入这条边一定就出现环了。
70 |
71 | 如图所示:
72 |
73 | 
74 |
75 | 已经判断 节点A 和 节点B 在在同一个集合(同一个根),如果将 节点A 和 节点B 连在一起就一定会出现环。
76 |
77 | 这个思路清晰之后,代码就很好写了。
78 |
79 | 并查集C++代码如下:
80 |
81 | ```CPP
82 | #include
83 | #include
84 | using namespace std;
85 | int n; // 节点数量
86 | vector father(1001, 0); // 按照节点大小范围定义数组
87 |
88 | // 并查集初始化
89 | void init() {
90 | for (int i = 0; i <= n; ++i) {
91 | father[i] = i;
92 | }
93 | }
94 | // 并查集里寻根的过程
95 | int find(int u) {
96 | return u == father[u] ? u : father[u] = find(father[u]);
97 | }
98 | // 判断 u 和 v是否找到同一个根
99 | bool isSame(int u, int v) {
100 | u = find(u);
101 | v = find(v);
102 | return u == v;
103 | }
104 | // 将v->u 这条边加入并查集
105 | void join(int u, int v) {
106 | u = find(u); // 寻找u的根
107 | v = find(v); // 寻找v的根
108 | if (u == v) return ; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回
109 | father[v] = u;
110 | }
111 |
112 | int main() {
113 | int s, t;
114 | cin >> n;
115 | init();
116 | for (int i = 0; i < n; i++) {
117 | cin >> s >> t;
118 | if (isSame(s, t)) {
119 | cout << s << " " << t << endl;
120 | return 0;
121 | } else {
122 | join(s, t);
123 | }
124 | }
125 | }
126 | ```
127 |
128 | 可以看出,主函数的代码很少,就判断一下边的两个节点在不在同一个集合就可以了。
129 |
130 |
131 |
132 | ## 其他语言版本
133 |
134 | ### Java
135 |
136 | ### Python
137 |
138 | ### Go
139 |
140 | ### Rust
141 |
142 | ### Javascript
143 |
144 | ### TypeScript
145 |
146 | ### PhP
147 |
148 | ### Swift
149 |
150 | ### Scala
151 |
152 | ### C#
153 |
154 | ### Dart
155 |
156 | ### C
157 |
158 |
--------------------------------------------------------------------------------
/problems/kamacoder/0111.构造二阶行列式.md:
--------------------------------------------------------------------------------
1 |
2 | # 111. 构造二阶行列式
3 |
4 | 暴力模拟就好,每个数不超过 20, 暴力枚举其实也没多大。
5 |
6 | ```CPP
7 | #include
8 | using namespace std;
9 | int main() {
10 | int n;
11 | cin >> n;
12 | for (int x = 1; x <= 20; x++) {
13 | for (int y = 1; y <= 20; y++) {
14 | for (int i = 1; i <= 20; i++) {
15 | for (int j = 1; j <= 20; j++) {
16 | if ((x * j - y * i) == n) {
17 | cout << x << " " << y << endl;
18 | cout << i << " " << j << endl;
19 | return 0;
20 | }
21 | }
22 | }
23 | }
24 | }
25 | cout << -1 << endl;
26 | }
27 | ```
28 |
--------------------------------------------------------------------------------
/problems/kamacoder/0112.挑战boss.md:
--------------------------------------------------------------------------------
1 |
2 | # 112. 挑战boss
3 |
4 | 本题题意有点绕,注意看一下 题目描述中的【提示信息】,但是在笔试中,是不给这样的提示信息的。
5 |
6 | 简单模拟:
7 |
8 | ```CPP
9 | #include
10 | using namespace std;
11 | int main() {
12 | int n, a, b, k = 0;
13 | cin >> n >> a >> b;
14 | string s;
15 | cin >> s;
16 | int result = 0;
17 | for (int i = 0; i < s.size(); i++) {
18 | int cur = a + k * b;
19 | result += cur;
20 | ++k;
21 | if (s[i] == 'x') k = 0;
22 | }
23 | cout << result << endl;
24 | return 0;
25 | }
26 | ```
27 |
--------------------------------------------------------------------------------
/problems/kamacoder/0113.国际象棋.md:
--------------------------------------------------------------------------------
1 |
2 | # 113.国际象棋
3 |
4 | 广搜,但本题如果广搜枚举马和象的话会超时。
5 |
6 | 广搜要只枚举马的走位,同时判断是否在对角巷直接走象
7 |
8 | ```CPP
9 | #include
10 | using namespace std;
11 | const int N = 100005, mod = 1000000007;
12 | using ll = long long;
13 | int n, ans;
14 | int dir[][2] = {{1, 2}, {1, -2}, {-1, 2}, {-1, -2}, {2, 1}, {2, -1}, {-2, -1}, {-2, 1}};
15 | int main() {
16 | int x1, y1, x2, y2;
17 | cin >> n;
18 | while (n--) {
19 | scanf("%d%d%d%d", &x1, &y1, &x2, &y2);
20 | if (x1 == x2 && y1 == y2) {
21 | cout << 0 << endl;
22 | continue;
23 | }
24 | // 判断象走一步到达
25 | int d = abs(x1 - x2) - abs(y1 - y2);
26 | if (!d) {cout << 1 << endl; continue;}
27 | // 判断马走一步到达
28 | bool one = 0;
29 | for (int i = 0; i < 8; ++i) {
30 | int dx = x1 + dir[i][0], dy = y1 + dir[i][1];
31 | if (dx == x2 && dy == y2) {
32 | cout << 1 << endl;
33 | one = true;
34 | break;
35 | }
36 | }
37 | if (one) continue;
38 | // 接下来为两步的逻辑, 象走两步或者马走一步,象走一步
39 | // 象直接两步可以到达,这个计算是不是同颜色的格子,象可以在两步到达所有同颜色的格子
40 | int d2 = abs(x1 - x2) + abs(y1 - y2);
41 | if (d2 % 2 == 0) {
42 | cout << 2 << endl;
43 | continue;
44 | }
45 | // 接下来判断马 + 象的组合
46 | bool two = 0;
47 | for (int i = 0; i < 8; ++i) {
48 | int dx = x1 + dir[i][0], dy = y1 + dir[i][1];
49 | int d = abs(dx - x2) - abs(dy - y2);
50 | if (!d) {cout << 2 << endl; two = true; break;}
51 | }
52 | if (two) continue;
53 | // 剩下的格子全都是三步到达的
54 | cout << 3 << endl;
55 | }
56 | return 0;
57 | }
58 |
59 | ```
60 |
--------------------------------------------------------------------------------
/problems/kamacoder/0114.小欧的平均数.md:
--------------------------------------------------------------------------------
1 |
2 | # 114. 小欧的平均数
3 |
4 | 这道题非常的脑筋急转弯, 读题都要理解半天。
5 |
6 | 初步读题,感觉好像是求 如何最小加减,得到三个数的平均数。
7 |
8 | 但题意不是这样的。
9 |
10 | 小欧的说的三个数平衡,只是三个数里 任何两个数 相加都能被2整除, 那么 也就是说,这三个数 要么都是 奇数,要么都是偶数,才能达到小欧所说的平衡。
11 |
12 | 所以题目要求的,就是,三个数,最小加减1 几次 可以让三个数都变成奇数,或者都变成偶数。
13 |
14 | 所以最终的结果 不是1 就是0,没有其他的。
15 |
16 | 录友可能想,题目出的这么绕干啥? 没办法,企业的笔试题就是这样的。
17 |
18 | ```CPP
19 | #include
20 | #include
21 | using namespace std;
22 | int main() {
23 | int x, y, z;
24 | cin >> x >> y >> z;
25 | int count = (x % 2 == 0) + (y % 2 == 0) + (z % 2 == 0);
26 | cout << min(3 - count, count);
27 | }
28 | ```
29 |
30 |
--------------------------------------------------------------------------------
/problems/kamacoder/0115.组装手机.md:
--------------------------------------------------------------------------------
1 |
2 | # 115. 组装手机
3 |
4 | 这道题是比较难得哈希表题目。 把代码随想录哈希表章节理解透彻,做本题没问题。
5 |
6 | 思路是
7 |
8 | 1. 用哈希表记录 外壳售价 和 手机零件售价 出现的次数
9 | 2. 记录总和出现的次数
10 | 3. 遍历总和,减去 外壳售价,看 手机零件售价出现了几次
11 | 4. 最后累加,取最大值
12 |
13 | 有一个需要注意的点: 数字可以重复,在计算个数的时候,如果计算重复的数字
14 |
15 | 例如 如果输入是
16 |
17 | ```
18 | 4
19 | 1 1 1 1
20 | 1 1 1 1
21 | ```
22 | 那么输出应该是 4, 外壳售价 和 手机零件售价 是可以重复的。
23 |
24 | 代码如下:
25 |
26 | ```CPP
27 | #include
28 | #include
29 | #include
30 | #include
31 | using namespace std;
32 | int main() {
33 | int n;
34 | cin >> n;
35 | vector aVec(n, 0);
36 | vector bVec(n, 0);
37 | unordered_map aUmap;
38 | unordered_map bUmap;
39 | for (int i = 0; i < n; i++) {
40 | cin >> aVec[i];
41 | aUmap[aVec[i]]++;
42 | }
43 | for (int i = 0; i < n; i++) {
44 | cin >> bVec[i];
45 | bUmap[bVec[i]]++;
46 | }
47 | unordered_set uset;
48 | for (int i = 0; i < n; i++) {
49 | for (int j = 0; j < n; j++){
50 | uset.insert(aVec[i] + bVec[j]);
51 | }
52 | }
53 | int result = 0;
54 | for (int sum : uset) {
55 | //cout << p.first << endl;
56 | int count = 0;
57 | for (pair p : aUmap) {
58 | //cout << p.first - aVec[i] << endl;
59 | if (sum - p.first > 0 && bUmap[sum - p.first] != 0) {
60 | count += min(bUmap[sum - p.first], p.second);
61 | }
62 | }
63 | result = max(result, count);
64 | }
65 | cout << result << endl;
66 | }
67 | ```
68 |
--------------------------------------------------------------------------------
/problems/kamacoder/0121.大数减法.md:
--------------------------------------------------------------------------------
1 |
2 | 参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!
3 |
4 | # 大数减法
5 |
6 | 本题测试数据超过int 和 longlong了,所以考察的使用 string 来模拟 两个大数的 加减操作。
7 |
8 | 当然如果使用python或者Java 使用库函数都可以水过。
9 |
10 | 使用字符串来模拟过程,需要处理以下几个问题:
11 |
12 | * 负号处理:要考虑正负数的处理,如果大数相减的结果是负数,需要在结果前加上负号。
13 | * 大数比较:在进行减法之前,需要确定哪个数大,以便知道结果是否需要添加负号。
14 | * 位数借位:处理大数相减时的借位问题,这类似于手动减法。
15 |
16 | ```CPP
17 | #include
18 | #include
19 | #include
20 | using namespace std;
21 |
22 | // 比较两个字符串表示的数字,返回1表示a > b,0表示a == b,-1表示a < b
23 | int compareStrings(const string& a, const string& b) {
24 | if (a.length() > b.length()) return 1;
25 | if (a.length() < b.length()) return -1;
26 | return a.compare(b);
27 | }
28 |
29 | // 去除字符串左侧的前导零
30 | string removeLeadingZeros(const string& num) {
31 | size_t start = 0;
32 | while (start < num.size() && num[start] == '0') {
33 | start++;
34 | }
35 | return start == num.size() ? "0" : num.substr(start);
36 | }
37 |
38 | // 大数相减,假设a >= b
39 | string subtractStrings(const string& a, const string& b) {
40 | string result;
41 | int len1 = a.length(), len2 = b.length();
42 | int carry = 0;
43 |
44 | for (int i = 0; i < len1; i++) {
45 | int digitA = a[len1 - 1 - i] - '0';
46 | int digitB = i < len2 ? b[len2 - 1 - i] - '0' : 0;
47 |
48 | int digit = digitA - digitB - carry;
49 | if (digit < 0) {
50 | digit += 10;
51 | carry = 1;
52 | } else {
53 | carry = 0;
54 | }
55 |
56 | result.push_back(digit + '0');
57 | }
58 |
59 | // 去除结果中的前导零
60 | reverse(result.begin(), result.end());
61 | return removeLeadingZeros(result);
62 | }
63 |
64 | string subtractLargeNumbers(const string& num1, const string& num2) {
65 | string a = num1, b = num2;
66 |
67 | // 比较两个数的大小
68 | int cmp = compareStrings(a, b);
69 |
70 | if (cmp == 0) {
71 | return "0"; // 如果两个数相等,结果为0
72 | } else if (cmp < 0) {
73 | // 如果a < b,交换它们并在结果前加上负号
74 | swap(a, b);
75 | return "-" + subtractStrings(a, b);
76 | } else {
77 | return subtractStrings(a, b);
78 | }
79 | }
80 |
81 | int main() {
82 | string num1, num2;
83 | cin >> num1 >> num2;
84 |
85 | string result = subtractLargeNumbers(num1, num2);
86 | cout << result << endl;
87 |
88 | return 0;
89 | }
90 |
91 | ```
92 |
--------------------------------------------------------------------------------
/problems/kamacoder/0121.小红的区间翻转.md:
--------------------------------------------------------------------------------
1 |
2 | # 121. 小红的区间翻转
3 |
4 | 比较暴力的方式,就是直接模拟, 枚举所有 区间,然后检查其翻转的情况。
5 |
6 | 在检查翻转的时候,需要一些代码优化,否则容易超时。
7 |
8 | ```CPP
9 | #include
10 | #include
11 | using namespace std;
12 |
13 | bool canTransform(const vector& a, const vector& b, int left, int right) {
14 | // 提前检查翻转区间的值是否可以匹配
15 | for (int i = left, j = right; i <= right; i++, j--) {
16 | if (a[i] != b[j]) {
17 | return false;
18 | }
19 | }
20 | // 检查翻转区间外的值是否匹配
21 | for (int i = 0; i < left; i++) {
22 | if (a[i] != b[i]) {
23 | return false;
24 | }
25 | }
26 | for (int i = right + 1; i < a.size(); i++) {
27 | if (a[i] != b[i]) {
28 | return false;
29 | }
30 | }
31 | return true;
32 | }
33 |
34 | int main() {
35 | int n;
36 | cin >> n;
37 |
38 | vector a(n);
39 | vector b(n);
40 |
41 | for (int i = 0; i < n; i++) {
42 | cin >> a[i];
43 | }
44 |
45 | for (int i = 0; i < n; i++) {
46 | cin >> b[i];
47 | }
48 |
49 | int count = 0;
50 |
51 | // 遍历所有可能的区间
52 | for (int left = 0; left < n; left++) {
53 | for (int right = left; right < n; right++) {
54 | // 检查翻转区间 [left, right] 后,a 是否可以变成 b
55 | if (canTransform(a, b, left, right)) {
56 | count++;
57 | }
58 | }
59 | }
60 | cout << count << endl;
61 | return 0;
62 | }
63 | ```
64 |
65 | 也可以事先计算好,最长公共前缀,和最长公共后缀。
66 |
67 | 在公共前缀和公共后缀之间的部分进行翻转操作,这样我们可以减少很多不必要的翻转尝试。
68 |
69 | 通过在公共前缀和后缀之间的部分,找到可以通过翻转使得 a 和 b 相等的区间。
70 |
71 | 以下 为评论区 卡码网用户:码鬼的C++代码
72 |
73 | ```CPP
74 | #include
75 | #include
76 |
77 | using namespace std;
78 |
79 | int main() {
80 | int n;
81 | cin >> n;
82 | vector a(n), b(n);
83 | for (int i = 0; i < n; i++) {
84 | cin >> a[i];
85 | }
86 | for (int i = 0; i < n; i++) {
87 | cin >> b[i];
88 | }
89 |
90 | vector prefix(n, 0), suffix(n, 0);
91 |
92 | // 计算前缀相等的位置
93 | int p = 0;
94 | while (p < n && a[p] == b[p]) {
95 | prefix[p] = 1;
96 | p++;
97 | }
98 |
99 | // 计算后缀相等的位置
100 | int s = n - 1;
101 | while (s >= 0 && a[s] == b[s]) {
102 | suffix[s] = 1;
103 | s--;
104 | }
105 |
106 | int count = 0;
107 |
108 | // 遍历所有可能的区间
109 | for (int i = 0; i < n - 1; i++) {
110 | for (int j = i + 1; j < n; j++) {
111 | // 判断前缀和后缀是否相等
112 | if ((i == 0 || prefix[i - 1] == 1) && (j == n - 1 || suffix[j + 1] == 1)) {
113 | // 判断翻转后的子数组是否和目标数组相同
114 | bool is_palindrome = true;
115 | for (int k = 0; k <= (j - i) / 2; k++) {
116 | if (a[i + k] != b[j - k]) {
117 | is_palindrome = false;
118 | break;
119 | }
120 | }
121 | if (is_palindrome) {
122 | count++;
123 | }
124 | }
125 | }
126 | }
127 |
128 | cout << count << endl;
129 |
130 | return 0;
131 | }
132 | ```
133 |
--------------------------------------------------------------------------------
/problems/kamacoder/0122.滑动窗口最大值.md:
--------------------------------------------------------------------------------
1 |
2 | # 滑动窗口最大值
3 |
4 | 本题是 [代码随想录:滑动窗口最大值](https://www.programmercarl.com/0239.%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E6%9C%80%E5%A4%A7%E5%80%BC.html) 的升级版。
5 |
6 | 在[代码随想录:滑动窗口最大值](https://www.programmercarl.com/0239.%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E6%9C%80%E5%A4%A7%E5%80%BC.html) 中详细讲解了如何求解 滑动窗口的最大值。
7 |
8 | 那么求滑动窗口的最小值原理也是一样的, 大家稍加思考,把优先级队列里的 大于 改成小于 就行了。
9 |
10 | 求最大值的优先级队列(从大到小)
11 | ```
12 | while (!que.empty() && value > que.back()) {
13 | ```
14 |
15 | 求最小值的优先级队列(从小到大)
16 | ```
17 | while (!que.empty() && value > que.back()) {
18 | ```
19 |
20 | 这样在滑动窗口里 最大值最小值都求出来了,遍历一遍找出 差值最大的就好。
21 |
22 | 至于输入,需要一波字符串处理,比较考察基本功。
23 |
24 | CPP代码如下:
25 |
26 | ```CPP
27 | #include
28 | #include
29 | #include
30 | #include
31 | #include
32 | using namespace std;
33 | class MyBigQueue { //单调队列(从大到小)
34 | public:
35 | deque que; // 使用deque来实现单调队列
36 | // 每次弹出的时候,比较当前要弹出的数值是否等于队列出口元素的数值,如果相等则弹出。
37 | // 同时pop之前判断队列当前是否为空。
38 | void pop(int value) {
39 | if (!que.empty() && value == que.front()) {
40 | que.pop_front();
41 | }
42 | }
43 | // 如果push的数值大于入口元素的数值,那么就将队列后端的数值弹出,直到push的数值小于等于队列入口元素的数值为止。
44 | // 这样就保持了队列里的数值是单调从大到小的了。
45 | void push(int value) {
46 | while (!que.empty() && value > que.back()) {
47 | que.pop_back();
48 | }
49 | que.push_back(value);
50 |
51 | }
52 | // 查询当前队列里的最大值 直接返回队列前端也就是front就可以了。
53 | int front() {
54 | return que.front();
55 | }
56 | };
57 |
58 | class MySmallQueue { //单调队列(从小到大)
59 | public:
60 | deque que;
61 |
62 | void pop(int value) {
63 | if (!que.empty() && value == que.front()) {
64 | que.pop_front();
65 | }
66 | }
67 |
68 | // 和上面队列的区别是这里换成了小于,
69 | void push(int value) {
70 | while (!que.empty() && value < que.back()) {
71 | que.pop_back();
72 | }
73 | que.push_back(value);
74 |
75 | }
76 |
77 | int front() {
78 | return que.front();
79 | }
80 | };
81 |
82 | int main() {
83 | string input;
84 |
85 | getline(cin, input);
86 |
87 | vector nums;
88 | int k;
89 |
90 | // 找到并截取nums的部分
91 | int numsStart = input.find('[');
92 | int numsEnd = input.find(']');
93 | string numsStr = input.substr(numsStart + 1, numsEnd - numsStart - 1);
94 | // cout << numsStr << endl;
95 |
96 | // 用字符串流处理nums字符串,提取数字
97 | stringstream ss(numsStr);
98 | string temp;
99 | while (getline(ss, temp, ',')) {
100 | nums.push_back(stoi(temp));
101 | }
102 |
103 | // 找到并提取k的值
104 | int kStart = input.find("k = ") + 4;
105 | k = stoi(input.substr(kStart));
106 |
107 | MyBigQueue queB; // 获取区间最大值
108 | MySmallQueue queS; // 获取区间最小值
109 | // vector result;
110 | for (int i = 0; i < k; i++) { // 先将前k的元素放进队列
111 | queB.push(nums[i]);
112 | queS.push(nums[i]);
113 | }
114 |
115 | int result = queB.front() - queS.front();
116 | for (int i = k; i < nums.size(); i++) {
117 | queB.pop(nums[i - k]); // 滑动窗口移除最前面元素
118 | queB.push(nums[i]); // 滑动窗口前加入最后面的元素
119 |
120 | queS.pop(nums[i - k]);
121 | queS.push(nums[i]);
122 |
123 | result = max (result, queB.front() - queS.front());
124 | }
125 | cout << result << endl;
126 | }
127 | ```
128 |
--------------------------------------------------------------------------------
/problems/kamacoder/0123.小红的数组构造.md:
--------------------------------------------------------------------------------
1 |
2 | 121. 小红的数组构造
3 |
4 | 本题大家不要想着真去模拟数组的情况,那样就想复杂了。
5 |
6 | 数组只能是:1k、2k、3k ... (n-1)k、nk,这样 总和就是最小的。
7 |
8 | 注意最后的和可能超过int,所以用 long long。
9 |
10 | 代码如下:
11 |
12 | ```CPP
13 | #include
14 | using namespace std;
15 | int main () {
16 | long long result = 0;
17 | int n, k;
18 | cin >> n >> k;
19 | for (int i = 1; i <= n; i++) {
20 | result += i * k;
21 | }
22 | cout << result << endl;
23 | }
24 | ```
25 |
26 | 优化思路:
27 |
28 |
29 | 由于要计算1到n的整数之和,可以利用等差数列求和公式来优化计算。
30 |
31 | 和公式:1 + 2 + 3 + ... + n = n * (n + 1) / 2
32 |
33 | 因此,总和 result = k * (n * (n + 1) / 2)
34 |
35 | ```CPP
36 |
37 | #include
38 | using namespace std;
39 |
40 | int main() {
41 | long long result = 0;
42 | int n, k;
43 | cin >> n >> k;
44 |
45 | // 使用等差数列求和公式进行计算
46 | result = k * (n * (n + 1LL) / 2);
47 |
48 | cout << result << endl;
49 | return 0;
50 | }
51 |
52 | ```
53 |
--------------------------------------------------------------------------------
/problems/kamacoder/0124.精华帖子.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # 122.精华帖子
4 |
5 |
6 | 开辟一个数组,默认都是0,把精华帖标记为1.
7 |
8 | 使用前缀和,快速计算出,k 范围内 有多少个精华帖。
9 |
10 | 前缀和要特别注意区间问题,即 vec[i+k] - vec[i] 求得区间和是 (i, i + k] 这个区间,注意这是一个左开右闭的区间。
11 |
12 | 所以前缀和 很容易漏掉 vec[0] 这个数值的计算
13 |
14 | ```CPP
15 | #include
16 | #include
17 | using namespace std;
18 | int main() {
19 | int n, m, k, l, r;
20 | cin >> n >> m >> k;
21 | vector vec(n);
22 | while (m--) {
23 | cin >> l >> r;
24 | for (int i = l; i < r; i++) vec[i] = 1;
25 | }
26 | int result = 0;
27 | for (int i = 0; i < k; i++) result += vec[i]; // 提前预处理result,包含vec[0]的区间,否则前缀和容易漏掉这个区间
28 |
29 | for (int i = 1; i < n; i++) {
30 | vec[i] += vec[i - 1];
31 | }
32 |
33 | for (int i = 0; i < n - k; i++) {
34 | result = max (result, vec[i + k] - vec[i]);
35 | }
36 | cout << result << endl;
37 | }
38 | ```
39 |
--------------------------------------------------------------------------------
/problems/kamacoder/0125.连续子数组最大和.md:
--------------------------------------------------------------------------------
1 |
2 | # 123.连续子数组最大和
3 |
4 | 这道题目可以说是 [代码随想录,动态规划:最大子序和](https://www.programmercarl.com/0053.%E6%9C%80%E5%A4%A7%E5%AD%90%E5%BA%8F%E5%92%8C%EF%BC%88%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%EF%BC%89.html) 的升级版。
5 |
6 | 题目求的是 可以替换一个数字 之后 的 连续子数组最大和。
7 |
8 | 如果替换的是数组下标 i 的元素。
9 |
10 | 那么可以用 [代码随想录,动态规划:最大子序和](https://www.programmercarl.com/0053.%E6%9C%80%E5%A4%A7%E5%AD%90%E5%BA%8F%E5%92%8C%EF%BC%88%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%EF%BC%89.html) 的方法,先求出 [0 - i) 区间的 最大子序和 dp1 和 (i, n)的最大子序和dp2 。
11 |
12 | 然后在遍历一遍i, 计算 dp1 + dp2 + vec[i] 的最大值就可以。
13 |
14 | 正序遍历,求出 [0 - i) 区间的 最大子序,dp[ i - 1] 表示 是 包括下标i - 1(以vec[i - 1]为结尾)的最大连续子序列和为dp[i - 1]。
15 |
16 | 所以 在计算区间 (i, n)即 dp2 的时候,我们要倒叙。 因为我们求的是以 包括下标i + 1 为起始位置的最大连续子序列和为dp[i + 1]。
17 |
18 | 这样 dp1 + dp2 + vec[i] 才是一个完整区间。
19 |
20 | 这里就体现出对 dp数组定义的把控,本题如果对 dp数组含义理解不清,其实是不容易做出来的。
21 |
22 | 代码:
23 |
24 | ```CPP
25 | #include
26 | #include
27 | #include
28 | using namespace std;
29 | int main() {
30 | int t, n, x;
31 | cin >> t;
32 | while (t--) {
33 | cin >> n >> x;
34 | vector vec(n);
35 | for (int i = 0; i < n; i++) cin >> vec[i];
36 | vector dp1(n);
37 | dp1[0] = vec[0];
38 | int res = vec[0];
39 | // 从前向后统计最大子序和
40 | for (int i = 1; i < n; i++) {
41 | dp1[i] = max(dp1[i - 1] + vec[i], vec[i]); // 状态转移公式
42 | res = max(res, dp1[i]);
43 | }
44 |
45 | res = max(res, vec[n - 1]);
46 | // 从后向前统计最大子序和
47 | vector dp2(n);
48 | dp2[n - 1] = vec[n - 1];
49 | for (int i = n - 2; i >= 0; i--) {
50 | dp2[i] = max(dp2[i + 1] + vec[i], vec[i]);
51 |
52 | }
53 |
54 | for (int i = 0 ; i < n ; i++) {
55 | int dp1res = 0;
56 | if (i > 0) dp1res = max(dp1[i-1], 0);
57 | int dp2res = 0;
58 | if (i < n - 1 ) dp2res = max(dp2[i+1], 0);
59 |
60 | res = max(res, dp1res + dp2res + x);
61 | }
62 | cout << res << endl;
63 | }
64 |
65 | }
66 | ```
67 |
--------------------------------------------------------------------------------
/problems/kamacoder/0127.小美的排列询问.md:
--------------------------------------------------------------------------------
1 |
2 | # 小美的排列询问
3 |
4 | 模拟题,注意 x 和y 不分先后
5 |
6 | ```CPP
7 |
8 | #include
9 | #include
10 | using namespace std;
11 | int main() {
12 | int n, x, y;
13 | cin >> n;
14 | vector vec(n, 0);
15 | for (int i =0; i < n; i++) {
16 | cin >> vec[i];
17 | }
18 | cin >> x >> y;
19 | for (int i = 0; i < n - 1; i++) {
20 | if (x == vec[i] && y == vec[i + 1]) || (y == vec[i] && x == vec[i + 1]) ) {
21 | cout << "Yes" << endl;
22 | return 0;
23 | }
24 | }
25 | cout << "No" << endl;
26 |
27 | }
28 |
29 | ```
30 |
--------------------------------------------------------------------------------
/problems/kamacoder/0128.小美走公路.md:
--------------------------------------------------------------------------------
1 |
2 | # 小美走公路
3 |
4 | 在处理环形情况的时候,很多录友容易算懵了,不是多算一个数,就是少算一个数。
5 |
6 | 这里这样的题目,最好的方式是将 两个环展开,首尾相连,这样我们就可以通过 直线的思维去解题了
7 |
8 | 两个注意点:
9 |
10 | 1. x 可以比 y 大,题目没规定 x 和y 的大小顺序
11 | 2. 累计相加的数可能超过int
12 |
13 |
14 | ```CPP
15 | #include
16 | #include
17 | using namespace std;
18 | int main () {
19 | int n;
20 | cin >> n;
21 | vector vec(2* n + 1, 0);
22 | for (int i = 1; i <= n; i++) {
23 | cin >> vec[i];
24 | vec[n + i] = vec[i];
25 | }
26 | int x, y;
27 | cin >> x >> y;
28 | int xx = min(x ,y); // 注意点1:x 可以比 y 大
29 | int yy = max(x, y);
30 | long long a = 0, b = 0; // 注意点2:相加的数可能超过int
31 | for (int i = xx; i < yy; i++) a += vec[i];
32 | for (int i = yy; i < xx + n; i++ ) b += vec[i];
33 | cout << min(a, b) << endl;
34 | }
35 | ```
36 |
--------------------------------------------------------------------------------
/problems/kamacoder/0129.小美的蛋糕切割.md:
--------------------------------------------------------------------------------
1 |
2 | # 小美的蛋糕切割
3 |
4 | 二维前缀和,不了解前缀和的录友 可以自行查一下,是一个很容易理解的算法思路
5 |
6 | ```CPP
7 |
8 | #include
9 | #include
10 | #include
11 |
12 | using namespace std;
13 | int main () {
14 | int n, m;
15 | cin >> n >> m;
16 | int sum = 0;
17 | vector> vec(n, vector(m, 0)) ;
18 | for (int i = 0; i < n; i++) {
19 | for (int j = 0; j < m; j++) {
20 | cin >> vec[i][j];
21 | sum += vec[i][j];
22 | }
23 | }
24 | // 统计横向
25 | vector horizontal(n, 0);
26 | for (int i = 0; i < n; i++) {
27 | for (int j = 0 ; j < m; j++) {
28 | horizontal[i] += vec[i][j];
29 | }
30 | }
31 | // 统计纵向
32 | vector vertical(m , 0);
33 | for (int j = 0; j < m; j++) {
34 | for (int i = 0 ; i < n; i++) {
35 | vertical[j] += vec[i][j];
36 | }
37 | }
38 | int result = INT_MAX;
39 | int horizontalCut = 0;
40 | for (int i = 0 ; i < n; i++) {
41 | horizontalCut += horizontal[i];
42 | result = min(result, abs(sum - horizontalCut - horizontalCut));
43 | }
44 | int verticalCut = 0;
45 | for (int j = 0; j < m; j++) {
46 | verticalCut += vertical[j];
47 | result = min(result, abs(sum - verticalCut - verticalCut));
48 | }
49 | cout << result << endl;
50 | }
51 | ```
52 |
--------------------------------------------------------------------------------
/problems/kamacoder/0130.小美的字符串变换.md:
--------------------------------------------------------------------------------
1 |
2 | # 130.小美的字符串变换
3 |
4 | 本题是[岛屿数量](./0099.岛屿的数量广搜.md)的进阶版,主要思路和代码都是一样的,统计一个图里岛屿的数量,也是染色问题。
5 |
6 | 1、 先枚举各个可能出现的矩阵
7 | 2、 针对矩阵经行广搜染色(深搜,并查集一样可以)
8 | 3、 统计岛屿数量最小的数量。
9 |
10 | ```CPP
11 | #include
12 | #include
13 | #include
14 | #include
15 | using namespace std;
16 |
17 | // 广搜代码同 卡码网:99. 岛屿数量
18 | int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 四个方向
19 | void bfs(const vector>& grid, vector>& visited, int x, int y, char a) {
20 | queue> que;
21 | que.push({x, y});
22 | visited[x][y] = true; // 只要加入队列,立刻标记
23 | while(!que.empty()) {
24 | pair cur = que.front(); que.pop();
25 | int curx = cur.first;
26 | int cury = cur.second;
27 | for (int i = 0; i < 4; i++) {
28 | int nextx = curx + dir[i][0];
29 | int nexty = cury + dir[i][1];
30 | if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue; // 越界了,直接跳过
31 | if (!visited[nextx][nexty] && grid[nextx][nexty] == a) {
32 | que.push({nextx, nexty});
33 | visited[nextx][nexty] = true; // 只要加入队列立刻标记
34 | }
35 | }
36 | }
37 | }
38 |
39 | int main() {
40 | int n;
41 | string s;
42 | cin >> n;
43 | int result = INT_MAX;
44 | cin >> s;
45 | for (int k = 1; k < n; k++) {
46 | if (n % k != 0) continue;
47 | // 计算出 矩阵的 行 和 列
48 | int x = n / k;
49 | int y = k;
50 | //cout << x << " " << y << endl;
51 | vector> vec(x, vector(y, 0));
52 | // 填装矩阵
53 | int sCount = 0;
54 | for (int i = 0; i < x; i++) {
55 | for (int j = 0; j < y; j++) {
56 | vec[i][j] = s[sCount++];
57 | }
58 | }
59 |
60 | // 开始广搜染色
61 | vector> visited(x, vector(y, false));
62 | int count = 0;
63 | for (int i = 0; i < x; i++) {
64 | for (int j = 0; j < y; j++) {
65 |
66 | if (!visited[i][j]) {
67 | count++; // 遇到没访问过的陆地,+1
68 | bfs(vec, visited, i, j, vec[i][j]); // 将与其链接的陆地都标记上 true
69 | }
70 | }
71 | }
72 | // 取岛屿数量最少的
73 | result = min (result, count);
74 |
75 | }
76 | cout << result << endl;
77 | }
78 | ```
79 |
--------------------------------------------------------------------------------
/problems/kamacoder/0131.小美的树上染色.md:
--------------------------------------------------------------------------------
1 | # 131. 小美的树上染色
2 |
3 | 本题为树形dp 稍有难度,主要在于 递推公式上。
4 |
5 | dp数组的定义:
6 |
7 | dp[cur][1] :当前节点染色,那么当前节点为根节点及其左右子节点中,可以染色的最大数量
8 |
9 | dp[cur][0] :当前节点不染色,那么当前节点为根节点及其左右子节点中,可以染色的最大数量
10 |
11 | 关于 dp转移方程
12 |
13 | 1、 情况一:
14 |
15 | 如果当前节点不染色,那就去 子节点 染色 或者 不染色的最大值。
16 |
17 | `dp[cur][0] += max(dp[child][0], dp[child][1]);`
18 |
19 |
20 | 2、情况二:
21 |
22 | 那么当前节点染色的话,这种情况就不好想了。
23 |
24 | 首先这不是二叉树,每一个节点都有可能 会有n个子节点。
25 |
26 | 所以我们要分别讨论,每一个子节点的情况 对父节点的影响。
27 |
28 | 那么父节点 针对每种情况,就要去 最大值, 也就是 `dp[cur][1] = max(dp[cur][1], 每个自孩子的情况)`
29 |
30 | 如图,假如节点1 是我们要计算的父节点,节点2是我们这次要计算的子节点。
31 |
32 | 
33 |
34 | 选中一个节点2 作为我们这次计算的子节点,父节点染色的话,子节点必染色。
35 |
36 | 接下来就是计算 父节点1和该子节点2染色的话, 以子节点2 为根的 染色节点的最大数量 。
37 |
38 | 是:节点2不染色 且 以节点2为根节点的最大 染色数量 + 2, + 2 是因为 节点 1 和 节点2 要颜色了,染色节点增加两个。
39 |
40 | 代码:`dp[child][0] + 2`
41 |
42 | 细心的录友会发现,那我们只计算了 红色框里面的,那么框外 最大的染色数量是多少呢?
43 |
44 | 
45 |
46 |
47 | 先看 作为子节点的节点2 为根节点的最大染色数量是多少? 取一个最值,即 节点2染色 或者 不染色取最大值。
48 |
49 | 代码:`max(dp[child][0], dp[child][1])`
50 |
51 | 那么红框以外的 染色最大节点数量 就是 `dp[cur][0] - max(dp[child][0], dp[child][1])`
52 |
53 | (cur是节点1,child是节点2)
54 |
55 | 红框以外的染色最大数量 + 父节点1和该子节点2染色的话 以子节点2 为根的 染色节点的最大数量 就是 节点1 染色的最大节点数量。
56 |
57 | 代码:
58 |
59 | `dp[cur][1] = max(dp[cur][1], dp[cur][0] - max(dp[child][0], dp[child][1]) + dp[child][0] + 2);`
60 |
61 | 整体代码如下:
62 |
63 | ```CPP
64 |
65 | #include
66 | #include
67 | #include
68 | #include
69 | #include
70 |
71 | using namespace std;
72 |
73 | int maxN = 10005;
74 | vector> dp (maxN, vector(2, 0));
75 | vector> grid(maxN); // 邻接表
76 | vector value(maxN); // 存储每个节点的权值
77 |
78 |
79 | // 在树上进行动态规划的函数
80 | void dpOnTheTree(int cur) {
81 |
82 | for (int child : grid[cur]) {
83 | // 后序遍历,从下向上计算
84 | dpOnTheTree(child);
85 | // 情况一
86 | dp[cur][0] += max(dp[child][0], dp[child][1]);
87 |
88 | }
89 |
90 | // 计算dp[1] - 当前节点染色
91 | for (int child : grid[cur]) {
92 | long mul = value[cur] * value[child]; // 当前节点和相邻节点权值的乘积
93 | long sqrtNum = (long) sqrt(mul);
94 |
95 | if (sqrtNum * sqrtNum == mul) { // 如果乘积是完全平方数
96 | // 情况二
97 | // dp[cur][0] 表示所有子节点 染色或者不染色的 最大染色数量
98 | // max(dp[child][0], dp[child][1]) 需要染色节点的孩子节点的最大染色数量
99 | // dp[cur][0] - max(dp[child][0], dp[child][1]) 除了要染色的节点及其子节点,其他孩子的最大染色数量
100 | // 最后 + dp[child][0] + 2 , 就是本节点染色的最大染色节点数量
101 | dp[cur][1] = max(dp[cur][1], dp[cur][0] - max(dp[child][0], dp[child][1]) + dp[child][0] + 2);
102 | }
103 | }
104 |
105 | }
106 |
107 | int main() {
108 |
109 | int n;
110 | cin >> n; // 输入节点数量
111 |
112 | // 读取节点权值
113 | for (int i = 1; i <= n; ++i) {
114 | cin >> value[i];
115 | }
116 |
117 | // 构建树的邻接表
118 | for (int i = 1; i < n; ++i) {
119 | int x, y;
120 | cin >> x >> y;
121 | grid[x].push_back(y);
122 | }
123 |
124 | // 从根节点(节点1)开始进行动态规划
125 | dpOnTheTree(1);
126 |
127 | // 输出最大染色节点数量
128 | cout << max(dp[1][0], dp[1][1]) << endl;
129 |
130 | return 0;
131 | }
132 |
133 | ```
134 |
135 |
--------------------------------------------------------------------------------
/problems/kamacoder/0132.夹吃旗.md:
--------------------------------------------------------------------------------
1 |
2 | # 132. 夹吃棋
3 |
4 | [题目链接](https://kamacoder.com/problempage.php?pid=1209)
5 |
6 | 这道题是模拟题,但很多录友可能想复杂了。
7 |
8 | 行方向,白棋吃,只有这样的布局 `o*o`,黑棋吃,只有这样的布局 `*o*`
9 |
10 | 列方向也是同理的。
11 |
12 | 想到这一点,本题的代码就容易写了, C++代码如下:
13 |
14 | ```CPP
15 | #include
16 | #include
17 | using namespace std;
18 | int main() {
19 | int n;
20 | cin >> n;
21 | while (n--) {
22 | int black = 0, white = 0;
23 | vector grid(3, "");
24 | // 判断行
25 | for (int i = 0; i < 3; i++) {
26 | cin >> grid[i];
27 | if (grid[i] == "o*o") white++;
28 | if (grid[i] == "*o*") black++;
29 | }
30 | // 判断列
31 | for (int i = 0; i < 3; i++) {
32 | string s;
33 | s += grid[0][i];
34 | s += grid[1][i];
35 | s += grid[2][i];
36 | if (s == "o*o") white++;
37 | if (s == "*o*") black++;
38 | }
39 | // 如果一个棋盘的局面没有一方被夹吃或者黑白双方都被对面夹吃,则认为是平局
40 | if ((!white && !black) || (white && black)) cout << "draw" << endl;
41 | // 白棋赢
42 | else if (white && !black) cout << "yukan" << endl;
43 | // 黑棋赢
44 | else cout << "kou" << endl;
45 | }
46 | }
47 | ```
48 |
--------------------------------------------------------------------------------
/problems/kamacoder/0133.小红买药.md:
--------------------------------------------------------------------------------
1 |
2 | # 133. 小红买药
3 |
4 | [题目链接](https://kamacoder.com/problempage.php?pid=1210)
5 |
6 | 本题是一道直观的模拟题,但也并不简单,很多情况容易漏了,笔试现场可能要多错几次 才能把情况都想到。
7 |
8 | 主要是三个情况:
9 |
10 | * 小红没症状,药有副作用,统计加一,同时要给小红标记上症状
11 | * 小红有症状,药治不了,同时也没副症状 ,这时也要统计加一
12 | * 小红有症状,药可以治,给小红取消症状标记
13 |
14 |
15 | ```CPP
16 | #include
17 | #include
18 | using namespace std;
19 | int main() {
20 | int n, m, q, u;
21 | cin >> n;
22 | string s;
23 | cin >> s;
24 | cin >> m;
25 | vector a(m + 1); // 因为后面u是从1开始的
26 | vector b(m + 1);
27 | for (int i = 1; i <= m; i++) {
28 | cin >> a[i] >> b[i];
29 | }
30 | cin >> q;
31 | while (q--) {
32 | cin >> u;
33 | int num = 0;
34 | for (int i = 0; i < n; i++) {
35 | // s 没症状,但b给了副作用,统计num的同时,要给s标记上症状
36 | if (s[i] == '0' && b[u][i] == '1') {
37 | num ++;
38 | s[i] = '1';
39 | }
40 | // s 有症状,但 a治不了,b也没副症状
41 | else if (s[i] == '1' && a[u][i] == '0' && a[u][i] == '0') num++;
42 | // s 有症状,a 可以治
43 | else if (s[i] == '1' && a[u][i] == '1') s[i] = '0';
44 | }
45 | cout << num << endl;
46 | }
47 | }
48 | ```
49 |
--------------------------------------------------------------------------------
/problems/kamacoder/0134.皇后移动的最小步数.md:
--------------------------------------------------------------------------------
1 |
2 | # 134. 皇后移动的最小步数
3 |
4 | [题目链接](https://kamacoder.com/problempage.php?pid=1211)
5 |
6 | 本题和 [代码随想录-不同路径](https://www.programmercarl.com/0062.%E4%B8%8D%E5%90%8C%E8%B7%AF%E5%BE%84.html) 有一些类似。
7 |
8 | 关键是弄清楚递推公式
9 |
10 | 一共分三个情况,
11 |
12 | 情况一,向右移动:
13 |
14 | 然后从 (i, j) 再向右走 到 (i, k)。 无论k 多大,步数只加1 :
15 |
16 | `dp[i][k] = dp[i][j] + 1`
17 |
18 | 那么 `dp[i][k]` 也有可能 从其他方向得到,例如 从上到下, 或者斜上方到达 dp[i][k]
19 |
20 | 本题我们要求最小步数,所以取最小值:`dp[i][k] = min(dp[i][k], dp[i][j] + 1);`
21 |
22 | 情况二,向下移动:
23 |
24 | 从 (i, j) 再向下走 到 (k, j)。 无论k 多大,步数只加1 :
25 |
26 | `dp[k][j] = dp[i][j] + 1;`
27 |
28 | 同理 `dp[i][k]` 也有可能 从其他方向得到,取最小值:`dp[k][j] = min(dp[k][j], dp[i][j] + 1);`
29 |
30 | 情况三,右下方移动:
31 |
32 | 从 (i, j) 再向右下方移动 到 (i + k, j + k)。 无论k 多大,步数只加1 :
33 |
34 | `dp[i + k][j + k] = dp[i][j] + 1`
35 |
36 | 同理 `dp[i + k][j + k]` 也有可能 从其他方向得到,取最小值:`dp[i + k][j + k] = min(dp[i + k][j + k], dp[i][j] + 1);`
37 |
38 |
39 | ```CPP
40 | #include
41 | #include
42 | using namespace std;
43 | const int INF = 4e6; // 最多步数也就是 2000 * 2000
44 | int main() {
45 | int n, m;
46 | cin >> n >> m;
47 | vector> grid(n, vector(m));
48 | for (int i = 0; i < n; i++) {
49 | for (int j = 0; j < m; j++) {
50 | cin >> grid[i][j];
51 | }
52 | }
53 | vector> dp(n, vector(m, INF));
54 | dp[0][0] = 0;
55 | for (int i = 0; i < n; i++) {
56 | for (int j = 0; j < m; j++) {
57 | if (grid[i][j] == '*') continue;
58 | // 向右移动k个格子
59 | for (int k = j + 1; k < m && grid[i][k] == '.'; k++) {
60 | dp[i][k] = min(dp[i][k], dp[i][j] + 1);
61 | }
62 | // 向下移动 k个格子
63 | for (int k = i + 1; k < n && grid[k][j] == '.'; k++) {
64 | dp[k][j] = min(dp[k][j], dp[i][j] + 1);
65 | }
66 | // 向右下移动k个格子
67 | for (int k = 1; i + k < n && j + k < m && grid[i + k][j + k] == '.'; k++) {
68 | dp[i + k][j + k] = min(dp[i + k][j + k], dp[i][j] + 1);
69 | }
70 | }
71 | }
72 | if (dp[n - 1][m - 1] == INF) cout << -1 << endl;
73 | else cout << dp[n - 1][m - 1] << endl;
74 | }
75 | ```
76 |
77 |
78 |
--------------------------------------------------------------------------------
/problems/kamacoder/0135.获取连通的相邻节点列表.md:
--------------------------------------------------------------------------------
1 |
2 | # 135. 获取连通的相邻节点列表
3 |
4 | 本题是一个 “阅读理解”题,其实题目的算法很简单,但理解题意很费劲。
5 |
6 | 题目描述中的【提示信息】 是我后加上去了,华为笔试的时候没有这个 【提示信息】。
7 |
8 | 相信没有 【提示信息】大家理解题意 平均要多用半个小时。
9 |
10 | 思路:
11 |
12 | 1. 将第一行数据加入set中
13 | 2. 后面输出数据,判断是否在 set里
14 | 3. 最后把结果排个序
15 |
16 |
17 | ```CPP
18 | #include
19 | #include
20 | #include
21 | #include
22 | using namespace std;
23 | int main() {
24 | unordered_set uset;
25 | int n, a;
26 | cin >> n;
27 | while (n--) {
28 | cin >> a;
29 | uset.insert(a);
30 | }
31 | int m, x, vlan_id;
32 | long long tb;
33 | vector vecTB;
34 | cin >> m;
35 | while(m--) {
36 | cin >> tb;
37 | cin >> x;
38 | vector vecVlan_id(x);
39 | for (int i = 0; i < x; i++) {
40 | cin >> vecVlan_id[i];
41 | }
42 | for (int i = 0; i < x; i++) {
43 | if (uset.find(vecVlan_id[i]) != uset.end()) {
44 | vecTB.push_back(tb);
45 | break;
46 | }
47 | }
48 |
49 | }
50 | cout << vecTB.size() << endl;
51 | if (vecTB.size() != 0) {
52 | sort(vecTB.begin(), vecTB.end());
53 | for (int i = 0; i < vecTB.size() ; i++) cout << vecTB[i] << " ";
54 | }
55 | }
56 |
57 | ```
58 |
59 | ## 其他语言版本
60 |
61 | ### Java
62 |
63 | ```Java
64 | import java.util.*;
65 |
66 | public class Main {
67 | public static void main(String[] args) {
68 | Scanner scanner = new Scanner(System.in);
69 | Set uset = new HashSet<>();
70 | int n = scanner.nextInt();
71 | while (n-- > 0) {
72 | int a = scanner.nextInt();
73 | uset.add(a);
74 | }
75 |
76 | int m = scanner.nextInt();
77 | List vecTB = new ArrayList<>();
78 | while (m-- > 0) {
79 | long tb = scanner.nextLong();
80 | int x = scanner.nextInt();
81 | List vecVlan_id = new ArrayList<>();
82 | for (int i = 0; i < x; i++) {
83 | vecVlan_id.add(scanner.nextInt());
84 | }
85 | for (int vlanId : vecVlan_id) {
86 | if (uset.contains(vlanId)) {
87 | vecTB.add(tb);
88 | break;
89 | }
90 | }
91 | }
92 |
93 | System.out.println(vecTB.size());
94 | if (!vecTB.isEmpty()) {
95 | Collections.sort(vecTB);
96 | for (long tb : vecTB) {
97 | System.out.print(tb + " ");
98 | }
99 | }
100 | }
101 | }
102 |
103 | ```
104 |
105 | ### Python
106 |
107 | ```python
108 | def main():
109 | import sys
110 | input = sys.stdin.read
111 | data = input().split()
112 |
113 | index = 0
114 | n = int(data[index])
115 | index += 1
116 | uset = set()
117 | for _ in range(n):
118 | a = int(data[index])
119 | index += 1
120 | uset.add(a)
121 |
122 | m = int(data[index])
123 | index += 1
124 | vecTB = []
125 | while m > 0:
126 | tb = int(data[index])
127 | index += 1
128 | x = int(data[index])
129 | index += 1
130 | vecVlan_id = []
131 | for _ in range(x):
132 | vecVlan_id.append(int(data[index]))
133 | index += 1
134 | for vlan_id in vecVlan_id:
135 | if vlan_id in uset:
136 | vecTB.append(tb)
137 | break
138 | m -= 1
139 |
140 | print(len(vecTB))
141 | if vecTB:
142 | vecTB.sort()
143 | print(" ".join(map(str, vecTB)))
144 |
145 | if __name__ == "__main__":
146 | main()
147 | ```
148 |
--------------------------------------------------------------------------------
/problems/kamacoder/0139.可爱串.md:
--------------------------------------------------------------------------------
1 |
2 | # 可爱串
3 |
4 | 整体思路,就含有 子序列的字符串数量 减去 含有子串的字符串数量。
5 |
6 | 因为子序列数量已经是包含子串数量的。 剩下的就是 只有子序列 且没有子串的 字符串数量。
7 |
8 |
9 | 需要注意我们求的不是 长度为 i 的字符串里有多少个 red 子序列。
10 |
11 | **而是 可以有多少个 长度为i 的字符串 含有子序列 red**
12 |
13 | 同理,可以有多少个长度为i的字符串含有 red 子串
14 |
15 | 认清这一点很重要!
16 |
17 | ### 求子串
18 |
19 | dp2[i][3] 长度为i 且 含有子串 red 的字符串数量 有多少
20 |
21 | dp2[i][2] 长度为i 且 含有子串 re 的字符串数量有多少
22 |
23 | dp2[i][1] 长度为 i 且 含有子串 r 的字符串数量有多少
24 |
25 | dp2[1][0] 长度为 i 且 含有 只有 de, ee , e, d的字符串的字符串数量有多少。
26 |
27 | ```CPP
28 | // 求子串
29 | dp2[0][0] = 1;
30 | for(int i = 1;i <= n; i++) {
31 | dp2[i][0] = (dp2[i - 1][2] + dp2[i - 1][1] + dp2[i - 1][0] * 2) % mod; // 含有 re 的可以把 r改成d, 含有r 的可以改成
32 | dp2[i][1] = (dp2[i - 1][2] + dp2[i - 1][1] + dp2[i - 1][0]) % mod;
33 | dp2[i][2] = (dp2[i - 1][1]);
34 | dp2[i][3] = (dp2[i - 1][3] * 3 + dp2[i - 1][2]) % mod;
35 | }
36 | ``
37 |
38 | ### 求子序列
39 |
40 | dp1[i][3] 长度为i 且 含有子序列 red 的字符串数量 有多少
41 |
42 | dp2[i][2] 长度为i 且 含有子序列 re 的字符串数量有多少
43 |
44 | dp2[i][1] 长度为 i 且 含有子序列 r 的字符串数量有多少
45 |
46 | dp2[1][0] 长度为 i 且 含有 只含有 e 和 d 的字符串的字符串数量有多少。
47 |
48 | ```CPP
49 |
50 | // 求子序列
51 | dp1[0][0]=1;
52 | for(int i=1;i<=n;i++)
53 | {
54 | dp1[i][0] = (dp1[i - 1][0] * 2) % mod;
55 | dp1[i][1] = (dp1[i - 1][0] + dp1[i - 1][1] * 2) % mod;
56 | dp1[i][2] = (dp1[i - 1][1] + dp1[i - 1][2] * 2) % mod;
57 | dp1[i][3] = (dp1[i - 1][2] + dp1[i - 1][3] * 3) % mod;
58 | }
59 | ```
60 |
61 |
62 |
63 | ```CPP
64 |
65 | #include
66 | using namespace std;
67 |
68 | using ll=long long;
69 | const int mod=1e9+7;
70 |
71 | int main()
72 | {
73 | int n;
74 |
75 | cin>>n;
76 | vector> dp1(n + 1,vector (4,0));
77 | vector> dp2(n + 1,vector (4,0));
78 | // 求子串
79 | dp2[0][0] = 1;
80 | for(int i = 1;i <= n; i++) {
81 | dp2[i][0] = (dp2[i - 1][2] + dp2[i - 1][1] + dp2[i - 1][0] * 2) % mod;
82 | dp2[i][1] = (dp2[i - 1][2] + dp2[i - 1][1] + dp2[i - 1][0]) % mod;
83 | dp2[i][2] = (dp2[i - 1][1]);
84 | dp2[i][3] = (dp2[i - 1][3] * 3 + dp2[i - 1][2]) % mod;
85 | }
86 |
87 | // 求子序列
88 | dp1[0][0]=1;
89 | for(int i=1;i<=n;i++)
90 | {
91 | dp1[i][0] = (dp1[i - 1][0] * 2) % mod;
92 | dp1[i][1] = (dp1[i - 1][0] + dp1[i - 1][1] * 2) % mod;
93 | dp1[i][2] = (dp1[i - 1][1] + dp1[i - 1][2] * 2) % mod;
94 | dp1[i][3] = (dp1[i - 1][2] + dp1[i - 1][3] * 3) % mod;
95 | }
96 |
97 | cout<<(dp1[n][3] - dp2[n][3])%mod;
98 |
99 | }
100 |
101 | ```
102 |
--------------------------------------------------------------------------------
/problems/kamacoder/0142.两个字符串的最小ASCII删除总和.md:
--------------------------------------------------------------------------------
1 |
2 | # 142. 两个字符串的最小 ASCII 删除总和
3 |
4 | 本题和[代码随想录:两个字符串的删除操作](https://www.programmercarl.com/0583.%E4%B8%A4%E4%B8%AA%E5%AD%97%E7%AC%A6%E4%B8%B2%E7%9A%84%E5%88%A0%E9%99%A4%E6%93%8D%E4%BD%9C.html) 思路基本是一样的。
5 |
6 | 属于编辑距离问题,如果想彻底了解,建议看看「代码随想录」的编辑距离总结篇。
7 |
8 | 本题dp数组含义:
9 |
10 | dp[i][j] 表示 以i-1为结尾的字符串word1,和以j-1位结尾的字符串word2,想要达到相等,所需要删除元素的最小ASCII 删除总和。
11 |
12 | 如果 s1[i - 1] 与 s2[j - 1] 相同,则不用删:`dp[i][j] = dp[i - 1][j - 1]`
13 |
14 | 如果 s1[i - 1] 与 s2[j - 1] 不相同,删word1 的 最小删除和: `dp[i - 1][j] + s1[i - 1]` ,删word2的最小删除和: `dp[i][j - 1] + s2[j - 1]`
15 |
16 | 取最小值: `dp[i][j] = min(dp[i - 1][j] + s1[i - 1], dp[i][j - 1] + s2[j - 1])`
17 |
18 |
19 |
20 | ```CPP
21 | #include
22 | #include
23 | using namespace std;
24 | int main() {
25 | string s1, s2;
26 | cin >> s1 >> s2;
27 | vector> dp(s1.size() + 1, vector(s2.size() + 1, 0));
28 |
29 | // s1 如果变成空串的最小删除ASCLL值综合
30 | for (int i = 1; i <= s1.size(); i++) dp[i][0] = dp[i - 1][0] + s1[i - 1];
31 | // s2 如果变成空串的最小删除ASCLL值综合
32 | for (int j = 1; j <= s2.size(); j++) dp[0][j] = dp[0][j - 1] + s2[j - 1];
33 |
34 | for (int i = 1; i <= s1.size(); i++) {
35 | for (int j = 1; j <= s2.size(); j++) {
36 | if (s1[i - 1] == s2[j - 1]) dp[i][j] = dp[i - 1][j - 1];
37 | else dp[i][j] = min(dp[i - 1][j] + s1[i - 1], dp[i][j - 1] + s2[j - 1]);
38 | }
39 | }
40 | cout << dp[s1.size()][s2.size()] << endl;
41 | }
42 | ```
43 |
44 | ### Java
45 |
46 | ```Java
47 | import java.util.Scanner;
48 |
49 | public class Main {
50 | public static void main(String[] args) {
51 | Scanner scanner = new Scanner(System.in);
52 | String s1 = scanner.nextLine();
53 | String s2 = scanner.nextLine();
54 | int[][] dp = new int[s1.length() + 1][s2.length() + 1];
55 |
56 | // s1 如果变成空串的最小删除ASCII值综合
57 | for (int i = 1; i <= s1.length(); i++) {
58 | dp[i][0] = dp[i - 1][0] + s1.charAt(i - 1);
59 | }
60 | // s2 如果变成空串的最小删除ASCII值综合
61 | for (int j = 1; j <= s2.length(); j++) {
62 | dp[0][j] = dp[0][j - 1] + s2.charAt(j - 1);
63 | }
64 |
65 | for (int i = 1; i <= s1.length(); i++) {
66 | for (int j = 1; j <= s2.length(); j++) {
67 | if (s1.charAt(i - 1) == s2.charAt(j - 1)) {
68 | dp[i][j] = dp[i - 1][j - 1];
69 | } else {
70 | dp[i][j] = Math.min(dp[i - 1][j] + s1.charAt(i - 1), dp[i][j - 1] + s2.charAt(j - 1));
71 | }
72 | }
73 | }
74 | System.out.println(dp[s1.length()][s2.length()]);
75 | scanner.close();
76 | }
77 | }
78 |
79 |
80 | ```
81 |
82 | ### python
83 |
84 | ```python
85 | def min_delete_sum(s1: str, s2: str) -> int:
86 | dp = [[0] * (len(s2) + 1) for _ in range(len(s1) + 1)]
87 |
88 | # s1 如果变成空串的最小删除ASCII值综合
89 | for i in range(1, len(s1) + 1):
90 | dp[i][0] = dp[i - 1][0] + ord(s1[i - 1])
91 | # s2 如果变成空串的最小删除ASCII值综合
92 | for j in range(1, len(s2) + 1):
93 | dp[0][j] = dp[0][j - 1] + ord(s2[j - 1])
94 |
95 | for i in range(1, len(s1) + 1):
96 | for j in range(1, len(s2) + 1):
97 | if s1[i - 1] == s2[j - 1]:
98 | dp[i][j] = dp[i - 1][j - 1]
99 | else:
100 | dp[i][j] = min(dp[i - 1][j] + ord(s1[i - 1]), dp[i][j - 1] + ord(s2[j - 1]))
101 |
102 | return dp[len(s1)][len(s2)]
103 |
104 | if __name__ == "__main__":
105 | s1 = input().strip()
106 | s2 = input().strip()
107 | print(min_delete_sum(s1, s2))
108 | ```
109 |
--------------------------------------------------------------------------------
/problems/kamacoder/0144.字典序最小的01字符串.md:
--------------------------------------------------------------------------------
1 |
2 | # 0144.字典序最小的01字符串
3 |
4 | 贪心思路:移动尽可能 移动前面的1 ,这样可以是 字典序最小
5 |
6 | 从前到后遍历,遇到 0 ,就用前面的 1 来交换
7 |
8 | ```CPP
9 | #include
10 | #include
11 | using namespace std;
12 | int main() {
13 | int n,k;
14 | cin >> n >> k;
15 | string s;
16 | cin >> s;
17 | for(int i = 0; i < n && k > 0; i++) {
18 | if(s[i] == '0') {
19 | // 开始用前面的 1 来交换
20 | int j = i;
21 | while(j > 0 && s[j - 1] == '1' && k > 0) {
22 | swap(s[j], s[j - 1]);
23 | --j;
24 | --k;
25 | }
26 | }
27 | }
28 | cout << s << endl;
29 | return 0;
30 | }
31 |
32 | ```
33 |
34 | Java:
35 |
36 | ```Java
37 |
38 | import java.util.*;
39 |
40 | public class Main {
41 | public static void main(String[] args) {
42 | Scanner scanner = new Scanner(System.in);
43 | int n = scanner.nextInt();
44 | int k = scanner.nextInt();
45 | scanner.nextLine(); // 消耗掉换行符
46 | String s = scanner.nextLine();
47 | char[] ch = s.toCharArray();
48 |
49 | for (int i = 0; i < n && k > 0; i++) {
50 | if (ch[i] == '0') {
51 | // 开始用前面的 1 来交换
52 | int j = i;
53 | while (j > 0 && ch[j - 1] == '1' && k > 0) {
54 | char tmp = ch[j];
55 | ch[j] = ch[j - 1];
56 | ch[j - 1] = tmp;
57 | j--;
58 | k--;
59 | }
60 | }
61 | }
62 |
63 | System.out.println(new String(ch));
64 | }
65 | }
66 | ```
67 |
--------------------------------------------------------------------------------
/problems/kamacoder/0145.数组子序列的排列.md:
--------------------------------------------------------------------------------
1 |
2 | # 145. 数组子序列的排列
3 |
4 | 每个元素出现的次数相乘就可以了。
5 |
6 | 注意 “长度为 m 的数组,1 到 m 每个元素都出现过,且恰好出现 1 次。” ,题目中有n个元素,所以我们要统计的就是 1 到 n 元素出现的个数。
7 |
8 | 因为如果有一个元素x 大于n了, 那不可能出现 长度为x的数组 且 1 到 x 每个元素都出现过。
9 |
10 | ```CPP
11 | #include "bits/stdc++.h"
12 | using namespace std;
13 | int main(){
14 | int n;
15 | int x;
16 | cin >> n;
17 | unordered_map umap;
18 | for(int i = 0; i < n; ++i){
19 | cin >> x;
20 | if(umap.find(x) != umap.end()) umap[x]++;
21 | else umap[x] = 1;
22 | }
23 | long long res = 0;
24 | long long num = 1;
25 | for (int i = 1; i <= n; i++) {
26 | if (umap.find(i) == umap.end()) break; // 如果i都没出现,后面得数也不能 1 到 m 每个元素都出现过
27 | num = (num * umap[i]) % 1000000007;
28 | res += num;
29 | res %= 1000000007;
30 | }
31 | cout << res << endl;
32 | }
33 |
34 | ```
35 |
36 | ```Java
37 |
38 | import java.util.HashMap;
39 | import java.util.Map;
40 | import java.util.Scanner;
41 |
42 | public class Main {
43 | public static void main(String[] args) {
44 | Scanner sc = new Scanner(System.in);
45 | int n = sc.nextInt();
46 | Map map = new HashMap<>();
47 | for (int i = 0; i < n; i++) {
48 | int x = sc.nextInt();
49 | map.put(x, map.getOrDefault(x, 0) + 1);
50 | }
51 | long res = 0;
52 | long num = 1;
53 | for (int i = 1; i <= n; i++) {
54 | if (!map.containsKey(i)) break; // 如果i都没出现,后面得数也不能1到m每个元素都出现过
55 | num = (num * map.get(i)) % 1000000007;
56 | res += num;
57 | res %= 1000000007;
58 | }
59 | System.out.println(res);
60 | sc.close();
61 | }
62 | }
63 |
64 | ```
65 |
66 |
67 | ```python
68 | def main():
69 | import sys
70 | input = sys.stdin.read
71 | data = input().split()
72 |
73 | n = int(data[0])
74 | umap = {}
75 |
76 | for i in range(1, n + 1):
77 | x = int(data[i])
78 | if x in umap:
79 | umap[x] += 1
80 | else:
81 | umap[x] = 1
82 |
83 | res = 0
84 | num = 1
85 | MOD = 1000000007
86 |
87 | for i in range(1, n + 1):
88 | if i not in umap:
89 | break # 如果i都没出现,后面得数也不能1到m每个元素都出现过
90 | num = (num * umap[i]) % MOD
91 | res = (res + num) % MOD
92 |
93 | print(res)
94 |
95 | if __name__ == "__main__":
96 | main()
97 |
98 | ```
99 |
--------------------------------------------------------------------------------
/problems/kamacoder/0146.传送树.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # 146. 传送树
6 |
7 | 本题题意是比较绕的,我后面给补上了 【提示信息】对 题目输出样例讲解一下,相对会容易理解的多。
8 |
9 | ```CPP
10 | #include
11 | #include
12 | #include
13 | using namespace std;
14 |
15 | vector> edge; // 邻接表来存图
16 | vector nxt;
17 | int n;
18 |
19 | /*
20 | * 递归函数,用于找到每个节点的下一个传送门节点,并记录在nxt数组中。
21 | * 遍历当前节点的所有子节点,递归调用findNext以确保子节点的nxt值已经计算出来。
22 | * 更新当前节点的nxt值为其子节点中编号最小的节点。
23 | * 如果当前节点是叶子节点(即没有子节点),则将其nxt值设置为自身。
24 | */
25 | void findNext(int node) {
26 | for (int v : edge[node]) {
27 | findNext(v);
28 | if (nxt[node] == -1 || nxt[node] > min(v, nxt[v])) {
29 | nxt[node] = min(v, nxt[v]);
30 | }
31 | }
32 |
33 | // 叶子节点
34 | if (nxt[node] == -1) {
35 | nxt[node] = node;
36 | }
37 | }
38 |
39 | // 计算从节点u出发经过若干次传送门到达叶子节点所需的步数。
40 | // 通过不断访问nxt节点,直到到达叶子节点,记录访问的节点数。
41 | int get(int u) {
42 | int cnt = 1;
43 | while (nxt[u] != u) {
44 | cnt++;
45 | u = nxt[u];
46 | }
47 | return cnt;
48 | }
49 |
50 | int main() {
51 | cin >> n;
52 | edge.resize(n + 1);
53 | nxt.resize(n + 1, -1);
54 | for (int i = 1; i <= n; ++i) {
55 | int a, b;
56 | cin >> a >> b;
57 | edge[a].push_back(b);
58 | }
59 | findNext(1);
60 | for (int i = 1; i <= n; ++i) {
61 | cout << get(i) << ' ';
62 | }
63 | }
64 |
65 | ```
66 |
--------------------------------------------------------------------------------
/problems/kamacoder/0147.三珠互斥.md:
--------------------------------------------------------------------------------
1 |
2 | # 三珠互斥
3 |
4 | 1. 如果k * 3 大于 n 了,那说明一定没结果,如果没想明白,大家举个例子试试看
5 | 2. 分别求出三个红珠子之间的距离
6 | 3. 对这三段距离从小到大排序 y1, y2, y3
7 | 4. 如果第一段距离y1 小于k,说明需要交换 k - y 次, 同理 第二段距离y2 小于k,说明需要交换 k - y2 次
8 | 5. y1 y2 都调整好了,不用计算y3,因为 y3是距离最大
9 |
10 | ```CPP
11 | #include
12 | using namespace std;
13 |
14 | int main(){
15 | int t;
16 | cin >> t;
17 | int n, k, a1, a2, a3;
18 | vector dis(3);
19 |
20 | while (t--) {
21 | cin >> n >> k >> a1 >> a2 >> a3;
22 | if(k * 3 > n){
23 | cout << -1 << endl;
24 | continue;
25 | }
26 | dis[0] = min(abs(a1 - a2), n - abs(a1 - a2));
27 | dis[1] = min(abs(a1 - a3), n - abs(a1 - a3));
28 | dis[2] = min(abs(a3 - a2), n - abs(a3 - a2));
29 |
30 | sort(dis.begin(), dis.end());
31 |
32 | int result = 0;
33 | if (dis[0] < k) result += (k - dis[0]);
34 | if (dis[1] < k) result += (k - dis[1]);
35 |
36 | cout << result << endl;
37 | }
38 | return 0;
39 | }
40 | ```
41 |
42 | Java代码:
43 |
44 | ```Java
45 | import java.util.*;
46 |
47 | public class Main {
48 | public static void main(String[] args) {
49 | Scanner scanner = new Scanner(System.in);
50 | int t = scanner.nextInt();
51 |
52 | while (t-- > 0) {
53 | int n = scanner.nextInt();
54 | int k = scanner.nextInt();
55 | int a1 = scanner.nextInt();
56 | int a2 = scanner.nextInt();
57 | int a3 = scanner.nextInt();
58 | if (k * 3 > n) {
59 | System.out.println(-1);
60 | continue;
61 | }
62 |
63 | List dis = new ArrayList<>(3);
64 | dis.add(Math.min(Math.abs(a1 - a2), n - Math.abs(a1 - a2)));
65 | dis.add(Math.min(Math.abs(a1 - a3), n - Math.abs(a1 - a3)));
66 | dis.add(Math.min(Math.abs(a3 - a2), n - Math.abs(a3 - a2)));
67 |
68 | Collections.sort(dis);
69 |
70 | int result = 0;
71 | if (dis.get(0) < k) result += (k - dis.get(0));
72 | if (dis.get(1) < k) result += (k - dis.get(1));
73 |
74 | System.out.println(result);
75 | }
76 | }
77 | }
78 | ```
79 |
--------------------------------------------------------------------------------
/problems/kamacoder/0148.扑克牌同花顺.md:
--------------------------------------------------------------------------------
1 |
2 | # 扑克牌同花顺
3 |
4 | 首先我们要定义一个结构体,来存放我们的数据
5 |
6 | `map<花色,{同一花色牌集合,同一花色的牌对应的牌数量}>`
7 |
8 | 再遍历 每一个花色下,每一个牌 的数量
9 |
10 | 代码如下详细注释:
11 |
12 |
13 | ```CPP
14 | #include
15 | using namespace std;
16 |
17 | string cards[] = {"H","S","D","C"};
18 | typedef long long ll;
19 | struct color
20 | {
21 | set st; // 同一花色 牌的集合
22 | map cnt; // 同一花色 牌对应的数量
23 | };
24 | unordered_map umap;
25 |
26 | int main() {
27 | int n;
28 | cin >> n;
29 | for (int i = 0; i < n; i++) {
30 | int x, y;
31 | string card;
32 | cin >> x >> y >> card;
33 | umap[card].st.insert(x);
34 | umap[card].cnt[x] += y;
35 | }
36 | ll sum = 0;
37 | // 遍历每一个花色
38 | for (string cardOne : cards) {
39 | color colorOne = umap[cardOne];
40 | // 遍历 同花色 每一个牌
41 | for (int number : colorOne.st) {
42 | ll numberCount = colorOne.cnt[number]; // 获取牌为number的数量是 numberCount
43 |
44 | // 统计 number 到 number + 4 都是否有牌,用cal 把 number 到number+4 的数量记下来
45 | ll cal = numberCount;
46 | for (int j = number + 1; j <= number + 4; j++) cal = min(cal, colorOne.cnt[j]);
47 | // 统计结果
48 | sum += cal;
49 | // 把统计过的同花顺数量减下去
50 | for (int j = number + 1; j <= number + 4; j++) colorOne.cnt[j] -= cal;
51 | }
52 | }
53 | cout << sum << endl;
54 | }
55 | ```
56 |
57 | Java代码如下:
58 |
59 | ```Java
60 |
61 | import java.util.*;
62 |
63 | public class Main {
64 | static String[] cards = {"H", "S", "D", "C"}; // 花色数组
65 |
66 | static class Color {
67 | Set st; // 同一花色牌的集合
68 | Map cnt; // 同一花色牌对应的数量
69 |
70 | Color() {
71 | st = new HashSet<>(); // 初始化集合
72 | cnt = new HashMap<>(); // 初始化映射
73 | }
74 | }
75 |
76 | static Map umap = new HashMap<>(); // 用于存储每种花色对应的Color对象
77 |
78 | public static void main(String[] args) {
79 | Scanner scanner = new Scanner(System.in);
80 | int n = scanner.nextInt(); // 读取牌的数量
81 |
82 | for (int i = 0; i < n; i++) {
83 | int x = scanner.nextInt(); // 读取牌的值
84 | int y = scanner.nextInt(); // 读取牌的数量
85 | String card = scanner.next(); // 读取牌的花色
86 |
87 | umap.putIfAbsent(card, new Color()); // 如果不存在该花色,则创建一个新的Color对象
88 | umap.get(card).st.add(x); // 将牌的值加入集合
89 | umap.get(card).cnt.put(x, umap.get(card).cnt.getOrDefault(x, 0L) + y); // 更新牌的数量
90 | }
91 |
92 | long sum = 0; // 结果累加器
93 |
94 | // 遍历每一种花色
95 | for (String cardOne : cards) {
96 | Color colorOne = umap.getOrDefault(cardOne, new Color()); // 获取对应花色的Color对象
97 |
98 | // 遍历同花色的每一张牌
99 | for (int number : colorOne.st) {
100 | long numberCount = colorOne.cnt.get(number); // 获取当前牌的数量
101 |
102 | // 计算从当前牌到number+4的最小数量
103 | long cal = numberCount;
104 | for (int j = number + 1; j <= number + 4; j++) {
105 | cal = Math.min(cal, colorOne.cnt.getOrDefault(j, 0L)); // 更新cal为最小值
106 | }
107 |
108 | // 将结果累加到sum
109 | sum += cal;
110 |
111 | // 将统计过的同花顺数量减去
112 | for (int j = number + 1; j <= number + 4; j++) {
113 | colorOne.cnt.put(j, colorOne.cnt.getOrDefault(j, 0L) - cal);
114 | }
115 | }
116 | }
117 |
118 | System.out.println(sum); // 输出结果
119 | }
120 | }
121 |
122 | ```
123 |
--------------------------------------------------------------------------------
/problems/kamacoder/0149.好数组.md:
--------------------------------------------------------------------------------
1 |
2 | # 149. 好数组
3 |
4 | 贪心思路:
5 |
6 | 整体思路是移动到中间位置(中位数),一定是 移动次数最小的。
7 |
8 | 有一个数可以不改变,对数组排序之后, 最小数 和 最大数 一定是移动次数最多的,所以分别保留最小 和 最大的不变。
9 |
10 | 中间可能有两个位置,所以要计算中间偏前 和 中间偏后的
11 |
12 | 代码如下:
13 |
14 | ```CPP
15 | #include
16 | using namespace std;
17 |
18 | int main() {
19 | int n;
20 | cin >> n;
21 | vector arr(n);
22 | for (int i = 0; i < n; ++i) {
23 | cin >> arr[i];
24 | }
25 | sort(arr.begin(), arr.end());
26 |
27 | if (arr[0] == arr[n - 1]) {
28 | cout << 1 << endl;
29 | return 0;
30 | }
31 | long cnt = 0L;
32 | long cnt1 = 0L;
33 |
34 | // 如果要保留一个不改变,要不不改最小的,要不不改最大的。
35 |
36 | // 取中间偏前的位置
37 | long mid = arr[(n - 2) / 2];
38 |
39 | // 不改最大的
40 | for (int i = 0; i < n - 1; i++) {
41 | cnt += abs(arr[i] - mid);
42 | }
43 |
44 | // 取中间偏后的位置
45 | mid = arr[n / 2];
46 |
47 | // 不改最小的
48 | for (int i = 1; i < n; i++) {
49 | cnt1 += abs(arr[i] - mid);
50 | }
51 |
52 | cout << min(cnt, cnt1) << endl;
53 | return 0;
54 | }
55 | ```
56 |
57 | Java代码如下:
58 |
59 | ```Java
60 |
61 | import java.util.*;
62 |
63 | public class Main {
64 | public static void main(String[] args) {
65 | Scanner scanner = new Scanner(System.in);
66 | int n = scanner.nextInt();
67 | long[] arr = new long[n];
68 | for (int i = 0; i < n; ++i) {
69 | arr[i] = scanner.nextLong();
70 | }
71 | Arrays.sort(arr);
72 |
73 | if (arr[0] == arr[n - 1]) {
74 | System.out.println(1);
75 | return;
76 | }
77 | long cnt = 0L;
78 | long cnt1 = 0L;
79 |
80 | // 如果要保留一个不改变,要不不改最小的,要不不改最大的。
81 |
82 | // 取中间偏前的位置
83 | long mid = arr[(n - 2) / 2];
84 |
85 | // 不改最大的
86 | for (int i = 0; i < n - 1; i++) {
87 | cnt += Math.abs(arr[i] - mid);
88 | }
89 |
90 | // 取中间偏后的位置
91 | mid = arr[n / 2];
92 |
93 | // 不改最小的
94 | for (int i = 1; i < n; i++) {
95 | cnt1 += Math.abs(arr[i] - mid);
96 | }
97 |
98 | System.out.println(Math.min(cnt, cnt1));
99 | }
100 | }
101 |
102 | ```
103 |
--------------------------------------------------------------------------------
/problems/kamacoder/0150.极长连续段的权值.md:
--------------------------------------------------------------------------------
1 |
2 | # 150. 极长连续段的权值
3 |
4 | 动态规划,枚举最后边节点的情况:
5 |
6 | ```CPP
7 | #include
8 | #include
9 | using namespace std;
10 |
11 | int main() {
12 | int n;
13 | cin >> n;
14 | string s;
15 | cin >> s;
16 |
17 | long long result = 1;
18 | long long a = 1;
19 |
20 | for (int i = 1; i < n; ++i) {
21 | // 加上本身长度为1的子串
22 | if (s[i] == s[i - 1]) {
23 | a += 1;
24 | result += a;
25 | // 以最右节点为终点,每个子串的级长连续段都+1,再加本身长度为1的子串
26 | } else {
27 | a = a + i + 1;
28 | result += a;
29 | }
30 | }
31 | cout << result << endl;
32 | return 0;
33 | }
34 | ```
35 |
36 | Java代码如下:
37 |
38 | ```Java
39 | import java.util.Scanner;
40 |
41 | public class Main {
42 | public static void main(String[] args) {
43 | Scanner scanner = new Scanner(System.in);
44 | int n = scanner.nextInt();
45 | String s = scanner.next();
46 |
47 | long result = 1;
48 | long a = 1;
49 |
50 | for (int i = 1; i < n; ++i) {
51 | // 加上本身长度为1的子串
52 | if (s.charAt(i) == s.charAt(i - 1)) {
53 | a += 1;
54 | result += a;
55 | // 以最右节点为终点,每个子串的级长连续段都+1,再加本身长度为1的子串
56 | } else {
57 | a = a + i + 1;
58 | result += a;
59 | }
60 | }
61 |
62 | System.out.println(result);
63 | }
64 | }
65 |
66 | ```
67 |
--------------------------------------------------------------------------------
/problems/kamacoder/0151.手机流畅运行的秘密.md:
--------------------------------------------------------------------------------
1 | # 151. 手机流畅运行的秘密
2 |
3 | [题目链接](https://kamacoder.com/problempage.php?pid=1229)
4 |
5 | 先运行 能留下电量多的 任务,才能有余电运行其他任务。
6 |
7 | 任务1,1:10 ,运行完 能留下 9个电
8 |
9 | 任务2,2:12,运行完 能留下 10个电
10 |
11 | 任务3,3:10,运行完 能留下 7个电。
12 |
13 | 运行顺序: 任务2 -> 任务1 -> 任务3
14 |
15 | 按照 最低初始电量 - 耗电量,从大到小排序。
16 |
17 | 计算总电量,需要 从小到大 遍历, 不断取 总电量 + 任务耗电量 与 任务最低初始电量 的最大值。
18 |
19 | ```CPP
20 | #include
21 | using namespace std;
22 |
23 | bool cmp(const pair& taskA, const pair& taskB) {
24 | return (taskA.second - taskA.first) < (taskB.second - taskB.first);
25 | }
26 | int main() {
27 | string str, tmp;
28 | vector> tasks;
29 |
30 | //处理输入
31 | getline(cin, str);
32 | stringstream ss(str);
33 | while (getline(ss, tmp, ',')) {
34 | int p = tmp.find(":");
35 | string a = tmp.substr(0, p);
36 | string b = tmp.substr(p + 1);
37 | tasks.push_back({stoi(a), stoi(b)});
38 | }
39 |
40 | // 按照差值从小到大排序
41 | sort(tasks.begin(), tasks.end(), cmp);
42 |
43 | // 收集结果
44 | int result = 0;
45 | for (int i = 0 ; i < tasks.size(); i++) {
46 | result = max(result + tasks[i].first, tasks[i].second);
47 | }
48 |
49 | result = result <= 4800 ? result : -1;
50 | cout << result << endl;
51 |
52 | }
53 | ```
54 |
55 | Java版本:
56 |
57 | ```Java
58 | import java.util.*;
59 | import java.util.stream.Collectors;
60 |
61 | public class Main {
62 | public static void main(String[] args) {
63 | Scanner sc = new Scanner(System.in);
64 | String str = sc.nextLine();
65 | String[] tasksArray = str.split(",");
66 | List tasks = Arrays.stream(tasksArray)
67 | .map(task -> {
68 | String[] parts = task.split(":");
69 | return new Pair(Integer.parseInt(parts[0]), Integer.parseInt(parts[1]));
70 | })
71 | .collect(Collectors.toList());
72 |
73 | // 按照差值从小到大排序
74 | Collections.sort(tasks, (taskA, taskB) ->
75 | (taskA.second - taskA.first) - (taskB.second - taskB.first)
76 | );
77 |
78 | // 收集结果
79 | int result = 0;
80 | for (Pair task : tasks) {
81 | result = Math.max(result + task.first, task.second);
82 | }
83 |
84 | result = result <= 4800 ? result : -1;
85 | System.out.println(result);
86 | }
87 | }
88 |
89 | class Pair {
90 | int first;
91 | int second;
92 |
93 | Pair(int first, int second) {
94 | this.first = first;
95 | this.second = second;
96 | }
97 | }
98 |
99 | ```
100 |
101 | Python版本:
102 |
103 | ```python
104 | def main():
105 | import sys
106 | input = sys.stdin.read
107 |
108 | str = input().strip()
109 | tasks = []
110 | for tmp in str.split(','):
111 | a, b = map(int, tmp.split(':'))
112 | tasks.append((a, b))
113 |
114 | # 按照差值从小到大排序
115 | tasks.sort(key=lambda task: task[1] - task[0])
116 |
117 | # 收集结果
118 | result = 0
119 | for task in tasks:
120 | result = max(result + task[0], task[1])
121 |
122 | result = result if result <= 4800 else -1
123 | print(result)
124 |
125 | if __name__ == "__main__":
126 | main()
127 | ```
128 |
--------------------------------------------------------------------------------
/problems/kamacoder/0152.小米手机通信校准.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # 152. 小米手机通信校准
4 |
5 | [题目链接](https://kamacoder.com/problempage.php?pid=1230)
6 |
7 | 一道模拟题,但比较考察 代码能力。
8 |
9 | 遍历去找 里 freq 最近的 freg就好, 需要记录刚遍历过的的freg和 loss,因为可能有 相邻一样的 freg。
10 |
11 | ```CPP
12 | #include
13 | using namespace std;
14 |
15 | int main() {
16 | int freq;
17 | cin >> freq;
18 | string data;
19 | double result = 0;
20 | int last_freg = 0; // 记录上一个 freg
21 | int last_loss = 0; // 记录上一个loss
22 | while(cin >> data) {
23 | int index = data.find(':');
24 | int freg = stoi(data.substr(0, index)); // 获取 freg 和 loss
25 | int loss = stoi(data.substr(index + 1));
26 | // 两遍一样
27 | if(abs(freg - freq) == abs(last_freg - freq)) {
28 | result = (double)(last_loss + loss)/2.0;
29 | } // 否则更新最新的result
30 | else if(abs(freg - freq) < abs(last_freg - freq)){
31 | result = (double)loss;
32 | }
33 | last_freg = freg;
34 | last_loss = loss;
35 | }
36 | printf("%.1lf\n", result);
37 | return 0;
38 | }
39 |
40 | ```
41 |
42 | Java 版本:
43 |
44 | ```Java
45 |
46 | import java.util.Scanner;
47 |
48 | public class Main {
49 | public static void main(String[] args) {
50 | Scanner sc = new Scanner(System.in);
51 | int freq = sc.nextInt();
52 | sc.nextLine(); // 读取换行符
53 |
54 | String inputLine = sc.nextLine(); // 读取包含所有后续输入的行
55 | String[] data = inputLine.split(" "); // 根据空格分割输入
56 |
57 | double result = 0;
58 | int lastFreq = 0; // 记录上一个 freg
59 | int lastLoss = 0; // 记录上一个 loss
60 |
61 | for (String entry : data) {
62 | int index = entry.indexOf(':');
63 | int freg = Integer.parseInt(entry.substring(0, index)); // 获取 freg 和 loss
64 | int loss = Integer.parseInt(entry.substring(index + 1));
65 |
66 | // 两遍一样
67 | if (Math.abs(freg - freq) == Math.abs(lastFreq - freq)) {
68 | result = (double) (lastLoss + loss) / 2.0;
69 | }
70 | // 否则更新最新的 result
71 | else if (Math.abs(freg - freq) < Math.abs(lastFreq - freq)) {
72 | result = (double) loss;
73 | }
74 |
75 | lastFreq = freg;
76 | lastLoss = loss;
77 | }
78 |
79 | System.out.printf("%.1f\n", result);
80 | sc.close();
81 | }
82 | }
83 |
84 | ```
85 |
86 | Python版本:
87 |
88 | ```python
89 | def main():
90 | import sys
91 | input = sys.stdin.read
92 | data = input().split()
93 |
94 | freq = int(data[0])
95 | result = 0
96 | last_freg = 0 # 记录上一个 freg
97 | last_loss = 0 # 记录上一个 loss
98 |
99 | for i in range(1, len(data)):
100 | item = data[i]
101 | index = item.find(':')
102 | freg = int(item[:index]) # 获取 freg 和 loss
103 | loss = int(item[index + 1:])
104 |
105 | # 两遍一样
106 | if abs(freg - freq) == abs(last_freg - freq):
107 | result = (last_loss + loss) / 2.0
108 | # 否则更新最新的 result
109 | elif abs(freg - freq) < abs(last_freg - freq):
110 | result = loss
111 |
112 | last_freg = freg
113 | last_loss = loss
114 |
115 | print(f"{result:.1f}")
116 |
117 | if __name__ == "__main__":
118 | main()
119 |
120 |
121 | ```
122 |
--------------------------------------------------------------------------------
/problems/kamacoder/图论为什么用ACM模式.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | # 图论为什么统一使用ACM模式
5 |
6 | 代码随想录图论章节给大家统一换成ACM输入输出模式。
7 |
8 | 图论是在笔试还有面试中,通常都是以ACM模式来考察大家,而大家习惯在力扣刷题(核心代码模式),核心代码模式对图的存储和输出都隐藏了。
9 |
10 | 而图论题目的输出输出 相对其他类型题目来说是最难处理的。
11 |
12 | ACM模式是最考察候选人对代码细节把控程度的, 图的构成,图的输出,这些只有ACM输入输出模式才能体现出来。
13 |
14 | ### 输入的细节
15 |
16 | 图论的输入难在 图的存储结构,**如果没有练习过 邻接表和邻接矩阵 ,很多录友是写不出来的**。
17 |
18 | 而力扣上是直接给好现成的 数据结构,可以直接用,所以练习不到图的输入,也练习不到邻接表和邻接矩阵。
19 |
20 | **如果连邻接表 和 邻接矩阵都不知道或者写不出来的话,可以说 图论没有入门**。
21 |
22 | 举个例子,对于力扣 [797.所有可能的路径](https://leetcode.cn/problems/all-paths-from-source-to-target/description/) ,录友了解深度优先搜索之后,这道题目就是模板题,是送分题。
23 |
24 | 如果面试的时候出一道原题 (笔试都是ACM模式,部分面试也是ACM模式),不少熟练刷力扣的录友都难住了,**因为不知道图应该怎么存,也不知道自己存的图如何去遍历**。
25 |
26 | 即使面试的时候,有的面试官,让你用核心代码模式做题,当你写出代码后,**面试官补充一句:这个图 你是怎么存的**?
27 |
28 | 难道和面试官说:我只知道图的算法,但我不知道图怎么存。
29 |
30 | 后面大家在刷 代码随想录图论第一题[98. 所有可达路径](./0098.所有可达路径.md) 的时候,就可以感受到图存储的难点所在。
31 |
32 | 所以这也是为什么我要让大家练习 ACM模式,也是我为什么 在代码随想录图论讲解中,不惜自己亲自出题,让大家统一练习ACM模式。
33 |
34 | ### 输出的细节
35 |
36 | 同样,图论的输出也有细节,例如 求节点1 到节点5的所有路径, 输出可能是:
37 |
38 | ```
39 | 1 2 4 5
40 | 1 3 5
41 | ```
42 |
43 | 表示有两条路可以到节点5, 那储存这个结果需要二维数组,最后在一起输出,力扣是直接return数组就好了,但 ACM模式要求我们自己输出,这里有就细节了。
44 |
45 | 就拿 只输出一行数据,输出 `1 2 4 5` 来说,
46 |
47 | 很多录友代码可能直接就这么写了:
48 |
49 | ```CPP
50 | for (int i = 0 ; i < result.size(); i++) {
51 | cout << result[i] << " ";
52 | }
53 | ```
54 |
55 | 这么写输出的结果是 `1 2 4 5 `, 发现结果是对的,一提交,发现OJ返回 格式错误 或者 结果错误。
56 |
57 | 如果没练习过这种输出方式的录友,就开始怀疑了,这结果一样一样的,怎么就不对,我在力扣上提交都是对的!
58 |
59 | **大家要注意,5 后面要不要有空格**!
60 |
61 | 上面这段代码输出,5后面是加上了空格了,如果判题机判断 结果的长度,标准答案`1 2 4 5`长度是7,而上面代码输出的长度是 8,很明显就是不对的。
62 |
63 | 所以正确的写法应该是:
64 |
65 | ```CPP
66 | for (int i = 0 ; i < result.size() - 1; i++) {
67 | cout << result[i] << " ";
68 | }
69 | cout << result[result.size() - 1];
70 | ```
71 |
72 | 这么写,最后一个元素后面就没有空格了。
73 |
74 | 这是很多录友经常疏忽的,也是大家刷习惯了 力扣(核心代码模式)根本不会注意到的细节。
75 |
76 | **同样在工程开发中,这些细节都是影响系统稳定运行的因素之一**。
77 |
78 | **ACM模式 除了考验算法思路,也考验 大家对 代码的把控力度**, 而 核心代码模式 只注重算法的解题思路,所以输入输出这些就省略掉了。
79 |
80 |
81 | ### 其他
82 |
83 | **大家如果熟练ACM模式,那么核心代码模式没问题,但反过来就不一定了**。
84 |
85 | 而且我在讲解图论的时候,最头疼的就是找题,在力扣上 找题总是找不到符合思路且来完整表达算法精髓的题目。
86 |
87 | 特别是最短路算法相关的题目,例如 Bellman_ford系列 ,Floyd ,A * 等等总是找不到符合思路的题目。
88 |
89 | 索性统一我自己来出题,这其中也是巨大的工作量。为了给大家带来极致的学习体验,我在很多细节上都下了功夫。
90 |
91 | 等大家将图论刷完,就会感受到我的良苦用心。加油
92 |
93 |
94 |
--------------------------------------------------------------------------------
/problems/kamacoder/图论广搜理论基础.md:
--------------------------------------------------------------------------------
1 | # 广度优先搜索理论基础
2 |
3 | 在[深度优先搜索](./图论深搜理论基础.md)的讲解中,我们就讲过深度优先搜索和广度优先搜索的区别。
4 |
5 | 广搜(bfs)是一圈一圈的搜索过程,和深搜(dfs)是一条路跑到黑然后再回溯。
6 |
7 | ## 广搜的使用场景
8 |
9 | 广搜的搜索方式就适合于解决两个点之间的最短路径问题。
10 |
11 | 因为广搜是从起点出发,以起始点为中心一圈一圈进行搜索,一旦遇到终点,记录之前走过的节点就是一条最短路。
12 |
13 | 当然,也有一些问题是广搜 和 深搜都可以解决的,例如岛屿问题,**这类问题的特征就是不涉及具体的遍历方式,只要能把相邻且相同属性的节点标记上就行**。 (我们会在具体题目讲解中详细来说)
14 |
15 | ## 广搜的过程
16 |
17 | 上面我们提过,BFS是一圈一圈的搜索过程,但具体是怎么一圈一圈来搜呢。
18 |
19 | 我们用一个方格地图,假如每次搜索的方向为 上下左右(不包含斜上方),那么给出一个start起始位置,那么BFS就是从四个方向走出第一步。
20 |
21 | 
22 |
23 | 如果加上一个end终止位置,那么使用BFS的搜索过程如图所示:
24 |
25 | 
26 |
27 | 我们从图中可以看出,从start起点开始,是一圈一圈,向外搜索,方格编号1为第一步遍历的节点,方格编号2为第二步遍历的节点,第四步的时候我们找到终止点end。
28 |
29 | 正是因为BFS一圈一圈的遍历方式,所以一旦遇到终止点,那么一定是一条最短路径。
30 |
31 | 而且地图还可以有障碍,如图所示:
32 |
33 | 
34 |
35 | 在第五步,第六步 我只把关键的节点染色了,其他方向周边没有去染色,大家只要关注关键地方染色的逻辑就可以。
36 |
37 | 从图中可以看出,如果添加了障碍,我们是第六步才能走到end终点。
38 |
39 | 只要BFS只要搜到终点一定是一条最短路径,大家可以参考上面的图,自己再去模拟一下。
40 |
41 | ## 代码框架
42 |
43 | 大家应该好奇,这一圈一圈的搜索过程是怎么做到的,是放在什么容器里,才能这样去遍历。
44 |
45 | 很多网上的资料都是直接说用队列来实现。
46 |
47 | 其实,我们仅仅需要一个容器,能保存我们要遍历过的元素就可以,**那么用队列,还是用栈,甚至用数组,都是可以的**。
48 |
49 | **用队列的话,就是保证每一圈都是一个方向去转,例如统一顺时针或者逆时针**。
50 |
51 | 因为队列是先进先出,加入元素和弹出元素的顺序是没有改变的。
52 |
53 | **如果用栈的话,就是第一圈顺时针遍历,第二圈逆时针遍历,第三圈有顺时针遍历**。
54 |
55 | 因为栈是先进后出,加入元素和弹出元素的顺序改变了。
56 |
57 | 那么广搜需要注意 转圈搜索的顺序吗? 不需要!
58 |
59 | 所以用队列,还是用栈都是可以的,但大家都习惯用队列了,**所以下面的讲解用我也用队列来讲,只不过要给大家说清楚,并不是非要用队列,用栈也可以**。
60 |
61 | 下面给出广搜代码模板,该模板针对的就是,上面的四方格的地图: (详细注释)
62 |
63 | ```CPP
64 | int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 表示四个方向
65 | // grid 是地图,也就是一个二维数组
66 | // visited标记访问过的节点,不要重复访问
67 | // x,y 表示开始搜索节点的下标
68 | void bfs(vector>& grid, vector>& visited, int x, int y) {
69 | queue> que; // 定义队列
70 | que.push({x, y}); // 起始节点加入队列
71 | visited[x][y] = true; // 只要加入队列,立刻标记为访问过的节点
72 | while(!que.empty()) { // 开始遍历队列里的元素
73 | pair cur = que.front(); que.pop(); // 从队列取元素
74 | int curx = cur.first;
75 | int cury = cur.second; // 当前节点坐标
76 | for (int i = 0; i < 4; i++) { // 开始想当前节点的四个方向左右上下去遍历
77 | int nextx = curx + dir[i][0];
78 | int nexty = cury + dir[i][1]; // 获取周边四个方向的坐标
79 | if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue; // 坐标越界了,直接跳过
80 | if (!visited[nextx][nexty]) { // 如果节点没被访问过
81 | que.push({nextx, nexty}); // 队列添加该节点为下一轮要遍历的节点
82 | visited[nextx][nexty] = true; // 只要加入队列立刻标记,避免重复访问
83 | }
84 | }
85 | }
86 |
87 | }
88 | ```
89 |
90 |
91 | ## 总结
92 |
93 | 当然广搜还有很多细节需要注意的地方,后面我会针对广搜的题目还做针对性的讲解。
94 |
95 | **因为在理论篇讲太多细节,可能会让刚学广搜的录友们越看越懵**,所以细节方面针对具体题目在做讲解。
96 |
97 | 本篇我们重点讲解了广搜的使用场景,广搜的过程以及广搜的代码框架。
98 |
99 | 其实在二叉树章节的[层序遍历](https://programmercarl.com/0102.%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E5%B1%82%E5%BA%8F%E9%81%8D%E5%8E%86.html)中,我们也讲过一次广搜,相当于是广搜在二叉树这种数据结构上的应用。
100 |
101 | 这次则从图论的角度上再详细讲解一次广度优先遍历。
102 |
103 | 相信看完本篇,大家会对广搜有一个基础性的认识,后面再来做对应的题目就会得心应手一些。
104 |
105 |
--------------------------------------------------------------------------------
/problems/kamacoder/图论总结篇.md:
--------------------------------------------------------------------------------
1 |
2 | # 图论总结篇
3 |
4 | 从深搜广搜 到并查集,从最小生成树到拓扑排序, 最后是最短路算法系列。
5 |
6 | 至此算上本篇,一共30篇文章,图论之旅就在此收官了。
7 |
8 | 在[0098.所有可达路径](./0098.所有可达路径.md) ,我们接触了两种图的存储方式,邻接表和邻接矩阵,掌握两种图的存储方式很重要。
9 |
10 | 图的存储方式也是大家习惯在核心代码模式下刷题 经常忽略的 知识点。因为在力扣上刷题不需要掌握图的存储方式。
11 |
12 | ## 深搜与广搜
13 |
14 | 在二叉树章节中,其实我们讲过了 深搜和广搜在二叉树上的搜索过程。
15 |
16 | 在图论章节中,深搜与广搜就是在图这个数据结构上的搜索过程。
17 |
18 | 深搜与广搜是图论里基本的搜索方法,大家需要掌握三点:
19 |
20 | * 搜索方式:深搜是可一个方向搜,不到黄河不回头。 广搜是围绕这起点一圈一圈的去搜。
21 | * 代码模板:需要熟练掌握深搜和广搜的基本写法。
22 | * 应用场景:图论题目基本上可以即用深搜也可用广搜,无疑是用哪个方便而已
23 |
24 | ### 注意事项
25 |
26 | 需要注意的是,同样是深搜模板题,会有两种写法。
27 |
28 | 在[0099.岛屿的数量深搜.md](./0099.岛屿的数量深搜.md) 和 [0105.有向图的完全可达性](./0105.有向图的完全可达性.md),涉及到dfs的两种写法。
29 |
30 | **我们对dfs函数的定义是 是处理当前节点 还是处理下一个节点 很重要**,决定了两种dfs的写法。
31 |
32 | 这也是为什么很多录友看到不同的dfs写法,结果发现提交都能过的原因。
33 |
34 | 而深搜还有细节,有的深搜题目需要用到回溯的过程,有的就不用回溯的过程,
35 |
36 | 一般是需要计算路径的问题 需要回溯,如果只是染色问题(岛屿问题系列) 就不需要回溯。
37 |
38 | 例如: [0105.有向图的完全可达性](./0105.有向图的完全可达性.md) 深搜就不需要回溯,而 [0098.所有可达路径](./0098.所有可达路径.md) 中的递归就需要回溯,文章中都有详细讲解
39 |
40 | 注意:以上说的是不需要回溯,不是没有回溯,只要有递归就会有回溯,只是我们是否需要用到回溯这个过程,这是需要考虑的。
41 |
42 | 很多录友写出来的广搜可能超时了, 例如题目:[0099.岛屿的数量广搜](./0099.岛屿的数量广搜.md)
43 |
44 | 根本原因是**只要 加入队列就代表 走过,就需要标记,而不是从队列拿出来的时候再去标记走过**。
45 |
46 | 具体原因,我在[0099.岛屿的数量广搜](./0099.岛屿的数量广搜.md) 中详细讲了。
47 |
48 | 在深搜与广搜的讲解中,为了防止惯性思维,我特别加入了题目 [0106.岛屿的周长](./0106.岛屿的周长.md),提醒大家,看到类似的题目,也不要上来就想着深搜和广搜。
49 |
50 | 还有一些图的问题,在题目描述中,是没有图的,需要我们自己构建一个图,例如 [0110.字符串接龙](./0110.字符串接龙.md),题目中连线都没有,需要我们自己去思考 什么样的两个字符串可以连成线。
51 |
52 | ## 并查集
53 |
54 | 并查集相对来说是比较复杂的数据结构,其实他的代码不长,但想彻底学透并查集,需要从多个维度入手,
55 |
56 | 我在理论基础篇的时候 讲解如下重点:
57 |
58 | * 为什么要用并查集,怎么不用个二维数据,或者set、map之类的。
59 | * 并查集能解决那些问题,哪些场景会用到并查集
60 | * 并查集原理以及代码实现
61 | * 并查集写法的常见误区
62 | * 带大家去模拟一遍并查集的过程
63 | * 路径压缩的过程
64 | * 时间复杂度分析
65 |
66 | 上面这几个维度 大家都去思考了,并查集基本就学明白了。
67 |
68 | 其实理论基础篇就算是给大家出了一道裸的并查集题目了,所以在后面的题目安排中,会稍稍的拔高一些,重点在于并查集的应用上。
69 |
70 | 例如 并查集可以判断这个图是否是树,因为树的话,只有一个根,符合并查集判断集合的逻辑,题目:[0108.冗余连接](./0108.冗余连接.md)。
71 |
72 | 在[0109.冗余连接II](./0109.冗余连接II.md) 中 对有向树的判断难度更大一些,需要考虑的情况比较多。
73 |
74 |
75 | ## 最小生成树
76 |
77 | 最小生成树是所有节点的最小连通子图, 即:以最小的成本(边的权值)将图中所有节点链接到一起。
78 |
79 | 最小生成树算法,有prim 和 kruskal。
80 |
81 | **prim 算法是维护节点的集合,而 Kruskal 是维护边的集合**。
82 |
83 | 在 稀疏图中,用Kruskal更优。 在稠密图中,用prim算法更优。
84 |
85 | > 边数量较少为稀疏图,接近或等于完全图(所有节点皆相连)为稠密图
86 |
87 | Prim 算法 时间复杂度为 O(n^2),其中 n 为节点数量,它的运行效率和图中边树无关,适用稠密图。
88 |
89 | Kruskal算法 时间复杂度 为 O(nlogn),其中n 为边的数量,适用稀疏图。
90 |
91 | 关于 prim算法,我自创了三部曲,来帮助大家理解:
92 |
93 | 1. 第一步,选距离生成树最近节点
94 | 2. 第二步,最近节点加入生成树
95 | 3. 第三步,更新非生成树节点到生成树的距离(即更新minDist数组)
96 |
97 | 大家只要理解这三部曲, prim算法 至少是可以写出一个框架出来,然后在慢慢补充细节,这样不至于 自己在写prim的时候 两眼一抹黑 完全凭感觉去写。
98 |
99 | **minDist数组 是prim算法的灵魂,它帮助 prim算法完成最重要的一步,就是如何找到 距离最小生成树最近的点**。
100 |
101 | kruscal的主要思路:
102 |
103 | * 边的权值排序,因为要优先选最小的边加入到生成树里
104 | * 遍历排序后的边
105 | * 如果边首尾的两个节点在同一个集合,说明如果连上这条边图中会出现环
106 | * 如果边首尾的两个节点不在同一个集合,加入到最小生成树,并把两个节点加入同一个集合
107 |
108 | 而判断节点是否在一个集合 以及将两个节点放入同一个集合,正是并查集的擅长所在。
109 |
110 | 所以 Kruskal 是需要用到并查集的。
111 |
112 | 这也是我在代码随想录图论编排上 为什么要先 讲解 并查集 在讲解 最小生成树。
113 |
114 |
115 | ## 拓扑排序
116 |
117 | 拓扑排序 是在图上的一种排序。
118 |
119 | 概括来说,**给出一个 有向图,把这个有向图转成线性的排序 就叫拓扑排序**。
120 |
121 | 同样,拓扑排序也可以检测这个有向图 是否有环,即存在循环依赖的情况。
122 |
123 | 拓扑排序的一些应用场景,例如:大学排课,文件下载依赖 等等。
124 |
125 | 只要记住如下两步拓扑排序的过程,代码就容易写了:
126 |
127 | 1. 找到入度为0 的节点,加入结果集
128 | 2. 将该节点从图中移除
129 |
130 | ## 最短路算法
131 |
132 | 最短路算法是图论中,比较复杂的算法,而且不同的最短路算法都有不同的应用场景。
133 |
134 | 我在 [最短路算法总结篇](./最短路问题总结篇.md) 里已经做了一个高度的概括。
135 |
136 | 大家要时常温故而知新,才能透彻理解各个最短路算法。
137 |
138 |
139 | ## 总结
140 |
141 | 到最后,图论终于剧终了,相信这是市面上大家能看到最全最细致的图论讲解教程。
142 |
143 | 图论也是我 《代码随想录》所有章节里 所费精力最大的一个章节。
144 |
145 | 只为了不负录友们的期待。 大家加油💪🏻
146 |
--------------------------------------------------------------------------------
/problems/kamacoder/好二叉树.md:
--------------------------------------------------------------------------------
1 |
2 | 本题和 [96.不同的二叉搜索树](https://www.programmercarl.com/0096.%E4%B8%8D%E5%90%8C%E7%9A%84%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91.html) 比较像
3 |
4 | * 取模这里很容易出错
5 | * 过程中所用到的数值都有可能超过int,所以要改用longlong
6 |
7 | ```CPP
8 | #include
9 | #include
10 | using namespace std;
11 |
12 | long long mod = 1e9 + 7;
13 | long long dp(int t, vector& memory) {
14 | if (t % 2 == 0) return 0;
15 | if (t == 1) return 1;
16 | if (memory[t] != -1) return memory[t];
17 |
18 | long long result = 0;
19 | // 枚举左右子树节点的数量
20 | for (int i = 1; i < t; i += 2) {
21 | long long leftNum = dp(i, memory); // 左子树节点数量为i
22 | long long rightNum = dp(t - i - 1, memory); // 右子树节点数量为t - i - 1
23 | result += (leftNum * rightNum) % mod; // 注意这里是乘的关系
24 | result %= mod;
25 | }
26 | memory[t] = result;
27 | return result;
28 | }
29 | int main() {
30 | int n;
31 | cin >> n;
32 | vector memory(n + 1, -1);
33 | cout << dp(n, memory) << endl;
34 | }
35 | ```
36 |
37 |
38 | ```CPP
39 | #include
40 | #include
41 | #include
42 |
43 | using namespace std;
44 |
45 | const int MOD = 1000000007;
46 |
47 | int main() {
48 | int num;
49 | cin >> num;
50 |
51 | if (num % 2 == 0) {
52 | cout << 0 << endl;
53 | return 0;
54 | }
55 |
56 | vector dp(num + 1, 0);
57 | dp[1] = 1;
58 |
59 | for (int i = 3; i <= num; i += 2) {
60 | for (int j = 1; j <= i - 2; j += 2) {
61 | dp[i] = (dp[i] + dp[j] * dp[i - 1 - j]) % MOD;
62 | }
63 | }
64 |
65 | cout << dp[num] << endl;
66 | return 0;
67 | }
68 |
69 | ```
70 |
71 |
72 | 第二题的代码
73 |
74 | #include
75 | using namespace std;
76 |
77 | long fastexp(long base,long n,long mod){
78 | long answer = 1;
79 | while(n > 0){
80 | if(n % 2 == 1){
81 | answer = (answer * base) % mod;
82 | }
83 | base = (base * base) % mod;
84 | n /= 2;
85 | }
86 | return answer;
87 | }
88 | int kawaiiStrings(int n) {
89 | // write code here
90 | std::vector f(n + 1), g(n + 1), h(n + 1);
91 | long mod = 1000000007;
92 | for (long i = 2; i <= n; i++) g[i] = (g[i - 1] * 2 + (i - 1) * fastexp(2,i-2,mod)) % mod;
93 | for (long i = 3; i <= n; i++) f[i] = ((f[i - 1] * 3) % mod + g[i - 1]) % mod;
94 | for (long i = 3; i <= n; i++) h[i] = (fastexp(3, i - 3, mod) + h[i - 1] * 3 - h[i - 3]) % mod;
95 | return (f[n]-h[n]+mod)%mod;
96 |
97 | }
98 |
99 | int main(){
100 | int n;
101 | cin >> n;
102 | cout << kawaiiStrings(n) << endl;
103 | return 0;
104 | }
105 |
--------------------------------------------------------------------------------
/problems/kamacoder/完美数.md:
--------------------------------------------------------------------------------
1 |
2 | ```CPP
3 | #include
4 | #include
5 | using namespace std;
6 | int countOnes(long long num) {
7 | int zeroCount = 0;
8 | while (num > 0) {
9 | if (num % 10 != 0) { // 检查最低位是否为0
10 | zeroCount++;
11 | }
12 | num /= 10; // 移除最低位
13 | }
14 | return zeroCount;
15 | }
16 | int main() {
17 | int n;
18 | cin >> n;
19 | vector vec(n);
20 | for (int i = 0; i < n; i++) cin >> vec[i];
21 | int result = 0;
22 | for (int i = 0; i < n; i++) {
23 | for (int j = i + 1; j < n; j++) {
24 | if (countOnes(vec[i] * vec[j]) == 1) result++;
25 | }
26 | }
27 | cout << result << endl;
28 | }
29 | ```
30 |
--------------------------------------------------------------------------------
/problems/kamacoder/最短路问题总结篇.md:
--------------------------------------------------------------------------------
1 |
2 | # 最短路算法总结篇
3 |
4 | 至此已经讲解了四大最短路算法,分别是Dijkstra、Bellman_ford、SPFA 和 Floyd。
5 |
6 | 针对这四大最短路算法,我用了七篇长文才彻底讲清楚,分别是:
7 |
8 | * dijkstra朴素版
9 | * dijkstra堆优化版
10 | * Bellman_ford
11 | * Bellman_ford 队列优化算法(又名SPFA)
12 | * bellman_ford 算法判断负权回路
13 | * bellman_ford之单源有限最短路
14 | * Floyd 算法精讲
15 | * 启发式搜索:A * 算法
16 |
17 |
18 | 最短路算法比较复杂,而且各自有各自的应用场景,我来用一张表把讲过的最短路算法的使用场景都展现出来:
19 |
20 | 
21 |
22 | (因为A * 属于启发式搜索,和上面最短路算法并不是一类,不适合一起对比,所以没有放在一起)
23 |
24 |
25 | 可能有同学感觉:这个表太复杂了,我记也记不住。
26 |
27 | 其实记不住的原因还是对 这几个最短路算法没有深刻的理解。
28 |
29 | 这里我给大家一个大体使用场景的分析:
30 |
31 | **如果遇到单源且边为正数,直接Dijkstra**。
32 |
33 | 至于 **使用朴素版还是 堆优化版 还是取决于图的稠密度**, 多少节点多少边算是稠密图,多少算是稀疏图,这个没有量化,如果想量化只能写出两个版本然后做实验去测试,不同的判题机得出的结果还不太一样。
34 |
35 | 一般情况下,可以直接用堆优化版本。
36 |
37 | **如果遇到单源边可为负数,直接 Bellman-Ford**,同样 SPFA 还是 Bellman-Ford 取决于图的稠密度。
38 |
39 | 一般情况下,直接用 SPFA。
40 |
41 | **如果有负权回路,优先 Bellman-Ford**, 如果是有限节点最短路 也优先 Bellman-Ford,理由是写代码比较方便。
42 |
43 | **如果是遇到多源点求最短路,直接 Floyd**。
44 |
45 | 除非 源点特别少,且边都是正数,那可以 多次 Dijkstra 求出最短路径,但这种情况很少,一般出现多个源点了,就是想让你用 Floyd 了。
46 |
47 | 对于A * ,由于其高效性,所以在实际工程应用中使用最为广泛 ,由于其 结果的不唯一性,也就是可能是次短路的特性,一般不适合作为算法题。
48 |
49 | 游戏开发、地图导航、数据包路由等都广泛使用 A * 算法。
50 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/problems/qita/acm.md:
--------------------------------------------------------------------------------
1 |
2 | # 如何练习ACM模式输入输出模式 | 如何准备笔试 | 卡码网
3 |
4 | 卡码网地址:[https://kamacoder.com](https://kamacoder.com)
5 |
6 | ## 为什么卡码网
7 |
8 | 录友们在求职的时候会发现,很多公司的笔试题和面试题都是ACM模式, 而大家习惯去力扣刷题,力扣是核心代码模式。
9 |
10 | 当大家在做ACM模式的算法题的时候,需要自己处理数据的输入输出,**如果没有接触过的话,还是挺难的**。
11 |
12 | [知识星球](https://programmercarl.com/other/kstar.html)里很多录友的日常打卡中,都表示被 ACM模式折磨过:
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | 所以我正式推出:**卡码网**([https://kamacoder.com](https://kamacoder.com)),**专门帮助大家练习ACM模式**。
25 |
26 | 那么之前大家去哪里练习ACM模式呢?
27 |
28 | 去牛客做笔试真题,结果发现 ACM模式没练出来,题目倒是巨难,一点思路都没有,代码更没有写,ACM模式无从练起。
29 |
30 | 去洛谷,POJ上练习? 结果发现 题目超多,不知道从哪里开始刷,也没有一个循序渐进的刷题顺序。
31 |
32 | **而卡码网上有我精选+制作的25道题目**!我还把25题的后台测试数据制作了一遍,保证大家练习的效果。
33 |
34 | 为什么题目不多,只有25道?
35 |
36 | 因为大家练习ACM模式不需要那么多题目,有一个循序渐进的练习过程就好了。
37 |
38 | 这25道题目包含了数组、链表、哈希表、字符串、二叉树、动态规划以及图的的题目,常见的输入输出方式都覆盖了。
39 |
40 | **这是最精华的25道题目**!。
41 |
42 | ## 卡码网长什么样
43 |
44 | 来看看这极简的界面,没有烂七八糟的功能,只有刷题!
45 |
46 |
47 |
48 | 在「状态」这里可以看到 大家提交的代码和判题记录,目前卡码网([https://kamacoder.com](https://kamacoder.com))几乎无时无刻都有卡友在提交代码。
49 | 看看大家周六晚上都在做什么,刷哪些题目。
50 |
51 |
52 |
53 |
54 | 提交代码的界面是这样的,**目前支持所有主流刷题语言**。
55 |
56 |
57 |
58 | ## 题解
59 |
60 | 基本大家来卡码网([https://kamacoder.com](https://kamacoder.com))练习ACM模式,都是对输入输出不够了解的,所以想看现成的题解,看看究竟是怎么处理的。
61 |
62 | 所以我用C++把卡码网上25道题目的题解都写了,并发布到Github上:
63 |
64 | [https://github.com/youngyangyang04/kamacoder-solutions](https://github.com/youngyangyang04/kamacoder-solutions)
65 |
66 |
67 |
68 | **欢迎去Github上star,欢迎fork,也欢迎来Github仓库贡献其他语言版本,成为contributor**。
69 |
70 | 如果不懂如何和开源项目提交代码,[可以看这里](https://www.programmercarl.com/qita/join.html)
71 |
72 | 目前已经有两位录友贡献C和Java版本了。
73 |
74 |
75 |
76 | 期待在Github(https://github.com/youngyangyang04/kamacoder-solutions) 的contributors上也出现你的头像。
77 |
78 | 目前题解只有C++代码吗?
79 |
80 | 当然不是,大多数题目已经有了 Java、python、C版本。 **其他语言版本,就给录友们成为contributor的机会了**。
81 |
82 | ## 最后
83 |
84 | 卡码网地址:[https://kamacoder.com](https://kamacoder.com)
85 |
86 | 快去体验吧,笔试之前最好 把卡码网25道题目都刷完。
87 |
88 | 期待录友们成为最早一批把卡码网刷爆的coder!
89 |
90 |
--------------------------------------------------------------------------------
/problems/qita/server.md:
--------------------------------------------------------------------------------
1 |
2 | # 一台服务器有什么用!
3 |
4 | * [阿里云活动期间服务器购买](https://www.aliyun.com/minisite/goods?taskCode=shareNew2205&recordId=3641992&userCode=roof0wob)
5 | * [腾讯云活动期间服务器购买](https://curl.qcloud.com/EiaMXllu)
6 |
7 | 但在组织这场活动的时候,了解到大家都有一个共同的问题: **这个服务器究竟有啥用??**
8 |
9 | 这真是一个好问题,而且我一句两句还说不清楚,所以就专门发文来讲一讲。
10 |
11 | 同时我还录制的一期视频,我的视频号,大家可以关注一波。
12 |
13 |
14 | 一说到服务器,可能很多人都说搞分布式,做计算,搞爬虫,做程序后台服务,多人合作等等。
15 |
16 | 其实这些普通人都用不上,我来说一说大家能用上的吧。
17 |
18 | ## 搭建git私服
19 |
20 | 大家平时工作的时候一定有一个自己的工作文件夹,学生的话就是自己的课件,考试,准备面试的资料等等。
21 |
22 | 已经工作的录友,会有一个文件夹放着自己重要的文档,Markdown,图片,简历等等。
23 |
24 | 这么重要的文件夹,而且我们每天都要更新,也担心哪天电脑丢了,或者坏了,突然这些都不见了。
25 |
26 | 所以我们想备份嘛。
27 |
28 | 还有就是我们经常个人电脑和工作电脑要同步一些私人资料,而不是用微信传来传去。
29 |
30 | 这些都是git私服的使用场景,而且很好用。
31 |
32 | 大家也知道 github,gitee也可以搞私人仓库 用来备份,同步文件,但自己的文档可能放着很多重要的信息,包括自己的各种密码,密钥之类的,放到上面未必安全。你就不怕哪些重大bug把你的信息都泄漏了么[机智]
33 |
34 | 更关键的是,github 和 gitee都限速的。毕竟人家的功能定位并不是网盘。
35 |
36 | 项目里有大文件(几百M以上),例如pdf,ppt等等 其上传和下载速度会让你窒息。
37 |
38 | **后面我会发文专门来讲一讲,如何大家git私服!**
39 |
40 | ## 搞一个文件存储
41 |
42 | 这个可以用来生成文件的下载链接,也可以把本地文件传到服务器上。
43 |
44 | 相当于自己做一个对象存储,其实云厂商也有对象存储的产品。
45 |
46 | 不过我们自己也可以做一个,不够很多很同学应该都不知道对象存储怎么用吧,其实我们用服务器可以自己做一个类似的公司。
47 |
48 | 我现在就用自己用go写的一个工具,部署在服务器上。 用来和服务器传文件,或者生成一些文件的临时下载链接。
49 |
50 | 这些都是直接命令行操作的,
51 |
52 | 操作方式这样,我把命令包 包装成一个shell命令,想传那个文件,直接 uploadtomyserver,然后就返回可以下载的链接,这个文件也同时传到了我的服务器上。
53 |
54 | 
55 |
56 | 我也把我的项目代码放在了github上:
57 |
58 | https://github.com/youngyangyang04/fileHttpServer
59 |
60 | 感兴趣的录友可以去学习一波,顺便给个star。
61 |
62 |
63 | ## 网站
64 |
65 | 做网站,例如 大家知道用html 写几行代码,就可以生成一个网页,但怎么给别人展示呢?
66 |
67 | 大家如果用自己的电脑做服务器,只能同一个路由器下的设备可以访问你的网站,可能这个设备出了这个屋子 都访问不了你的网站了。
68 |
69 | 因为你的IP不是公网IP。
70 |
71 | 如果有了一台云服务器,都是配公网IP,你的网站就可以让任何人访问了。
72 |
73 | 或者说 你提供的一个服务就可以让任何人使用。
74 |
75 | 例如第二个例子中,我们可以自己开发一个文件存储,这个服务,我只把把命令行给其他人,其他人都可以使用我的服务来生成链接,当然他们的文件也都传到了我的服务器上。
76 |
77 | 再说一个使用场景。
78 |
79 | 我之前在组织免费里服务器的活动的时候,阿里云给我一个excel,让面就是从我这里买服务器录友的名单,我直接把这个名单甩到群里,让大家自己检查,出现在名单里就可以找我返现,这样做是不是也可以。
80 |
81 | 这么做有几个很大的问题:
82 | * 大家都要去下载excel,做对比,会有人改excel的内容然后就说是从你这里买的,我不可能挨个去比较excel有没有改动
83 | * excel有其他人的个人信息,这是不能暴漏的。
84 | * 如果每个人自己用excel查询,私信我返现,一个将近两千人找我返现,我微信根本处理不过来,这就变成体力活了。
85 |
86 | 那应该怎么做呢,
87 |
88 | 我就简单写一个查询的页面,后端逻辑就是读一个execel表格,大家在查询页面输入自己的阿里云ID,如果在excel里,页面就会返回返现群的二维码,大家就可以自主扫码加群了。
89 |
90 | 这样,我最后就直接在返现群里 发等额红包就好了,是不是极大降低人力成本了
91 |
92 | 当然我是把 17个返现群的二维码都生成好了,按照一定的规则,展现给查询通过的录友。
93 |
94 | 就是这样一个非常普通的查询页面。
95 |
96 | 
97 |
98 | 查询通过之后,就会展现返现群二维码。
99 |
100 | 
101 |
102 | 但要部署在服务器上,因为没有公网IP,别人用不了你的服务。
103 |
104 |
105 | ## 学习linux
106 |
107 | 学习linux其实在自己的电脑上搞一台虚拟机,或者安装双系统也可以学习,不过这很考验你的电脑性能如何了。
108 |
109 | 如果你有一个服务器,那就是独立的一台电脑,你怎么霍霍就怎么霍霍,而且一年都不用关机的,可以一直跑你的任务,和你本地电脑也完全隔离。
110 |
111 | 更方便的是,你目前系统假如是CentOS,想做一个实验需要在Ubuntu上,如果是云服务器,更换系统就是在 后台点一下,一键重装,云厂商基本都是支持所有系统一件安装的。
112 |
113 | 我们平时自己玩linux经常是配各种环境,然后这个linux就被自己玩坏了(一般都是毫无节制使用root权限导致的),总之就是环境配不起来了,基本就要重装了。
114 |
115 | 那云服务器重装系统可太方便了。
116 |
117 | 还有就是加入你好不容易配好的环境,如果以后把这个环境玩坏了,你先回退这之前配好的环境而不是重装系统在重新配一遍吧。
118 |
119 | 那么可以用云服务器的镜像保存功能,就是你配好环境的那一刻就可以打一个镜像包,以后如果环境坏了,直接回退到上次镜像包的状态,这是不是就很香了。
120 |
121 |
122 | ## 总结
123 |
124 | 其实云服务器还有很多其他用处,不过我就说一说大家普遍能用的上的。
125 |
126 |
127 | * [阿里云活动期间服务器购买](https://www.aliyun.com/minisite/goods?taskCode=shareNew2205&recordId=3641992&userCode=roof0wob)
128 | * [腾讯云活动期间服务器购买](https://curl.qcloud.com/EiaMXllu)
129 |
130 |
--------------------------------------------------------------------------------
/problems/qita/shejimoshi.md:
--------------------------------------------------------------------------------
1 |
2 | # 23种设计模式精讲 | 配套练习题 | 卡码网
3 |
4 | 关于设计模式的学习,大家应该还是看书或者看博客,但却没有一个边学边练的学习环境。
5 |
6 | 学完了一种设计模式 是不是应该去练一练?
7 |
8 | 所以卡码网 针对 23种设计,**推出了 23道编程题目,来帮助大家练习设计模式**。
9 |
10 |
11 |
12 | 这里的23到编程题目对应了 23种这几模式。 例如第一题,小明的购物车,就是单例模式:
13 |
14 |
15 |
16 | 区别于网上其他教程,本教程的特点是:
17 |
18 | * **23种设计模式全覆盖**,涵盖了所有Gang of Four设计模式,包括创建型、结构型和行为型设计模式。
19 | * 通过23道简单而实用的例子,**以刷算法题的形式了解每种设计模式的概念、结构和应用场景**。
20 | * **为每个设计模式提供清晰的文字解释、结构图和代码演示**,帮助你更好地理解和实践。
21 | * **难度安排循序渐进**,从基础的、常用的设计模式逐步深入。
22 |
23 | 这样的一个学习体验,要收费吗?
24 |
25 | **免费的**!
26 |
27 | 相信录友们可能还没有这种学习设计模式的体验,快去卡码网(kamacoder.com)上体验吧。
28 |
29 | 23道 设计模式的题目给大家出了,那么是不是得安排上对应的讲解?
30 |
31 | **当然安排**!
32 |
33 | 针对每道题目,还给大家编写了一套 23种设计模式精讲,已经开源到Github上:
34 |
35 | > https://github.com/youngyangyang04/kama-DesignPattern
36 |
37 | 支持Java,Python,Go,C++ 版本,也欢迎大家去Github上提交PR,补充其他语言版本。
38 |
39 | 所以题解也免费开放给录友!
40 |
41 | 同时还给全部整理到PDF上,这份PDF,我们写的很用心了,来个大家截个图:
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | 关于设计模式的题目,大家现在就可以去 卡码网(kamacoder)去做了。
52 |
53 | 关于这23道题目对应 设计模式精讲 PDF,也免费分享给录友们,大家可以加我的企业微信获取:
54 |
55 |
56 | 已经有我企业微信的录友,直接发:设计模式,这四个字就好,我会直接发你。
57 |
58 |
--------------------------------------------------------------------------------
/problems/toolgithub.sh:
--------------------------------------------------------------------------------
1 | #########################################################################
2 |
3 | # File Name: toolgithub.sh
4 | # Author: 程序员Carl
5 | # mail: programmercarl@163.com
6 | # Created Time: Sat Oct 15 16:36:23 2022
7 | #########################################################################
8 | #!/bin/bash
9 |
10 | #
11 | #
12 | #
13 | #
14 | #
参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!
15 |
16 | #
17 | #
18 | #
19 | #
20 | #
21 |
22 | for i in *.md
23 | do
24 | if [[ $i != 'README.md' ]]
25 | then
26 | # 移除开头
27 | sed -i '' '/align/d;/\"\"><\/a>/d;/<\/p>/d;/<\/a>/d;/20210924105952.png/d;/_blank/d' $i
28 | # 移除结尾
29 | sed -i '' '/训练营/d;/网站星球宣传海报/d' $i
30 |
31 |
32 | # 添加开头
33 | # 记得从后向前添加
34 | ex -sc '1i|
参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!
' -cx $i
35 | ex -sc '1i|' -cx $i
36 | ex -sc '1i|
' -cx $i
37 | ex -sc '1i|' -cx $i
38 | ex -sc '1i|' -cx $i
39 | # echo '## 其他语言版本' >> $i
40 | # echo '\n' >> $i
41 | # echo 'Java:' >> $i
42 | # echo '\n' >> $i
43 | # echo 'Python:' >> $i
44 | # echo '\n' >> $i
45 | # echo 'Go:' >> $i
46 | # echo '\n' >> $i
47 | # echo '\n' >> $i
48 |
49 | # 添加结尾
50 |
51 | echo '
' >> $i
52 | echo '' >> $i
53 | echo '
' >> $i
54 | echo '' >> $i
55 |
56 | # echo '-----------------------' >> $i
57 |
58 | # echo '
' >> $i
59 | fi
60 | done
61 |
62 | #
63 | #
64 | #
65 | #
66 |
67 | #
参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!
68 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/problems/前序/ACM模式.md:
--------------------------------------------------------------------------------
1 |
2 | # 什么是核心代码模式,什么又是ACM模式?
3 |
4 | 很多录友刷了不少题了,到现在也没有搞清楚什么是 ACM模式,什么是核心代码模式。
5 |
6 | 平时大家在力扣上刷题,就是 核心代码模式,即给你一个函数,直接写函数实现,例如这样:
7 |
8 | 
9 |
10 | 而ACM模式,是程序头文件,main函数,数据的输入输出都要自己处理,例如这样:
11 |
12 | 
13 |
14 | 大家可以发现 右边代码框什么都没有,程序从头到尾都需要自己实现,本题如果写完代码是这样的: (细心的录友可以发现和力扣上刷题是不一样的)
15 |
16 | 
17 |
18 |
19 | **如果大家从一开始学习算法就一直在力扣上的话,突然切到ACM模式会非常不适应**。
20 |
21 | 知识星球里也有很多录友,因为不熟悉ACM模式在面试的过程中吃了不少亏。
22 |
23 |
24 | 
25 |
26 | 
27 |
28 | 
29 |
30 | 
31 |
32 | 
33 |
34 | ## 面试究竟怎么考?
35 |
36 | 笔试的话,基本都是 ACM模式。
37 |
38 | 面试的话,看情况,有的面试官会让你写一个函数实现就可以,此时就是核心代码模式。
39 |
40 | 有的面试官会 给你一个编辑器,让你写完代码运行一下看看输出结果,此时就是ACM模式。
41 |
42 | 有的录友想,那我平时在力扣刷题,写的是核心代码模式,我也可以运行,为啥一定要用ACM模式。
43 |
44 | **大家在力扣刷题刷多了,已经忘了程序是如何运行的了**,力扣上写的代码,脱离力扣环境,那个函数,你怎么运行呢?
45 |
46 | 想让程序在本地运行起来,是不是需要补充 库函数,是不是要补充main函数,是不是要补充数据的输入和输出。 那不就是ACM模式了。
47 |
48 | 综合来看,** ACM模式更考察综合代码能力, 核心代码模式是更聚焦算法的实现逻辑**。
49 |
50 | ## 去哪练习ACM模式?
51 |
52 | 这里给大家推荐卡码网: [kamacoder.com](https://kamacoder.com/)
53 |
54 | 你只要能把卡码网首页的25道题目 都刷了 ,就把所有的ACM输入输出方式都练习到位了,不会有任何盲区。
55 |
56 | 
57 |
58 | 而且你不用担心,题目难度太大,直接给自己劝退,**卡码网的前25道题目都是我精心制作的,难度也是循序渐进的**,大家去刷一下就知道了。
59 |
60 |
61 |
--------------------------------------------------------------------------------
/problems/前序/Java处理输入输出.md:
--------------------------------------------------------------------------------
1 | 在面试中,我们常常会遇到面试官让我们用某种编程语言做题,并要求能够在本地编译运行。
2 | 这种模式也被称做ACM模式。
3 |
4 | 本文将介绍如何用Java处理输入输出。
5 |
6 |
--------------------------------------------------------------------------------
/problems/前序/vim.md:
--------------------------------------------------------------------------------
1 | # 人生苦短,我用VIM!| 最强vim配置
2 |
3 | > Github地址:[https://github.com/youngyangyang04/PowerVim](https://github.com/youngyangyang04/PowerVim)
4 | > Gitee地址:[https://gitee.com/programmercarl/power-vim](https://gitee.com/programmercarl/power-vim)
5 |
6 | 熟悉我的录友,应该都知道我是vim流,无论是写代码还是写文档(Markdown),都是vim,都没用IDE。
7 |
8 | 但这里我并不是说IDE不好用,IDE在 代码跟踪,引用跳转等等其实是很给力的,效率比vim高。
9 |
10 | 我用vim的话,如果需要跟踪代码的话,就用ctag去跳转,虽然很不智能(是基于规则匹配,不是语义匹配),但加上我自己的智能就也能用(这里真的要看对代码的把握程度了)
11 |
12 | 所以连跟踪代码都不用IDE的话,其他方面那我就更用不上IDE了。
13 |
14 | ## 为什么用VIM
15 |
16 | **至于写代码的效率,VIM完爆IDE**,其他不说,就使用IDE每次还要去碰鼠标,就很让人烦心!(真凸显了程序员的执着)
17 |
18 | 这里说一说vim的方便之处吧,搞后端开发的同学,都得玩linux吧,在linux下写代码,如果不会vim的话,会非常难受。
19 |
20 | 日常我们的开发机,线上服务器,预发布服务器,都是远端linux,需要跳板机连上去,进行操作,如果不会vim,每次都把代码拷贝到本地,修改编译,在传到远端服务器,还真的麻烦。
21 |
22 | 使用VIM的话,本地,服务器,开发机,一刀流,无缝切换,爽不。
23 |
24 | IDE那么很吃内存,打开个IDE卡半天,用VIM就很轻便了,秒开!
25 |
26 | 而且在我们日常开发中,工作年头多了,都会发现没有纯粹的C++,Java开发啥的,就是 C++也得写,Java也得写,有时候写Go起个http服务,写Python处理一下数据,写shell搞个自动部署,编译啥的。 **总是就是啥语言就得写,一些以项目需求为导向!**
27 |
28 | 写语言还要切换不同的IDE,熟悉不同的操作规则,想想是不是很麻烦。
29 |
30 | 听说好像现在有的IDE可以支持很多语言了,这个我还不太了解,但能确定的是,IDE支持的语言再多,也不会有vim多。
31 |
32 | **因为vim是编辑器!**,什么都可以写,不同的语言做一下相应的配置就好,写起来都是一样的顺畅。
33 |
34 | 应该不少录友感觉vim上快捷键太多了,根本记不过来,其实这和我看IDE是一样的想法,我看IDE上哪些按钮一排一排的也太多了,我都记不过来,所以索性一套vim流 扫遍所有代码,它不香么。
35 |
36 | 而且IDE集成编译、调试、智能补全、语法高亮、工程管理等等,隐藏了太多细节,使用vim,就都自己配置,想支持什么语言就自己配置,想怎么样就怎么样,需要什么就补什么,这不是很酷么?
37 |
38 | 可能有的同学感觉什么都要自己配置,有点恐惧。但一旦配置好的就非常舒服了。
39 |
40 | **其实工程师就要逢山开路遇水搭桥,这也是最基本的素质!**
41 |
42 | 从头打在一个自己的开发利器,再舒服不过了。
43 |
44 | ## PowerVim
45 |
46 | 这里给大家介绍一下我的vim配置吧,**这套vim配置我已经打磨了将近四年**,不断调整优化,已经可以完全满足工业级打开的需求了。
47 |
48 | 所以我给它起名为PowerVim。一个真正强大的vim。
49 |
50 | ```
51 | _____ __ ___
52 | | __ \ \ \ / (_)
53 | | |__) |____ _____ _ _\ \ / / _ _ __ ___
54 | | ___/ _ \ \ /\ / / _ \ '__\ \/ / | | '_ ` _ \
55 | | | | (_) \ V V / __/ | \ / | | | | | | |
56 | |_| \___/ \_/\_/ \___|_| \/ |_|_| |_| |_|
57 | ```
58 |
59 | 这个配置我开源在Github上,地址:[https://github.com/youngyangyang04/PowerVim](https://github.com/youngyangyang04/PowerVim)
60 |
61 |
62 |
63 | 来感受一下PowerVim的使用体验,看起来很酷吧!注意这些操作都不用鼠标的,一波键盘控制流!所以我平时写代码是不碰鼠标的!
64 |
65 | 
66 |
67 | ## 安装
68 |
69 | PowerVim的安装非常简单,我已经写好了安装脚本,只要执行以下就可以安装,而且不会影响你之前的vim配置,之前的配置都给做了备份,大家看一下脚本就知道备份在哪里了。
70 |
71 | 安装过程非常简单:
72 | ```bash
73 | git clone https://github.com/youngyangyang04/PowerVim.git
74 | cd PowerVim
75 | sh install.sh
76 | ```
77 |
78 | ## 特性
79 |
80 | 目前PowerVim支持如下功能,这些都是自己配置的:
81 |
82 | * CPP、PHP、JAVA代码补全,如果需要其他语言补全,可自行配置关键字列表在PowerVim/.vim/dictionary目录下
83 | * 显示文件函数变量列表
84 | * MiniBuf显示打开过的文件
85 | * 语法高亮支持C++ (including C++11)、 Go、Java、 Php、 Html、 Json 和 Markdown
86 | * 显示git状态,和主干或分支的添加修改删除的情况
87 | * 显示项目文件目录,方便快速打开
88 | * 快速注释,使用gcc注释当前行,gc注释选中的块
89 | * 项目内搜索关键字和文件夹
90 | * 漂亮的颜色搭配和状态栏显示
91 |
92 | ## 最后
93 |
94 | 当然 还有很多,我还详细写了PowerVim的快捷键,使用方法,插件,配置,等等,都在Github主页的README上。当时我的Github上写的都是英文README,这次为了方便大家阅读,我又翻译成中文README。
95 |
96 | 
97 |
98 | Github地址:[https://github.com/youngyangyang04/PowerVim](https://github.com/youngyangyang04/PowerVim)
99 |
100 | Gitee地址:[https://gitee.com/programmercarl/power-vim](https://gitee.com/programmercarl/power-vim)
101 |
102 | 最后,因为这个vim配置因为我一直没有宣传,所以star数量很少,录友们去给个star吧,真正的开发利器,值得顶起来!
103 |
104 |
--------------------------------------------------------------------------------
/problems/前序/上海互联网公司总结.md:
--------------------------------------------------------------------------------
1 |
2 | # 上海互联网公司总结
3 |
4 | **个人总结难免有所疏忽,欢迎大家补充,公司好坏没有排名哈!**
5 |
6 | ## 一线互联网
7 |
8 | * 百度(上海)
9 | * 阿里(上海)
10 | * 腾讯(上海)
11 | * 字节跳动(上海)
12 | * 蚂蚁金服(上海)
13 |
14 | ## 外企IT/互联网/硬件
15 |
16 | * 互联网
17 | * Google(上海)
18 | * 微软(上海)
19 | * LeetCode/力扣(上海)
20 | * unity(上海)游戏引擎
21 | * SAP(上海)主要产品是ERP
22 | * PayPal(上海)在线支付鼻祖
23 | * eBay(上海)电子商务公司
24 | * 偏硬件
25 | * IBM(上海)
26 | * Tesla(上海)特斯拉
27 | * Cisco(上海)思科
28 | * Intel(上海)
29 | * AMD(上海)半导体产品领域
30 | * EMC(上海)易安信是美国信息存储资讯科技公司
31 | * NVIDIA(上海)英伟达是GPU(图形处理器)的发明者,人工智能计算的引领者
32 |
33 | ## 二线互联网
34 |
35 | * 拼多多(总部)
36 | * 饿了么(总部)阿里旗下。
37 | * 哈啰出行(总部)阿里旗下
38 | * 盒马(总部)阿里旗下
39 | * 哔哩哔哩(总部)
40 | * 阅文集团(总部)腾讯旗下
41 | * 爱奇艺(上海)百度旗下
42 | * 携程(总部)
43 | * 京东(上海)
44 | * 网易(上海)
45 | * 美团点评(上海)
46 | * 唯品会(上海)
47 |
48 | ## 硬件巨头 (有软件/互联网业务)
49 |
50 | 华为(上海)
51 |
52 | ## 三线互联网
53 |
54 | * PPTV(总部)
55 | * 微盟(总部)企业云端商业及营销解决方案提供商
56 | * 喜马拉雅(总部)
57 | * 陆金所(总部)全球领先的线上财富管理平台
58 | * 口碑(上海)阿里旗下。
59 | * 三七互娱(上海)
60 | * 趣头条(总部)
61 | * 巨人网络(总部)游戏公司
62 | * 盛大网络(总部)游戏公司
63 | * UCloud(总部)云服务提供商
64 | * 达达集团(总部)本地即时零售与配送平台
65 | * 众安保险(总部)在线财产保险
66 | * 触宝(总部)触宝输入法等多款APP
67 | * 平安系列
68 |
69 | ## 明星创业公司
70 |
71 | * 小红书(总部)
72 | * 叮咚买菜(总部)
73 | * 蔚来汽车(总部)
74 | * 七牛云(总部)
75 | * 得物App(总部)品潮流尖货装备交易、球鞋潮品鉴别查验、互动潮流社区
76 | * 收钱吧(总部)开创了中国移动支付市场“一站式收款”
77 | * 蜻蜓FM(总部)音频内容聚合平台
78 | * 流利说(总部)在线教育
79 | * Soul(总部)社交软件
80 | * 美味不用等(总部)智慧餐饮服务商
81 | * 微鲸科技(总部)专注于智能家居领域
82 | * 途虎养车(总部)
83 | * 米哈游(总部)游戏公司
84 | * 莉莉丝游戏(总部)游戏公司
85 | * 樊登读书(总部)在线教育
86 |
87 | ## AI独角兽公司
88 |
89 | * 依图科技(总部)和旷视,商汤对标,都是做安防视觉
90 | * 深兰科技(总部)致力于人工智能基础研究和应用开发
91 |
92 | ## 其他行业,涉及互联网
93 | * 花旗、摩根大通等一些列金融巨头
94 | * 百姓网
95 | * 找钢网
96 | * 安居客
97 | * 前程无忧
98 | * 东方财富
99 | * 三大电信运营商:中国移动、中国电信、中国联通
100 | * 沪江英语
101 | * 各大银行
102 |
103 | 通知:很多同学感觉自己基础还比较薄弱,想循序渐进的从头学一遍数据结构与算法,那你来对地方了。在公众号左下角「算法汇总」里已经按照各个系列难易程度排好顺序了,大家跟着文章顺序打卡学习就可以了,留言区有很多录友都在从头打卡!「算法汇总」会持续更新,大家快去看看吧!
104 |
105 | ## 总结
106 |
107 | 大家如果看了[北京有这些互联网公司,你都知道么?](https://programmercarl.com/前序/北京互联网公司总结.html)和[深圳原来有这么多互联网公司,你都知道么?](https://programmercarl.com/前序/深圳互联网公司总结.html)就可以看出中国互联网氛围最浓的当然是北京,其次就是上海!
108 |
109 | 很多人说深圳才是第二,上海没有产生BAT之类的企业。
110 |
111 | **那么来看看上海在垂直领域上是如何独领风骚的,视频领域B站,电商领域拼多多小红书,生活周边有饿了么,大众点评(现与美团合并),互联网金融有蚂蚁金服和陆金所,出行领域有行业老大携程,而且BAT在上海都有部门还是很大的团队,再加上上海众多的外企,以及金融公司(有互联网业务)**。
112 |
113 | 此时就能感受出来,上海的互联网氛围要比深圳强很多!
114 |
115 | 好了,希望这份list可以帮助到想在上海发展的录友们。
116 |
117 | 相对于北京和上海,深圳互联网公司断层很明显,腾讯一家独大,二线三线垂直行业的公司很少,所以说深圳腾讯的员工流动性相对是较低的,因为基本没得选。
118 |
119 |
120 |
121 |
122 |
123 |
124 | -----------------------
125 |
126 |
--------------------------------------------------------------------------------
/problems/前序/代码风格.md:
--------------------------------------------------------------------------------
1 |
2 | # 看了这么多代码,谈一谈代码风格!
3 |
4 | 最近看了很多录友在[leetcode-master](https://mp.weixin.qq.com/s/wZRTrA9Rbvgq1yEkSw4vfQ)上提交的代码,发现很多录友的代码其实并不规范,这一点平时在交流群和知识星球里也能看出来。
5 |
6 | 很多录友对代码规范应该了解得不多,代码看起来并不舒服。
7 |
8 | 所以呢,我给大家讲一讲代码规范,我主要以C++代码为例。
9 |
10 | 需要强调一下,代码规范并不是仅仅是让代码看着舒服,这是一个很重要的习惯。
11 |
12 | ## 题外话
13 |
14 | 工作之后,**特别是在大厂,看谁的技术牛不牛逼,不用看谁写出多牛逼的代码,就代码风格扫一眼,立刻就能看出来是正规军还是野生程序员**。
15 |
16 | 很多人甚至不屑于了解代码规范,认为实现功能就行,这种观点其实在上个世纪是很普遍的,因为那时候一般写代码不需要合作,自己一个人撸整个项目,想怎么写就怎么写。
17 |
18 | 现在一些小公司,甚至大公司里的某些技术团队也不注重代码规范,赶进度撸出功能就完事,这种情况就要分两方面看:
19 |
20 | * 第一种情况:这个项目在业务上具有巨大潜力,需要抢占市场,只要先站住市场就能赚到钱,每年年终好几十万,那项目前期还关心啥代码风格,赶进度把功能撸出来,赚钱就完事了,例如12年的微信,15年的王者荣耀。这些项目都是后期再不断优化的。
21 |
22 | * 第二种情况:这个项目没赚到钱,半死不活的,代码还没有设计也没有规范,这样对技术人员的伤害就非常大了。
23 |
24 | **而不注重代码风格的团队,99.99%都是第二种情况**,如果你赶上了第一种情况,那就恭喜你了,本文下面的内容可以不用看了。
25 |
26 | ## 代码规范
27 |
28 | ### 变量命名
29 |
30 | 这里我简单说一说规范问题。
31 |
32 | **权威的C++规范以Google为主**,我给大家下载了一份中文版本,在公众号「代码随想录」后台回复:编程规范,就可以领取。
33 |
34 | **具体的规范要以自己团队风格为主**,融入团队才是最重要的。
35 |
36 | 我先来说说变量的命名。
37 |
38 | 主流有如下三种变量规则:
39 |
40 | * 小驼峰、大驼峰命名法
41 | * 下划线命名法
42 | * 匈牙利命名法
43 |
44 | 小驼峰,第一个单词首字母小写,后面其他单词首字母大写。例如 `int myAge;`
45 |
46 | 大驼峰法把第一个单词的首字母也大写了。例如:``int MyAge;``
47 |
48 | 通常来讲 java和go都使用驼峰,C++的函数和结构体命名也是用大驼峰,**大家可以看到题解中我的C++代码风格就是小驼峰,因为leetcode上给出的默认函数的命名就是小驼峰,所以我入乡随俗**。
49 |
50 | 下划线命名法是名称中的每一个逻辑断点都用一个下划线来标记,例如:`int my_age`,**下划线命名法是随着C语言的出现流行起来的,如果大家看过UNIX高级编程或者UNIX网络编程,就会发现大量使用这种命名方式**。
51 |
52 | 匈牙利命名法是:变量名 = 属性 + 类型 + 对象描述,例如:`int iMyAge`,这种命名是一个来此匈牙利的程序员在微软内部推广起来,然后推广给了全世界的Windows开发人员。
53 |
54 | 这种命名方式在没有IDE的时代,可以很好的提醒开发人员遍历的意义,例如看到iMyAge,就知道它是一个int型的变量,而不用找它的定义,缺点是一旦改变变量的属性,那么整个项目里这个变量名字都要改动,所以带来代码维护困难。
55 |
56 | **目前IDE已经很发达了,都不用标记变量属性了,IDE就会帮我们识别了,所以基本没人用匈牙利命名法了**,虽然我不用IDE,VIM大法好。
57 |
58 | 我做了一下总结如图:
59 |
60 | 
61 |
62 | ### 水平留白(代码空格)
63 |
64 | 经常看到有的同学的代码都堆在一起,看起来都费劲,或者是有的间隔有空格,有的没有空格,很不统一,有的同学甚至为了让代码精简,把所有空格都省略掉了。
65 |
66 | 大家如果注意我题解上的代码风格,我的空格都是有统一规范的。
67 |
68 | **我所有题解的C++代码,都是严格按照Google C++编程规范来的,这样代码看起来就让人感觉清爽一些**。
69 |
70 | 我举一些例子:
71 |
72 | 操作符左右一定有空格,例如
73 | ```
74 | i = i + 1;
75 | ```
76 |
77 | 分隔符(`,` 和`;`)前一位没有空格,后一位保持空格,例如:
78 |
79 | ```
80 | int i, j;
81 | for (int fastIndex = 0; fastIndex < nums.size(); fastIndex++)
82 | ```
83 |
84 | 大括号和函数保持同一行,并有一个空格例如:
85 |
86 | ```
87 | while (n) {
88 | n--;
89 | }
90 | ```
91 |
92 | 控制语句(while,if,for)后都有一个空格,例如:
93 | ```
94 | while (n) {
95 | if (k > 0) return 9;
96 | n--;
97 | }
98 | ```
99 |
100 | 以下是我刚写的力扣283.移动零的代码,大家可以看一下整体风格,注意空格的细节!
101 | ```CPP
102 | class Solution {
103 | public:
104 | void moveZeroes(vector& nums) {
105 | int slowIndex = 0;
106 | for (int fastIndex = 0; fastIndex < nums.size(); fastIndex++) {
107 | if (nums[fastIndex] != 0) {
108 | nums[slowIndex++] = nums[fastIndex];
109 | }
110 | }
111 | for (int i = slowIndex; i < nums.size(); i++) {
112 | nums[i] = 0;
113 | }
114 | }
115 | };
116 | ```
117 |
118 | 这里关于大括号是否要重启一行?
119 |
120 | Google规范是 大括号和 控制语句保持同一行的,我个人也很认可这种写法,因为可以缩短代码的行数,特别是项目中代码行数很多的情况下,这种写法是可以提高阅读代码的效率。
121 |
122 | 当然我并不是说一定要按照Google的规范来,**代码风格其实统一就行,没有严格的说谁对谁错**。
123 |
124 | ## 总结
125 |
126 | 如果还是学生,使用C++的话,可以按照题解中我的代码风格来,还是比较标准的。
127 |
128 | 如果不是C++就自己选一种代码风格坚持下来,
129 |
130 | 如果已经工作的录友,就要融入团队的代码风格了,团队怎么写,自己就怎么来,毕竟不是一个人在战斗。
131 |
132 | 就酱,以后我还会陆续分享,关于代码,求职,学习工作之类的内容。
133 |
134 |
135 | -----------------------
136 |
137 |
138 |
--------------------------------------------------------------------------------
/problems/前序/内存消耗.md:
--------------------------------------------------------------------------------
1 |
2 | # 刷了这么多题,你了解自己代码的内存消耗么?
3 |
4 | 理解代码的内存消耗,最关键是要知道自己所用编程语言的内存管理。
5 |
6 | ## 不同语言的内存管理
7 |
8 | 不同的编程语言各自的内存管理方式。
9 |
10 | * C/C++这种内存堆空间的申请和释放完全靠自己管理
11 | * Java 依赖JVM来做内存管理,不了解jvm内存管理的机制,很可能会因一些错误的代码写法而导致内存泄漏或内存溢出
12 | * Python内存管理是由私有堆空间管理的,所有的python对象和数据结构都存储在私有堆空间中。程序员没有访问堆的权限,只有解释器才能操作。
13 |
14 | 例如Python万物皆对象,并且将内存操作封装的很好,**所以python的基本数据类型所用的内存会要远大于存放纯数据类型所占的内存**,例如,我们都知道存储int型数据需要四个字节,但是使用Python 申请一个对象来存放数据的话,所用空间要远大于四个字节。
15 |
16 | ## C++的内存管理
17 |
18 | 以C++为例来介绍一下编程语言的内存管理。
19 |
20 | 如果我们写C++的程序,就要知道栈和堆的概念,程序运行时所需的内存空间分为 固定部分,和可变部分,如下:
21 |
22 | 
23 |
24 | 固定部分的内存消耗 是不会随着代码运行产生变化的, 可变部分则是会产生变化的
25 |
26 | 更具体一些,一个由C/C++编译的程序占用的内存分为以下几个部分:
27 |
28 | * 栈区(Stack) :由编译器自动分配释放,存放函数的参数值,局部变量的值等,其操作方式类似于数据结构中的栈。
29 | * 堆区(Heap) :一般由程序员分配释放,若程序员不释放,程序结束时可能由OS收回
30 | * 未初始化数据区(Uninitialized Data): 存放未初始化的全局变量和静态变量
31 | * 初始化数据区(Initialized Data):存放已经初始化的全局变量和静态变量
32 | * 程序代码区(Text):存放函数体的二进制代码
33 |
34 | 代码区和数据区所占空间都是固定的,而且占用的空间非常小,那么看运行时消耗的内存主要看可变部分。
35 |
36 | 在可变部分中,栈区间的数据在代码块执行结束之后,系统会自动回收,而堆区间数据是需要程序员自己回收,所以也就是造成内存泄漏的发源地。
37 |
38 | **而Java、Python的话则不需要程序员去考虑内存泄漏的问题,虚拟机都做了这些事情**。
39 |
40 | ## 如何计算程序占用多大内存
41 |
42 | 想要算出自己程序会占用多少内存就一定要了解自己定义的数据类型的大小,如下:
43 |
44 | 
45 |
46 | 注意图中有两个不一样的地方,为什么64位的指针就占用了8个字节,而32位的指针占用4个字节呢?
47 |
48 | 1个字节占8个比特,那么4个字节就是32个比特,可存放数据的大小为2^32,也就是4G空间的大小,即:可以寻找4G空间大小的内存地址。
49 |
50 | 大家现在使用的计算机一般都是64位了,所以编译器也都是64位的。
51 |
52 | 安装64位的操作系统的计算机内存都已经超过了4G,也就是指针大小如果还是4个字节的话,就已经不能寻址全部的内存地址,所以64位编译器使用8个字节的指针才能寻找所有的内存地址。
53 |
54 | 注意2^64是一个非常巨大的数,对于寻找地址来说已经足够用了。
55 |
56 | ## 内存对齐
57 |
58 | 再介绍一下内存管理中另一个重要的知识点:**内存对齐**。
59 |
60 | **不要以为只有C/C++才会有内存对齐,只要可以跨平台的编程语言都需要做内存对齐,Java、Python都是一样的**。
61 |
62 | 而且这是面试中面试官非常喜欢问到的问题,就是:**为什么会有内存对齐?**
63 |
64 | 主要是两个原因
65 |
66 | 1. 平台原因:不是所有的硬件平台都能访问任意内存地址上的任意数据,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。为了同一个程序可以在多平台运行,需要内存对齐。
67 |
68 | 2. 硬件原因:经过内存对齐后,CPU访问内存的速度大大提升。
69 |
70 | 可以看一下这段C++代码输出的各个数据类型大小是多少?
71 |
72 | ```CPP
73 | struct node{
74 | int num;
75 | char cha;
76 | }st;
77 | int main() {
78 | int a[100];
79 | char b[100];
80 | cout << sizeof(int) << endl;
81 | cout << sizeof(char) << endl;
82 | cout << sizeof(a) << endl;
83 | cout << sizeof(b) << endl;
84 | cout << sizeof(st) << endl;
85 | }
86 | ```
87 |
88 | 看一下和自己想的结果一样么, 我们来逐一分析一下。
89 |
90 | 其输出的结果依次为:
91 |
92 | ```
93 | 4
94 | 1
95 | 400
96 | 100
97 | 8
98 | ```
99 |
100 | 此时会发现,和单纯计算字节数的话是有一些误差的。
101 |
102 | 这就是因为内存对齐的原因。
103 |
104 | 来看一下内存对齐和非内存对齐产生的效果区别。
105 |
106 | CPU读取内存不是一次读取单个字节,而是一块一块的来读取内存,块的大小可以是2,4,8,16个字节,具体取多少个字节取决于硬件。
107 |
108 | 假设CPU把内存划分为4字节大小的块,要读取一个4字节大小的int型数据,来看一下这两种情况下CPU的工作量:
109 |
110 | 第一种就是内存对齐的情况,如图:
111 |
112 | 
113 |
114 | 一字节的char占用了四个字节,空了三个字节的内存地址,int数据从地址4开始。
115 |
116 | 此时,直接将地址4,5,6,7处的四个字节数据读取到即可。
117 |
118 | 第二种是没有内存对齐的情况如图:
119 |
120 | 
121 |
122 | char型的数据和int型的数据挨在一起,该int数据从地址1开始,那么CPU想要读这个数据的话来看看需要几步操作:
123 |
124 | 1. 因为CPU是四个字节四个字节来寻址,首先CPU读取0,1,2,3处的四个字节数据
125 | 2. CPU读取4,5,6,7处的四个字节数据
126 | 3. 合并地址1,2,3,4处四个字节的数据才是本次操作需要的int数据
127 |
128 | 此时一共需要两次寻址,一次合并的操作。
129 |
130 | **大家可能会发现内存对齐岂不是浪费的内存资源么?**
131 |
132 | 是这样的,但事实上,相对来说计算机内存资源一般都是充足的,我们更希望的是提高运行速度。
133 |
134 | **编译器一般都会做内存对齐的优化操作,也就是说当考虑程序真正占用的内存大小的时候,也需要认识到内存对齐的影响**。
135 |
136 |
137 | ## 总结
138 |
139 | 不少同学对这方面的知识很欠缺,基本处于盲区,通过这一篇大家可以初步补齐一下这块。
140 |
141 | 之后也可以有意识的去学习自己所用的编程语言是如何管理内存的,这些也是程序员的内功。
142 |
143 |
144 |
145 |
146 | -----------------------
147 |
148 |
149 |
--------------------------------------------------------------------------------
/problems/前序/刷力扣用不用库函数.md:
--------------------------------------------------------------------------------
1 | # 究竟什么时候用库函数,什么时候要自己实现
2 |
3 | 在[知识星球](https://programmercarl.com/other/kstar.html)里有录友问我,刷题究竟要不要用库函数? 刷题的时候总是禁不住库函数的诱惑,如果都不用库函数一些题目做起来还很麻烦。
4 |
5 | 估计不少录友都有这个困惑,我来说一说对于库函数的使用。
6 |
7 | 一些同学可能比较喜欢看力扣上直接调用库函数的评论和题解,**其实我感觉娱乐一下还是可以的,但千万别当真,别沉迷!**
8 |
9 | 例如:[字符串:151. 翻转字符串里的单词](https://programmercarl.com/0151.%E7%BF%BB%E8%BD%AC%E5%AD%97%E7%AC%A6%E4%B8%B2%E9%87%8C%E7%9A%84%E5%8D%95%E8%AF%8D.html)这道题目本身是综合考察同学们对字符串的处理能力,如果 split + reverse的话,那就失去了题目的意义了。
10 |
11 | 有的同学可能不屑于实现这么简单的功能,直接调库函数完事,把字符串分成一个个单词,一想就是那么一回事,多简单。
12 |
13 | 相信我,很多面试题都是一想很简单,实现起来一堆问题。 所以刷力扣本来就是为面试,也为了提高自己的代码能力,扎实一点没坏处。
14 |
15 | **那么平时写算法题目就全都不用库函数了么?**
16 |
17 | 当然也不是,这里我给大家提供一个标准。
18 |
19 | **如果题目关键的部分直接用库函数就可以解决,建议不要使用库函数**。
20 |
21 | **如果库函数仅仅是 解题过程中的一小部分,并且你已经很清楚这个库函数的内部实现原理的话,那么直接用库函数。**
22 |
23 | 使用库函数最大的忌讳就是不知道这个库函数怎么实现的,也不知道其时间复杂度,上来就用,这样写出来的算法,时间复杂度自己都掌握不好的。
24 |
25 | 例如for循环里套一个字符串的insert,erase之类的操作,你说时间复杂度是多少呢,很明显是O(n^2)的时间复杂度了。
26 |
27 | 在刷题的时候本着我说的标准来使用库函数,相信对大家会有所帮助!
28 |
29 |
--------------------------------------------------------------------------------
/problems/前序/力扣上的代码在本地编译运行.md:
--------------------------------------------------------------------------------
1 | # 力扣上的代码想在本地编译运行?
2 |
3 |
4 | 很多录友都问过我一个问题,就是力扣上的代码如何在本地编译运行?
5 |
6 | 其实在代码随想录刷题群里也经常出现这个场景,就是录友发一段代码上来,问大家这个代码怎么有问题? 如果我看到了一般我的回复:都是把那几个变量或者数组打印一下看看对不对,就知道了。
7 |
8 | 然后录友就问了:如何打日志呢?
9 |
10 | 其实在力扣上打日志也挺方便的,我一般调试就是直接在力扣上打日志,偶尔需要把代码粘到本地来运行添加日志debug一下。
11 |
12 | 在力扣上直接打日志,这个就不用讲,C++的话想打啥直接cout啥就可以了。
13 |
14 | 我来说一说力扣代码如何在本地运行。
15 |
16 | 毕竟我们天天用力扣刷题,也应该知道力扣上的代码如何在本地编译运行。
17 |
18 | 其实挺简单的,大家看一遍就会了。
19 |
20 | 我拿我们刚讲过的这道题[动态规划:使用最小花费爬楼梯](https://programmercarl.com/0746.使用最小花费爬楼梯.html)来做示范。
21 |
22 | 力扣746. 使用最小花费爬楼梯,完整的可以在直接本地运行的C++代码如下:
23 |
24 | ```CPP
25 | #include
26 | #include
27 | using namespace std;
28 |
29 | class Solution {
30 | public:
31 | int minCostClimbingStairs(vector& cost) {
32 | vector dp(cost.size());
33 | dp[0] = cost[0];
34 | dp[1] = cost[1];
35 | for (int i = 2; i < cost.size(); i++) {
36 | dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i];
37 | }
38 | return min(dp[cost.size() - 1], dp[cost.size() - 2]);
39 | }
40 | };
41 |
42 | int main() {
43 | int a[] = {1, 100, 1, 1, 1, 100, 1, 1, 100, 1};
44 | vector cost(a, a + sizeof(a) / sizeof(int));
45 | Solution solution;
46 | cout << solution.minCostClimbingStairs(cost) << endl;
47 | }
48 | ```
49 |
50 | 大家可以拿去跑一跑,直接粘到编译器上就行了。
51 |
52 | 我用的是linux下gcc来编译的,估计粘到其他编译器也没问题。
53 |
54 | 代码中可以看出,其实就是定义个main函数,构造个输入用例,然后定义一个solution变量,调用minCostClimbingStairs函数就可以了。
55 |
56 | 此时大家就可以随意构造测试数据,然后想怎么打日志就怎么打日志,没有找不出的bug。
57 |
58 |
59 |
60 |
61 |
62 |
63 | -----------------------
64 |
65 |
--------------------------------------------------------------------------------
/problems/前序/北京互联网公司总结.md:
--------------------------------------------------------------------------------
1 | # 北京互联网公司总结
2 |
3 | 欢迎大家参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!
4 |
5 |
6 | **个人总结难免有所疏忽,欢迎大家补充,公司好坏没有排名哈!**
7 |
8 | 如果要在北京找工作,这份list可以作为一个大纲,寻找自己合适的公司。
9 |
10 | ## 一线互联网
11 |
12 | * 百度(总部)
13 | * 阿里(北京)
14 | * 腾讯(北京)
15 | * 字节跳动(总部)
16 |
17 | ## 外企
18 |
19 | * 微软(北京)微软中国主要就是北京和苏州
20 | * Hulu(北京)美国的视频网站,听说福利待遇超级棒
21 | * Airbnb(北京)房屋租赁平台
22 | * Grab(北京)东南亚第一大出行 App
23 | * 印象笔记(北京)evernote在中国的独立品牌
24 | * FreeWheel(北京)美国最大的视频广告管理和投放平台
25 | * amazon(北京)全球最大的电商平台
26 |
27 | ## 二线互联网
28 |
29 | * 美团点评(总部)
30 | * 京东(总部)
31 | * 网易(北京)
32 | * 滴滴出行(总部)
33 | * 新浪(总部)
34 | * 快手(总部)
35 | * 搜狐(总部)
36 | * 搜狗(总部)
37 | * 360(总部)
38 |
39 | ## 硬件巨头 (有软件/互联网业务)
40 |
41 | * 华为(北京)
42 | * 联想(总部)
43 | * 小米(总部)后序要搬到武汉,互联网业务也是小米重头
44 |
45 | ## 三线互联网
46 |
47 | * 爱奇艺(总部)
48 | * 去哪儿网(总部)
49 | * 知乎(总部)
50 | * 豆瓣(总部)
51 | * 当当网(总部)
52 | * 完美世界(总部)游戏公司
53 | * 昆仑万维(总部)游戏公司
54 | * 58同城(总部)
55 | * 陌陌(总部)
56 | * 金山软件(北京)包括金山办公软件
57 | * 用友网络科技(总部)企业服务ERP提供商
58 | * 映客直播(总部)
59 | * 猎豹移动(总部)
60 | * 一点资讯(总部)
61 | * 国双(总部)企业级大数据和人工智能解决方案提供商
62 |
63 | ## 明星创业公司
64 |
65 | 可以发现北京一堆在线教育的公司,可能教育要紧盯了政策变化,所以都要在北京吧
66 |
67 | * 好未来(总部)在线教育
68 | * 猿辅导(总部)在线教育
69 | * 跟谁学(总部)在线教育
70 | * 作业帮(总部)在线教育
71 | * VIPKID(总部)在线教育
72 | * 雪球(总部)股市资讯
73 | * 唱吧(总部)
74 | * 每日优鲜(总部)让每个人随时随地享受食物的美好
75 | * 微店(总部)
76 | * 罗辑思维(总部)得到APP
77 | * 值得买科技(总部)让每一次消费产生幸福感
78 | * 拉勾网(总部)互联网招聘
79 |
80 | ## AI独角兽公司
81 |
82 | * 商汤科技(总部)专注于计算机视觉和深度学习
83 | * 旷视科技(总部)人工智能产品和解决方案公司
84 | * 第四范式(总部)人工智能技术与服务提供商
85 | * 地平线机器人(总部)边缘人工智能芯片的全球领导者
86 | * 寒武纪(总部)全球智能芯片领域的先行者
87 |
88 | ## 互联网媒体
89 |
90 | * 央视网
91 | * 搜房网
92 | * 易车网
93 | * 链家网
94 | * 自如网
95 | * 汽车之家
96 |
97 |
98 | 北京的互联网氛围绝对是最好的(暂不讨论户口和房价问题),大家如果看了[深圳原来有这么多互联网公司,你都知道么?](https://programmercarl.com/前序/深圳互联网公司总结.html)这篇之后,**会发现北京互联网外企和二线互联网公司数量多的优势,在深圳的互联网公司断档比较严重,如果去不了为数不多的一线公司,可选择的余地就非常少了,而北京选择的余地就很多!**
99 |
100 | 相对来说,深圳的硬件企业更多一些,因为珠三角制造业配套比较完善。而大多数互联网公司其实就是媒体公司,当然要靠近政治文化中心,这也是有原因的。
101 |
102 | 就酱,我也会陆续整理其他城市的互联网公司,希望对大家有所帮助。
103 |
104 |
105 |
106 |
107 |
108 | -----------------------
109 |
110 |
--------------------------------------------------------------------------------
/problems/前序/广州互联网公司总结.md:
--------------------------------------------------------------------------------
1 | # 广州互联网公司总结
2 |
3 | 欢迎大家参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!
4 |
5 |
6 | **个人总结难免有所疏忽,欢迎大家补充,公司好坏没有排名哈!**
7 |
8 | ## 一线互联网
9 |
10 | * 微信(总部) 有点难进!
11 | * 字节跳动(广州)
12 |
13 | ## 二线
14 | * 网易(总部)主要是游戏
15 |
16 | ## 三线
17 |
18 | * 唯品会(总部)
19 | * 欢聚时代(总部)旗下YY,虎牙,YY最近被浑水做空,不知百度还要不要收购了
20 | * 酷狗音乐(总部)
21 | * UC浏览器(总部)现在隶属阿里创始人何小鹏现在搞小鹏汽车
22 | * 荔枝FM(总部)用户可以在手机上开设自己的电台和录制节目
23 | * 映客直播(总部)股票已经跌成渣了
24 | * 爱范儿(总部)
25 | * 三七互娱(总部)游戏公司
26 | * 君海游戏(总部)游戏公司
27 | * 4399游戏(总部)游戏公司
28 | * 多益网络(总部)游戏公司
29 |
30 | ## 硬件巨头 (有软件/互联网业务)
31 | * 小鹏汽车(总部)新能源汽车小霸王
32 |
33 | ## 创业公司
34 |
35 | * 妈妈网(总部)母婴行业互联网公司
36 | * 云徙科技(总部)数字商业云服务提供商
37 | * Fordeal(总部)中东领先跨境电商平台
38 | * Mobvista(总部)移动数字营销
39 | * 久邦GOMO(总部)游戏
40 | * 深海游戏(总部)游戏
41 |
42 | ## 国企
43 |
44 | * 中国电信广州研发(听说没有996)
45 |
46 |
47 | ## 总结
48 |
49 | 同在广东省,难免不了要和深圳对比,大家如果看了这篇:[深圳原来有这么多互联网公司,你都知道么?](https://programmercarl.com/前序/深圳互联网公司总结.html)就能感受到鲜明的对比了。
50 |
51 | 广州大厂高端岗位其实比较少,本土只有微信和网易,微信呢毕竟还是腾讯的分部,而网易被很多人认为是杭州企业,其实网易总部在广州。
52 |
53 | 广州是唯一一个一线城市没有自己本土互联网巨头的城市,所以网易选择在广州扎根还是很正确的,毕竟杭州是阿里的天下,广州也应该扶持一把本土的互联网公司。
54 |
55 | 虽然对于互联网从业人员来说,广州的岗位要比深圳少很多,**但是!!广州的房价整体要比深圳低30%左右,而且广州的教育,医疗,公共资源完全碾压深圳**。
56 |
57 | 教育方面:大学广州有两个985,四个211,深圳这方面就不用说了,大家懂得。
58 |
59 | 基础教育方面深圳的小学初中高中学校数量远远不够用,小孩上学竞争很激烈,我也是经常听同事们说,耳濡目染了。
60 |
61 | 而医疗上基本深圳看不了的病都要往广州跑,深圳的医院数量也不够用。
62 |
63 | 在生活节奏上,广州更慢一些,更有生活的气息,而深圳生存下去的气息更浓烈一些。
64 |
65 | 所以很多在深圳打拼多年的IT从业者选择去广州安家也是有原因的。
66 |
67 | 但也有很多从广州跑到深圳的,深圳发展的机会更多,而广州教育医疗更丰富,房价不高(相对深圳)。
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | -----------------------
77 |
78 |
--------------------------------------------------------------------------------
/problems/前序/成都互联网公司总结.md:
--------------------------------------------------------------------------------
1 |
2 | # 成都互联网公司总结
3 | 欢迎大家参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!
4 |
5 |
6 | **排名不分先后,个人总结难免有所疏漏,欢迎补充!**
7 |
8 | ## 一线互联网
9 | * 腾讯(成都) 游戏,王者荣耀就在成都!
10 | * 阿里(成都)
11 | * 蚂蚁金服(成都)
12 | * 字节跳动(成都)
13 |
14 | ## 硬件巨头 (有软件/互联网业务)
15 |
16 | * 华为(成都)
17 | * OPPO(成都)
18 |
19 | ## 二线互联网
20 |
21 | * 京东(成都)
22 | * 美团(成都)
23 | * 滴滴(成都)
24 |
25 | ## 三线互联网
26 |
27 | * 完美世界 (成都)游戏
28 | * 聚美优品 (成都)
29 | * 陌陌 (成都)
30 | * 爱奇艺(成都)
31 |
32 | ## 外企互联网
33 |
34 | * NAVER China (成都)搜索引擎公司,主要针对韩国市场
35 |
36 | ## 创业公司
37 |
38 | * tap4fun(总部)游戏
39 | * 趣乐多(总部)游戏
40 | * 天上友嘉(总部)游戏
41 | * 三七互娱(成都)游戏
42 | * 咕咚(总部)智能运动
43 | * 百词斩(总部)在线教育
44 | * 晓多科技(总部)AI方向
45 | * 萌想科技(总部)实习僧
46 | * Camera360(总部)移动影像社区
47 | * 医联 (总部)医疗解决方案提供商
48 | * 小明太极 (总部)原创漫画文娱内容网站以及相关APP
49 | * 小鸡叫叫(总部)致力于儿童教育的智慧解决方案
50 |
51 |
52 | ## AI独角兽公司
53 |
54 | * 科大讯飞(成都)
55 | * 商汤(成都)
56 |
57 | ## 总结
58 |
59 | 可以看出成都相对一线城市的互联网氛围确实差了很多。**但是!成都已经是在内陆城市中甚至二线城市中的佼佼者了!**
60 |
61 | 从公司的情况上也可以看出:**成都互联网行业目前的名片是“游戏”**,腾讯、完美世界等大厂,还有无数小厂都在成都搞游戏,可能成都的天然属性就是娱乐,这里是游戏的沃土吧。
62 |
63 | 相信大家如果在一些招聘平台上去搜,其实很多公司都在成都,但都是把客服之类的工作安排在成都,而我在列举的时候尽量把研发相关在成都的公司列出来,这样对大家更有帮助。
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 | -----------------------
72 |
73 |
--------------------------------------------------------------------------------
/problems/前序/杭州互联网公司总结.md:
--------------------------------------------------------------------------------
1 | # 杭州互联网公司总结
2 |
3 | 欢迎大家参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!
4 |
5 |
6 | **个人总结难免有所疏忽,欢迎大家补充,公司好坏没有排名哈!**
7 |
8 | ## 一线互联网
9 |
10 | * 阿里巴巴(总部)
11 | * 蚂蚁金服(总部)阿里旗下
12 | * 阿里云(总部)阿里旗下
13 | * 网易(杭州) 网易云音乐
14 | * 字节跳动(杭州)抖音分部
15 |
16 | ## 外企
17 |
18 | * ZOOM (杭州研发中心)全球知名云视频会议服务提供商
19 | * infosys(杭州)印度公司,据说工资相对不高
20 | * 思科(杭州)
21 |
22 | ## 二线互联网
23 |
24 | * 滴滴(杭州)
25 | * 快手(杭州)
26 |
27 | ## 硬件巨头 (有软件/互联网业务)
28 |
29 | * 海康威视(总部)安防三巨头
30 | * 浙江大华(总部)安防三巨头
31 | * 杭州宇视(总部) 安防三巨头
32 | * 萤石
33 | * 华为(杭州)
34 | * vivo(杭州)
35 | * oppo(杭州)
36 | * 魅族(杭州)
37 |
38 | ## 三线互联网
39 |
40 | * 蘑菇街(总部)女性消费者的电子商务网站
41 | * 有赞(总部)帮助商家进行网上开店、社交营销
42 | * 菜鸟网络(杭州)
43 | * 花瓣网(总部)图片素材领导者
44 | * 兑吧(总部)用户运营服务平台
45 | * 同花顺(总部)网上股票证券交易分析软件
46 | * 51信用卡(总部)信用卡管理
47 | * 虾米(总部)已被阿里收购
48 | * 曹操出行(总部)
49 | * 口碑网 (总部)
50 |
51 | ## AI独角兽公司
52 |
53 | * 旷视科技(杭州)
54 | * 商汤(杭州)
55 |
56 | ## 创业公司
57 |
58 | * e签宝(总部)做电子签名
59 | * 婚礼纪(总部)好多结婚的朋友都用
60 | * 大搜车(总部)中国领先的汽车交易服务供应商
61 | * 二更(总部)自媒体
62 | * 丁香园(总部)
63 |
64 | ## 总结
65 |
66 | 杭州距离上海非常近,难免不了和上海做对比,上海是金融之都,如果看了[上海有这些互联网公司,你都知道么?](https://programmercarl.com/前序/上海互联网公司总结.html)就会发现上海互联网也是仅次于北京的。
67 |
68 | 而杭州是阿里的大本营,到处都有阿里的影子,虽然有网易在,但是也基本是盖过去了,很多中小公司也都是阿里某某高管出来创业的。
69 |
70 | 杭州的阿里带动了杭州的电子商务领域热度非常高,如果你想做电商想做直播带货想做互联网营销,杭州都是圣地!
71 |
72 | 如果要是写代码的话,每年各种节日促销,加班996应该是常态,电商公司基本都是这样,当然如果赶上一个好领导的话,回报也是很丰厚的。
73 |
74 | 「代码随想录」一直都是干活满满,值得介绍给每一位学习算法的同学!
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 | -----------------------
83 |
84 |
--------------------------------------------------------------------------------
/problems/前序/深圳互联网公司总结.md:
--------------------------------------------------------------------------------
1 |
2 | # 深圳互联网公司总结
3 |
4 | 欢迎大家参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!
5 |
6 | **个人总结难免有所疏忽,欢迎大家补充,公司好坏没有排名哈!**
7 |
8 | ## 一线互联网
9 |
10 | * 腾讯(总部深圳)
11 | * 百度(深圳)
12 | * 阿里(深圳)
13 | * 字节跳动(深圳)
14 |
15 | ## 硬件巨头 (有软件/互联网业务)
16 |
17 | * 华为(总部深圳)
18 | * 中兴(总部深圳)
19 | * 海能达(总部深圳)
20 | * oppo(总部深圳)
21 | * vivo(总部深圳)
22 | * 深信服(总部深圳)
23 | * 大疆(总部深圳,无人机巨头)
24 | * 一加手机(总部深圳)
25 | * 柔宇科技(最近口碑急转直下)
26 |
27 | ## 二线大厂
28 |
29 | * 快手(深圳)
30 | * 京东(深圳)
31 | * 顺丰(总部深圳)
32 |
33 | ## 三线大厂
34 |
35 | * 富途证券(2020年成功赴美上市,主要经营港股美股)
36 | * 微众银行(总部深圳)
37 | * 招银科技(总部深圳)
38 | * 平安系列(平安科技、平安寿险、平安产险、平安金融、平安好医生等)
39 | * Shopee(21年有裁员风波)
40 | * 有赞(深圳)
41 | * 迅雷(总部深圳)
42 | * 金蝶(总部深圳)
43 | * 随手记(总部深圳)
44 |
45 | ## AI独角兽公司
46 |
47 | * 商汤科技(人工智能领域的独角兽)
48 | * 追一科技(一家企业级智能服务AI公司)
49 | * 超多维科技 (计算机视觉、裸眼3D)
50 | * 优必选科技 (智能机器人、人脸识别)
51 |
52 | ## 明星创业公司
53 |
54 | * 丰巢科技(让生活更简单)
55 | * 人人都是产品经理(全球领先的产品经理和运营人 学习、交流、分享平台)
56 | * 大丰收(综合农业互联网服务平台)
57 | * 小鹅通(专注新教育的技术服务商)
58 | * 货拉拉(拉货就找货拉拉)
59 | * 编程猫(少儿编程教育头部企业)
60 | * HelloTalk(全球最大的语言学习社交社区)
61 | * 大宇无限( 拥有SnapTube, Lark Player 等多款广受海外新兴市场用户欢迎的产品)
62 | * 知识星球(深圳大成天下公司出品)
63 | * XMind(隶属深圳市爱思软件技术有限公司,思维导图软件)
64 | * 小赢科技(以技术重塑人类的金融体验)
65 |
66 | ## 其他行业(有软件/互联网业务)
67 |
68 | * 三大电信运营商:中国移动、中国电信、中国联通
69 | * 房产企业:恒大(暴雷)、万科
70 | * 中信深圳
71 | * 广发证券,深交所
72 | * 珍爱网(珍爱网是国内知名的婚恋服务网站之一)
73 |
74 |
75 |
76 |
77 |
78 | -----------------------
79 |
80 |
--------------------------------------------------------------------------------
/problems/前序/程序员写文档工具.md:
--------------------------------------------------------------------------------
1 |
2 | # 程序员应该用什么用具来写文档?
3 |
4 | 欢迎大家参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!
5 |
6 |
7 |
8 | Carl平时写东西,都是统一使用markdown,包括题解啊,笔记啊,所以这里给大家安利一波markdown对程序员的重要性!
9 |
10 | 程序员为什么要学习markdown呢?
11 |
12 | **一个让你难以拒绝的理由:markdown可以让你养成了记录的习惯**。
13 |
14 | 自从使用了markdown之后,就喜欢了写文档无法自拔,包括记录工作日志,记录周会,记录季度计划,记录学习目标,写各种设计文档。
15 |
16 | 有一种写代码一样的舒爽,markdown 和vim 一起用,简直绝配!
17 |
18 | 那来说一说markdown的好处。
19 |
20 | ## 为什么需要markdown
21 |
22 | 大家可能想为什么要使用markdown来写文档,而不用各种可以点击鼠标点点的那种所见即所得的工具来记笔记,例如word,云笔记之类的。
23 |
24 | 首先有如下几点:
25 |
26 | 1. Markdown可以在任何地方使用
27 |
28 | **可以使用它来创建网站,笔记,电子书,演讲稿,邮件信息和各种技术文档**
29 |
30 | 2. Markdown很轻便
31 |
32 | 事实上,**包含Markdown格式文本的文件可以被任何一个应用打开**。
33 |
34 | 如果感觉不喜欢当前使用的Markdown渲染应用,可以使用其他渲染应用来打开。
35 |
36 | 而鲜明对比的就是Microsoft Word,必须要使用特定的软件才能打开 .doc 或者 .docx的文档 而且可能还是乱码或者格式乱位。
37 |
38 | 3. Markdown是独立的平台
39 |
40 | **你可以创建Markdown格式文本的文件在任何一个可以运行的操作系统上**
41 |
42 | 4. Markdown已经无处不在
43 |
44 | **程序员的世界到处都是Markdown**,像简书,GitChat, GitHub,csdn等等都支持Markdown文档,正宗的官方技术文档都是使用Markdown来写的。
45 |
46 | 使用Markdown不仅可以非常方便的记录笔记,而且可以直接导出对应的网站内容,导出可打印的文档
47 |
48 | 至于markdown的语法,真的非常简单,不需要花费很长的时间掌握!
49 |
50 | 而且一旦你掌握了它,你就可以在任何地方任何平台使用Markdown来记录笔记,文档甚至写书。
51 |
52 | 很多人使用Markdown来创建网站的内容,但是Markdown更加擅长于格式化的文本内容,**使用Markdown 根部不用担心格式问题,兼容问题**。
53 |
54 | 很多后台开发程序员的工作环境是linux,linux下写文档最佳选择也是markdown。
55 |
56 | **我平时写代码,写文档都习惯在linux系统下进行(包括我的mac),所以我更喜欢vim + markdown**。
57 |
58 | 关于vim的话,后面我也可以单独介绍一波!
59 |
60 | ## Markdown常用语法
61 |
62 | 我这里就简单列举一些最基本的语法。
63 |
64 | ### 标题
65 |
66 | 使用'#' 可以展现1-6级别的标题
67 |
68 | ```
69 | # 一级标题
70 | ## 二级标题
71 | ### 三级标题
72 | ```
73 |
74 | ### 列表
75 |
76 | 使用 `*` 或者 `+` 或者 `-` 或者 `1. ` `2. ` 来表示列表
77 |
78 | 例如:
79 |
80 | ```
81 | * 列表1
82 | * 列表2
83 | * 列表3
84 | ```
85 |
86 | 效果:
87 | * 列表1
88 | * 列表2
89 | * 列表3
90 |
91 | ### 链接
92 |
93 | 使用 `[名字](url)` 表示连接,例如`[Github地址](https://github.com/youngyangyang04/Markdown-Resume-Template)`
94 |
95 |
96 | ### 添加图片
97 |
98 | 添加图片`` 例如``
99 |
100 | ### html 标签
101 |
102 | Markdown支持部分html,例如这样
103 |
104 | ```
105 | XXX
106 | ```
107 |
108 | ## Markdown 渲染
109 |
110 | 有如下几种方式渲染Markdown文档
111 |
112 | * 使用github来渲染,也就是把自己的 .md 文件传到github上,就是有可视化的展现,大家会发现github上每个项目都有一个README.md
113 | * 使用谷歌浏览器安装MarkDown Preview Plus插件,也可以打开markdown文件,但是渲染效果不太好
114 | * mac下建议使用macdown来打开 markdown文件,然后就可以直接导出pdf来打印了
115 | * window下可以使用Typora来打开markdown文件,同样也可以直接导出pdf来打印
116 |
117 | ## Markdown学习资料
118 |
119 | 我这里仅仅是介绍了几个常用的语法,刚开始学习Markdown的时候语法难免会忘。
120 |
121 | 所以建议把这个markdown demo:https://markdown-it.github.io/ 收藏一下,平时用到哪里了忘了就看一看。
122 |
123 | 就酱,后面我还会陆续给大家安利一些编程利器。
124 |
125 | ## 总结
126 |
127 | 如果还没有掌握markdown的你还在等啥,赶紧使用markdown记录起来吧
128 |
129 |
130 |
131 |
132 | -----------------------
133 |
134 |
--------------------------------------------------------------------------------
/problems/前序/程序员简历.md:
--------------------------------------------------------------------------------
1 | # 程序员的简历应该这么写!!(附简历模板)
2 |
3 | Carl校招社招都拿过大厂的offer,同时也看过很多应聘者的简历,这里把自己总结的简历技巧以及常见问题给大家梳理一下。
4 |
5 | ## 简历篇幅
6 |
7 | 首先程序员的简历力求简洁明了,不用设计上要过于复杂。
8 |
9 | 对于校招生,一页简历就够了,社招的话两页简历便可。
10 |
11 | 有的校招生说自己的经历太多了,简历要写出两三页,实际上基本是无关内容太多或者描述太啰唆,例如多过的校园活动,学生会经历等等。
12 |
13 | 既然是面试技术岗位,其他的方面一笔带过就好。
14 |
15 |
16 | ## 谨慎使用“精通”两字
17 |
18 | 应届生或者刚毕业的程序员在写简历的时候 **切记不要写精通某某语言**,如果真的学的很好,**推荐写“熟悉”或者“掌握”**。
19 |
20 | 但是有的同学可能仅仅使用一些语言例如go或者python写了一些小东西,或者了解一些语言的语法,就直接写上熟悉C++、JAVA、GO、PYTHON ,这也是大忌,如果C++更了解的话,建议写熟悉C++,了解JAVA、GO、PYTHON。
21 |
22 | **词语的强烈程度:精通 > 熟悉(推荐使用)> 掌握(推荐使用)> 了解(推荐使用)**
23 |
24 | 还有做好心理准备,一旦我们写了熟悉某某语言,这门语言就一定是面试中重点考察的一个点。
25 |
26 | 例如写了熟悉C++, 那么继承、多态、封装、虚函数、C++11的一些特性、STL就一定会被问道。
27 |
28 | **所以简历上写着熟悉哪一门语言,在准备面试的时候重点准备,其他语言几乎可以不用看了,面试官在面试中通常只会考察一门编程语言**。
29 |
30 |
31 | ## 拿不准的绝对不要写在简历上
32 |
33 | **不要为了简历上看上去很丰富,就写很多内容上去,内容越多,面试中考点就越多**。
34 |
35 | 简历中突出自己技能的几个点,而不是面面俱到。
36 |
37 | 想想看,面试官一定是拿着你的简历开始问问题的,**如果因为仅仅想展示自己多会一点点的东西就都写在简历上,等于给自己挖了一个“大坑”**。
38 |
39 | 例如仅仅部署过nginx服务器,就在简历上写熟悉nginx,那面试官可能上来就围绕着nginx问很多问题,同学们如果招架不住,然后说:“我仅仅部署过,底层实现我都不了解。这样就是让面试官有些失望”。
40 |
41 | **同时尽量不要写代码行数10万+ 在简历上**,这就相当于提高了面试官的期望。
42 |
43 | 首先就是代码行数10W+ 无从考证,而且这无疑大大提高的面试官的期望和面试官问问题的范围,这相当于告诉面试官“我写代码没问题,你就尽管问吧”。
44 |
45 | 如果简历上再没有侧重点的话,面试官就开始铺天盖地问起来,恐怕大家回答的效果也不会太好。
46 |
47 | ## 项目经验应该如何写
48 |
49 | **项目经验中要突出自己的贡献**,不要描述一遍项目就完事,要突出自己的贡献,是添加了哪些功能,还是优化了那些性能指数,最后再说说受益怎么样。
50 |
51 | 例如这个功能被多少人使用,例如性能提升了多少倍。
52 |
53 | 其实很多同学的一个通病就是在面试中说不出自己项目的难点,项目经历写了一大堆,各种框架数据库的使用都写上了,却答不出自己项目中的难点。
54 |
55 | 有的同学可能心里会想:“自己的项目没有什么难点,就是按照功能来做,遇到不会配置的不会调节的,就百度一下”。
56 |
57 | 其实大多数人做项目的时候都是这样的,不是每个项目都有什么难点,可是为什么一样的项目经验,别人就可以在难点上说出一二三来呢?
58 |
59 | 这里还是有一些技巧的,首先是**做项目的时候时刻保持着对难点的敏感程度**,很多我们费尽周折解决了一个问题,然后自己也不做记录,就忘掉了,**此时如果及时将自己的思考过程记录下来,就是面试中的重要素材,养成这样的习惯非常重要**。
60 |
61 | 很多同学埋怨自己的项目没难点,其实不然,**找到项目中的一点,深挖下去就会遇到难点,解决它,这种经历就可以拿来在面试中来说了**。
62 |
63 | 例如使用java完成的项目,在深挖一下Java内存管理,看看是不是可以减少一些虚拟机上内存的压力。
64 |
65 | 所以很多时候 **不是自己的项目没有难点,而是自己准备的不充分**。
66 |
67 | 项目经验是面试官一定会问的,那么不是每一个面试都是主动问项目中有哪些亮点或者难点,这时候就需要我们自己主动去说自己项目中的难点。
68 |
69 | ## 变被动为主动
70 |
71 | 再说一个面试中如何变被动为主动的技巧,例如自己的项目是一套分布式系统,我们在介绍项目的时候主动说:“项目中的难点就是分布式数据一致性的问题。”。
72 |
73 | **此时就应该知道面试官定会问:“你是如何解决数据一致性的?”**。
74 |
75 | 如果你对数据一致性协议的使用和原理足够的了解的话,就可以和面试官侃侃而谈了。
76 |
77 | 我们在简历中突出项目的难点在于数据一致性,并且**我们之前就精心准备一致性协议,数据一致性相关的知识,就等着面试官来问**,这样准备面试更有效率,这些写出来的简历也才是好的简历,而不是简历上泛泛而谈什么都说一些,最后都不太了解。
78 |
79 | 面试一共就三十分钟或者一个小时,说两个两个项目中的难点,既凸显出自己技术上的深度,同时项目中的难点是最好被我们自己掌控的,**因为这块是面试官必问的,就是我们可以变被动为主动的关键**。
80 |
81 | **真正好的简历是 当同学们把自己的简历递给面试官的时候,基本都知道面试官看着简历都会问什么问题**,然后将面试官的引导到自己最熟悉的领域,这样大家才会占有主动权。
82 |
83 |
84 | ## 博客的重要性
85 |
86 | 简历上可以放上自己的博客地址、Github地址甚至微博(如果发了很多关于技术的内容),**通过博客和github 面试官就可以快速判断同学们对技术的热情,以及学习的态度**,可以让面试官快速的了解同学们的技术水平。
87 |
88 | 如果有很多高质量博客和漂亮的github的话,即使面试现场发挥的不好,面试官通过博客也会知道这位同学基础还是很扎实,只是发挥的不好而已。
89 |
90 | 可以看出记录和总结的重要性。
91 |
92 | 写博客,不一定非要是技术大牛才写博客,大家都可以写博客来记录自己的收获,每一个知识点大家都可以写一篇技术博客,这方面要切忌懒惰!
93 |
94 | **我是欢迎录友们参考我的文章写博客来记录自己收获的,但一定要注明来自公众号「代码随想录」呀!**
95 |
96 | 同时大家对github不要畏惧,可以很容易找到一些小的项目来练手。
97 |
98 | 这里贴出我的Github,上面有一些我自己写的小项目,大家可以参考:https://github.com/youngyangyang04
99 |
100 | 面试只有短短的30分钟或者一个小时,如何把自己掌握的技术更好的展现给面试官呢,博客、github都是很好的选择,如果把这些放在简历上,面试官一定会看的,这都是加分项。
101 |
102 | ## 领取方式
103 |
104 | 最后福利,把我的简历模板贡献出来!如下图所示。
105 |
106 | 
107 |
108 | 这里是简历模板中Markdown的代码:[https://github.com/youngyangyang04/Markdown-Resume-Template](https://github.com/youngyangyang04/Markdown-Resume-Template) ,可以fork到自己Github仓库上,按照这个模板来修改自己的简历。
109 |
110 | **Word版本的简历,添加如下企业微信,通过之后就会发你word版本**。
111 |
112 |
113 |
114 | 如果已经有我的企业微信,直接回复:简历模板,就可以了。
115 |
116 | ## 总结
117 |
118 | **好的简历是敲门砖,同时也不要在简历上花费过多的精力,好的简历以及面试技巧都是锦上添花**,真的求得心得的offer靠的还是真才实学。
119 |
120 |
121 | -----------------------
122 |
123 |
--------------------------------------------------------------------------------
/problems/前序/空间复杂度.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # 空间复杂度分析
4 |
5 | * [关于时间复杂度,你不知道的都在这里!](https://programmercarl.com/前序/关于时间复杂度,你不知道的都在这里!.html)
6 | * [O(n)的算法居然超时了,此时的n究竟是多大?](https://programmercarl.com/前序/On的算法居然超时了,此时的n究竟是多大?.html)
7 | * [通过一道面试题目,讲一讲递归算法的时间复杂度!](https://programmercarl.com/前序/通过一道面试题目,讲一讲递归算法的时间复杂度!.html)
8 |
9 | 那么一直还没有讲空间复杂度,所以打算陆续来补上,内容不难,大家可以读一遍文章就有整体的了解了。
10 |
11 | 什么是空间复杂度呢?
12 |
13 | 是对一个算法在运行过程中占用内存空间大小的量度,记做S(n)=O(f(n)。
14 |
15 | 空间复杂度(Space Complexity)记作S(n) 依然使用大O来表示。利用程序的空间复杂度,可以对程序运行中需要多少内存有个预先估计。
16 |
17 | 关注空间复杂度有两个常见的相关问题
18 |
19 | 1. 空间复杂度是考虑程序(可执行文件)的大小么?
20 |
21 | 很多同学都会混淆程序运行时内存大小和程序本身的大小。这里强调一下**空间复杂度是考虑程序运行时占用内存的大小,而不是可执行文件的大小。**
22 |
23 | 2. 空间复杂度是准确算出程序运行时所占用的内存么?
24 |
25 | 不要以为空间复杂度就已经精准的掌握了程序的内存使用大小,很多因素会影响程序真正内存使用大小,例如编译器的内存对齐,编程语言容器的底层实现等等这些都会影响到程序内存的开销。
26 |
27 | 所以空间复杂度是预先大体评估程序内存使用的大小。
28 |
29 | 说到空间复杂度,我想同学们在OJ(online judge)上应该遇到过这种错误,就是超出内存限制,一般OJ对程序运行时的所消耗的内存都有一个限制。
30 |
31 | 为了避免内存超出限制,这也需要我们对算法占用多大的内存有一个大体的预估。
32 |
33 | 同样在工程实践中,计算机的内存空间也不是无限的,需要工程师对软件运行时所使用的内存有一个大体评估,这都需要用到算法空间复杂度的分析。
34 |
35 | 来看一下例子,什么时候的空间复杂度是 $O(1)$ 呢,C++代码如下:
36 |
37 | ```CPP
38 | int j = 0;
39 | for (int i = 0; i < n; i++) {
40 | j++;
41 | }
42 |
43 | ```
44 | 第一段代码可以看出,随着n的变化,所需开辟的内存空间并不会随着n的变化而变化。即此算法空间复杂度为一个常量,所以表示为大O(1)。
45 |
46 | 什么时候的空间复杂度是O(n)?
47 |
48 | 当消耗空间和输入参数n保持线性增长,这样的空间复杂度为O(n),来看一下这段C++代码
49 | ```CPP
50 | int* a = new int(n);
51 | for (int i = 0; i < n; i++) {
52 | a[i] = i;
53 | }
54 | ```
55 |
56 | 我们定义了一个数组出来,这个数组占用的大小为n,虽然有一个for循环,但没有再分配新的空间,因此,这段代码的空间复杂度主要看第一行即可,随着n的增大,开辟的内存大小呈线性增长,即 O(n)。
57 |
58 | 其他的 O(n^2), O(n^3) 我想大家应该都可以以此例举出来了,**那么思考一下 什么时候空间复杂度是 O(logn)呢?**
59 |
60 | 空间复杂度是logn的情况确实有些特殊,其实是在**递归的时候,会出现空间复杂度为logn的情况**。
61 |
62 | 至于如何求递归的空间复杂度,我会在专门写一篇文章来介绍的,敬请期待!
63 |
64 |
65 |
66 |
67 | -----------------------
68 |
69 |
--------------------------------------------------------------------------------
/problems/前序/编程素养部分的吹毛求疵.md:
--------------------------------------------------------------------------------
1 | ## 代码风格
2 |
3 | - `不甚了解`是不能更了解的意思,这个地方应该使用存疑。
4 | - `后期在不断优化`,'在'应为'再'。
5 | - `googlec++编程规范`,Google拼写错误
6 |
7 | ## 代码本地编译
8 |
9 | - `粘到本例来运行`存疑,应为本地
10 | - `本题运行`存疑,应为本地
11 |
12 | ## ACM二叉树
13 |
14 | - 左孩子和右孩子的下标不太好理解。我给出证明过程:
15 |
16 | 如果父节点在第k层,第$m,m \in [0,2^k]$个节点,则其左孩子所在的位置必然为$k+1$层,第$2*(m-1)+1$个节点。
17 |
18 | - 计算父节点在数组中的索引:
19 | $$
20 | index_{father}=(\sum_{i=0}^{i=k-1}2^i)+m-1=2^k-1+m-1
21 | $$
22 |
23 | - 计算左子节点在数组的索引:
24 | $$
25 | index_{left}=(\sum_{i=0}^{i=k}2^i)+2*m-1-1=2^{k+1}+2m-3
26 | $$
27 |
28 | - 故左孩子的下标为$index_{left}=index_{father}\times2+1$,同理可得到右子孩子的索引关系。也可以直接在左子孩子的基础上`+1`。
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/problems/前序/递归算法的时间复杂度.md:
--------------------------------------------------------------------------------
1 |
2 | # 通过一道面试题目,讲一讲递归算法的时间复杂度!
3 |
4 | > 本篇通过一道面试题,一个面试场景,来好好分析一下如何求递归算法的时间复杂度。
5 |
6 | 相信很多同学对递归算法的时间复杂度都很模糊,那么这篇来给大家通透的讲一讲。
7 |
8 | **同一道题目,同样使用递归算法,有的同学会写出了O(n)的代码,有的同学就写出了O(logn)的代码**。
9 |
10 | 这是为什么呢?
11 |
12 | 如果对递归的时间复杂度理解的不够深入的话,就会这样!
13 |
14 | 那么我通过一道简单的面试题,模拟面试的场景,来带大家逐步分析递归算法的时间复杂度,最后找出最优解,来看看同样是递归,怎么就写成了O(n)的代码。
15 |
16 | 面试题:求x的n次方
17 |
18 | 想一下这么简单的一道题目,代码应该如何写呢。最直观的方式应该就是,一个for循环求出结果,代码如下:
19 |
20 | ```CPP
21 | int function1(int x, int n) {
22 | int result = 1; // 注意 任何数的0次方等于1
23 | for (int i = 0; i < n; i++) {
24 | result = result * x;
25 | }
26 | return result;
27 | }
28 | ```
29 | 时间复杂度为O(n),此时面试官会说,有没有效率更好的算法呢。
30 |
31 | **如果此时没有思路,不要说:我不会,我不知道了等等**。
32 |
33 | 可以和面试官探讨一下,询问:“可不可以给点提示”。面试官提示:“考虑一下递归算法”。
34 |
35 | 那么就可以写出了如下这样的一个递归的算法,使用递归解决了这个问题。
36 |
37 | ```CPP
38 | int function2(int x, int n) {
39 | if (n == 0) {
40 | return 1; // return 1 同样是因为0次方是等于1的
41 | }
42 | return function2(x, n - 1) * x;
43 | }
44 | ```
45 | 面试官问:“那么这个代码的时间复杂度是多少?”。
46 |
47 | 一些同学可能一看到递归就想到了O(log n),其实并不是这样,递归算法的时间复杂度本质上是要看: **递归的次数 * 每次递归中的操作次数**。
48 |
49 | 那再来看代码,这里递归了几次呢?
50 |
51 | 每次n-1,递归了n次时间复杂度是O(n),每次进行了一个乘法操作,乘法操作的时间复杂度一个常数项O(1),所以这份代码的时间复杂度是 n × 1 = O(n)。
52 |
53 | 这个时间复杂度就没有达到面试官的预期。于是又写出了如下的递归算法的代码:
54 |
55 | ```CPP
56 | int function3(int x, int n) {
57 | if (n == 0) return 1;
58 | if (n == 1) return x;
59 |
60 | if (n % 2 == 1) {
61 | return function3(x, n / 2) * function3(x, n / 2)*x;
62 | }
63 | return function3(x, n / 2) * function3(x, n / 2);
64 | }
65 |
66 | ```
67 |
68 | 面试官看到后微微一笑,问:“这份代码的时间复杂度又是多少呢?” 此刻有些同学可能要陷入了沉思了。
69 |
70 | 我们来分析一下,首先看递归了多少次呢,可以把递归抽象出一棵满二叉树。刚刚同学写的这个算法,可以用一棵满二叉树来表示(为了方便表示,选择n为偶数16),如图:
71 |
72 | 
73 |
74 | 当前这棵二叉树就是求x的n次方,n为16的情况,n为16的时候,进行了多少次乘法运算呢?
75 |
76 | 这棵树上每一个节点就代表着一次递归并进行了一次相乘操作,所以进行了多少次递归的话,就是看这棵树上有多少个节点。
77 |
78 | 熟悉二叉树话应该知道如何求满二叉树节点数量,这棵满二叉树的节点数量就是`2^3 + 2^2 + 2^1 + 2^0 = 15`,可以发现:**这其实是等比数列的求和公式,这个结论在二叉树相关的面试题里也经常出现**。
79 |
80 | 这么如果是求x的n次方,这个递归树有多少个节点呢,如下图所示:(m为深度,从0开始)
81 |
82 | 
83 |
84 | **时间复杂度忽略掉常数项`-1`之后,这个递归算法的时间复杂度依然是O(n)**。对,你没看错,依然是O(n)的时间复杂度!
85 |
86 | 此时面试官就会说:“这个递归的算法依然还是O(n)啊”, 很明显没有达到面试官的预期。
87 |
88 | 那么O(logn)的递归算法应该怎么写呢?
89 |
90 | 想一想刚刚给出的那份递归算法的代码,是不是有哪里比较冗余呢,其实有重复计算的部分。
91 |
92 | 于是又写出如下递归算法的代码:
93 |
94 | ```CPP
95 | int function4(int x, int n) {
96 | if (n == 0) return 1;
97 | if (n == 1) return x;
98 | int t = function4(x, n / 2);// 这里相对于function3,是把这个递归操作抽取出来
99 | if (n % 2 == 1) {
100 | return t * t * x;
101 | }
102 | return t * t;
103 | }
104 | ```
105 |
106 | 再来看一下现在这份代码时间复杂度是多少呢?
107 |
108 | 依然还是看他递归了多少次,可以看到这里仅仅有一个递归调用,且每次都是n/2 ,所以这里我们一共调用了log以2为底n的对数次。
109 |
110 | **每次递归了做都是一次乘法操作,这也是一个常数项的操作,那么这个递归算法的时间复杂度才是真正的O(logn)**。
111 |
112 | 此时大家最后写出了这样的代码并且将时间复杂度分析的非常清晰,相信面试官是比较满意的。
113 |
114 | # 总结
115 |
116 | 对于递归的时间复杂度,毕竟初学者有时候会迷糊,刷过很多题的老手依然迷糊。
117 |
118 | **本篇我用一道非常简单的面试题目:求x的n次方,来逐步分析递归算法的时间复杂度,注意不要一看到递归就想到了O(logn)!**
119 |
120 | 同样使用递归,有的同学可以写出O(logn)的代码,有的同学还可以写出O(n)的代码。
121 |
122 | 对于function3 这样的递归实现,很容易让人感觉这是O(log n)的时间复杂度,其实这是O(n)的算法!
123 |
124 | ```CPP
125 | int function3(int x, int n) {
126 | if (n == 0) return 1;
127 | if (n == 1) return x;
128 | if (n % 2 == 1) {
129 | return function3(x, n / 2) * function3(x, n / 2)*x;
130 | }
131 | return function3(x, n / 2) * function3(x, n / 2);
132 | }
133 | ```
134 | 可以看出这道题目非常简单,但是又很考究算法的功底,特别是对递归的理解,这也是我面试别人的时候用过的一道题,所以整个情景我才写的如此逼真。
135 |
136 | 大厂面试的时候最喜欢用“简单题”来考察候选人的算法功底,注意这里的“简单题”可并不一定真的简单哦!
137 |
138 | 如果认真读完本篇,相信大家对递归算法的有一个新的认识的,同一道题目,同样是递归,效率可是不一样的!
139 |
140 |
141 |
142 | -----------------------
143 |
144 |
--------------------------------------------------------------------------------
/problems/剑指Offer58-II.左旋转字符串.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!
6 |
7 |
8 | # 右旋字符串
9 |
10 | 力扣已经将剑指offer题目下架,所以在卡码网上给大家提供类似的题目来练习
11 |
12 | [卡码网题目链接](https://kamacoder.com/problempage.php?pid=1065)
13 |
14 | 字符串的右旋转操作是把字符串尾部的若干个字符转移到字符串的前面。给定一个字符串 s 和一个正整数 k,请编写一个函数,将字符串中的后面 k 个字符移到字符串的前面,实现字符串的右旋转操作。
15 |
16 | 例如,对于输入字符串 "abcdefg" 和整数 2,函数应该将其转换为 "fgabcde"。
17 |
18 | 输入:输入共包含两行,第一行为一个正整数 k,代表右旋转的位数。第二行为字符串 s,代表需要旋转的字符串。
19 |
20 | 输出:输出共一行,为进行了右旋转操作后的字符串。
21 |
22 | 样例输入:
23 |
24 | ```
25 | 2
26 | abcdefg
27 | ```
28 |
29 | 样例输出:
30 |
31 | ```
32 | fgabcde
33 | ```
34 |
35 | 数据范围:1 <= k < 10000, 1 <= s.length < 10000;
36 |
37 |
38 | ## 思路
39 |
40 | 为了让本题更有意义,提升一下本题难度:**不能申请额外空间,只能在本串上操作**。 (Java不能在字符串上修改,所以使用java一定要开辟新空间)
41 |
42 | 不能使用额外空间的话,模拟在本串操作要实现右旋转字符串的功能还是有点困难的。
43 |
44 | 那么我们可以想一下上一题目[字符串:花式反转还不够!](https://programmercarl.com/0151.翻转字符串里的单词.html)中讲过,使用整体反转+局部反转就可以实现反转单词顺序的目的。
45 |
46 |
47 | 本题中,我们需要将字符串右移n位,字符串相当于分成了两个部分,如果n为2,符串相当于分成了两个部分,如图: (length为字符串长度)
48 |
49 | 
50 |
51 |
52 | 右移n位, 就是将第二段放在前面,第一段放在后面,先不考虑里面字符的顺序,是不是整体倒叙不就行了。如图:
53 |
54 | 
55 |
56 | 此时第一段和第二段的顺序是我们想要的,但里面的字符位置被我们倒叙,那么此时我们在把 第一段和第二段里面的字符再倒叙一把,这样字符顺序不就正确了。 如果:
57 |
58 | 
59 |
60 | 其实,思路就是 通过 整体倒叙,把两段子串顺序颠倒,两个段子串里的的字符在倒叙一把,**负负得正**,这样就不影响子串里面字符的顺序了。
61 |
62 | 整体代码如下:
63 |
64 | ```CPP
65 | // 版本一
66 | #include
67 | #include
68 | using namespace std;
69 | int main() {
70 | int n;
71 | string s;
72 | cin >> n;
73 | cin >> s;
74 | int len = s.size(); //获取长度
75 |
76 | reverse(s.begin(), s.end()); // 整体反转
77 | reverse(s.begin(), s.begin() + n); // 先反转前一段,长度n
78 | reverse(s.begin() + n, s.end()); // 再反转后一段
79 |
80 | cout << s << endl;
81 |
82 | }
83 | ```
84 |
85 | 那么整体反正的操作放在下面,先局部反转行不行?
86 |
87 | 可以的,不过,要记得 控制好 局部反转的长度,如果先局部反转,那么先反转的子串长度就是 len - n,如图:
88 |
89 | 
90 |
91 | 代码如下:
92 |
93 | ```CPP
94 | // 版本二
95 | #include
96 | #include
97 | using namespace std;
98 | int main() {
99 | int n;
100 | string s;
101 | cin >> n;
102 | cin >> s;
103 | int len = s.size(); //获取长度
104 | reverse(s.begin(), s.begin() + len - n); // 先反转前一段,长度len-n ,注意这里是和版本一的区别
105 | reverse(s.begin() + len - n, s.end()); // 再反转后一段
106 | reverse(s.begin(), s.end()); // 整体反转
107 | cout << s << endl;
108 |
109 | }
110 | ```
111 |
112 |
113 | ## 拓展
114 |
115 | 大家在做剑指offer的时候,会发现 剑指offer的题目是左反转,那么左反转和右反转 有什么区别呢?
116 |
117 | 其实思路是一样一样的,就是反转的区间不同而已。如果本题是左旋转n,那么实现代码如下:
118 |
119 | ```CPP
120 | #include
121 | #include
122 | using namespace std;
123 | int main() {
124 | int n;
125 | string s;
126 | cin >> n;
127 | cin >> s;
128 | int len = s.size(); //获取长度
129 | reverse(s.begin(), s.begin() + n); // 反转第一段长度为n
130 | reverse(s.begin() + n, s.end()); // 反转第二段长度为len-n
131 | reverse(s.begin(), s.end()); // 整体反转
132 | cout << s << endl;
133 |
134 | }
135 | ```
136 |
137 | 大家可以感受一下 这份代码和 版本二的区别, 其实就是反转的区间不同而已。
138 |
139 | 那么左旋转的话,可以不可以先整体反转,例如想版本一的那样呢?
140 |
141 | 当然可以。
142 |
143 |
144 |
145 |
146 | ## 其他语言版本
147 |
148 | ### Java:
149 |
150 |
151 |
152 | ### Python:
153 |
154 |
155 | ### Go:
156 |
157 |
158 | ### JavaScript:
159 |
160 |
161 | ### TypeScript:
162 |
163 |
164 | ### Swift:
165 |
166 |
167 |
168 | ### PHP:
169 |
170 |
171 | ### Scala:
172 |
173 |
174 | ### Rust:
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
--------------------------------------------------------------------------------
/problems/动态规划理论基础.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!
6 |
7 | # 动态规划理论基础
8 |
9 | 动态规划刷题大纲
10 |
11 |
12 |
13 | ## 算法公开课
14 |
15 | **[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[动态规划理论基础](https://www.bilibili.com/video/BV13Q4y197Wg),相信结合视频再看本篇题解,更有助于大家对本题的理解**。
16 |
17 |
18 | ## 什么是动态规划
19 |
20 | 动态规划,英文:Dynamic Programming,简称DP,如果某一问题有很多重叠子问题,使用动态规划是最有效的。
21 |
22 | 所以动态规划中每一个状态一定是由上一个状态推导出来的,**这一点就区分于贪心**,贪心没有状态推导,而是从局部直接选最优的,
23 |
24 | 在[关于贪心算法,你该了解这些!](https://programmercarl.com/%E8%B4%AA%E5%BF%83%E7%AE%97%E6%B3%95%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%80.html)中我举了一个背包问题的例子。
25 |
26 | 例如:有N件物品和一个最多能背重量为W 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。**每件物品只能用一次**,求解将哪些物品装入背包里物品价值总和最大。
27 |
28 | 动态规划中dp[j]是由dp[j-weight[i]]推导出来的,然后取max(dp[j], dp[j - weight[i]] + value[i])。
29 |
30 | 但如果是贪心呢,每次拿物品选一个最大的或者最小的就完事了,和上一个状态没有关系。
31 |
32 | 所以贪心解决不了动态规划的问题。
33 |
34 | **其实大家也不用死扣动规和贪心的理论区别,后面做做题目自然就知道了**。
35 |
36 | 而且很多讲解动态规划的文章都会讲最优子结构啊和重叠子问题啊这些,这些东西都是教科书的上定义,晦涩难懂而且不实用。
37 |
38 | 大家知道动规是由前一个状态推导出来的,而贪心是局部直接选最优的,对于刷题来说就够用了。
39 |
40 | 上述提到的背包问题,后序会详细讲解。
41 |
42 | ## 动态规划的解题步骤
43 |
44 | 做动规题目的时候,很多同学会陷入一个误区,就是以为把状态转移公式背下来,照葫芦画瓢改改,就开始写代码,甚至把题目AC之后,都不太清楚dp[i]表示的是什么。
45 |
46 | **这就是一种朦胧的状态,然后就把题给过了,遇到稍稍难一点的,可能直接就不会了,然后看题解,然后继续照葫芦画瓢陷入这种恶性循环中**。
47 |
48 | 状态转移公式(递推公式)是很重要,但动规不仅仅只有递推公式。
49 |
50 | **对于动态规划问题,我将拆解为如下五步曲,这五步都搞清楚了,才能说把动态规划真的掌握了!**
51 |
52 | 1. 确定dp数组(dp table)以及下标的含义
53 | 2. 确定递推公式
54 | 3. dp数组如何初始化
55 | 4. 确定遍历顺序
56 | 5. 举例推导dp数组
57 |
58 | 一些同学可能想为什么要先确定递推公式,然后在考虑初始化呢?
59 |
60 | **因为一些情况是递推公式决定了dp数组要如何初始化!**
61 |
62 | 后面的讲解中我都是围绕着这五点来进行讲解。
63 |
64 | 可能刷过动态规划题目的同学可能都知道递推公式的重要性,感觉确定了递推公式这道题目就解出来了。
65 |
66 | 其实 确定递推公式 仅仅是解题里的一步而已!
67 |
68 | 一些同学知道递推公式,但搞不清楚dp数组应该如何初始化,或者正确的遍历顺序,以至于记下来公式,但写的程序怎么改都通过不了。
69 |
70 | 后序的讲解的大家就会慢慢感受到这五步的重要性了。
71 |
72 | ## 动态规划应该如何debug
73 |
74 |
75 | 相信动规的题目,很大部分同学都是这样做的。
76 |
77 | 看一下题解,感觉看懂了,然后照葫芦画瓢,如果能正好画对了,万事大吉,一旦要是没通过,就怎么改都通过不了,对 dp数组的初始化,递推公式,遍历顺序,处于一种黑盒的理解状态。
78 |
79 | 写动规题目,代码出问题很正常!
80 |
81 | **找问题的最好方式就是把dp数组打印出来,看看究竟是不是按照自己思路推导的!**
82 |
83 | 一些同学对于dp的学习是黑盒的状态,就是不清楚dp数组的含义,不懂为什么这么初始化,递推公式背下来了,遍历顺序靠习惯就是这么写的,然后一鼓作气写出代码,如果代码能通过万事大吉,通过不了的话就凭感觉改一改。
84 |
85 | 这是一个很不好的习惯!
86 |
87 | **做动规的题目,写代码之前一定要把状态转移在dp数组的上具体情况模拟一遍,心中有数,确定最后推出的是想要的结果**。
88 |
89 | 然后再写代码,如果代码没通过就打印dp数组,看看是不是和自己预先推导的哪里不一样。
90 |
91 | 如果打印出来和自己预先模拟推导是一样的,那么就是自己的递归公式、初始化或者遍历顺序有问题了。
92 |
93 | 如果和自己预先模拟推导的不一样,那么就是代码实现细节有问题。
94 |
95 | **这样才是一个完整的思考过程,而不是一旦代码出问题,就毫无头绪的东改改西改改,最后过不了,或者说是稀里糊涂的过了**。
96 |
97 | 这也是我为什么在动规五步曲里强调推导dp数组的重要性。
98 |
99 | 举个例子哈:在「代码随想录」刷题小分队微信群里,一些录友可能代码通过不了,会把代码抛到讨论群里问:我这里代码都已经和题解一模一样了,为什么通过不了呢?
100 |
101 | 发出这样的问题之前,其实可以自己先思考这三个问题:
102 |
103 | * 这道题目我举例推导状态转移公式了么?
104 | * 我打印dp数组的日志了么?
105 | * 打印出来了dp数组和我想的一样么?
106 |
107 | **如果这灵魂三问自己都做到了,基本上这道题目也就解决了**,或者更清晰的知道自己究竟是哪一点不明白,是状态转移不明白,还是实现代码不知道该怎么写,还是不理解遍历dp数组的顺序。
108 |
109 | 然后在问问题,目的性就很强了,群里的小伙伴也可以快速知道提问者的疑惑了。
110 |
111 | **注意这里不是说不让大家问问题哈, 而是说问问题之前要有自己的思考,问题要问到点子上!**
112 |
113 | **大家工作之后就会发现,特别是大厂,问问题是一个专业活,是的,问问题也要体现出专业!**
114 |
115 | 如果问同事很不专业的问题,同事们会懒的回答,领导也会认为你缺乏思考能力,这对职场发展是很不利的。
116 |
117 | 所以大家在刷题的时候,就锻炼自己养成专业提问的好习惯。
118 |
119 | ## 总结
120 |
121 | 这一篇是动态规划的整体概述,讲解了什么是动态规划,动态规划的解题步骤,以及如何debug。
122 |
123 | 动态规划是一个很大的领域,今天这一篇讲解的内容是整个动态规划系列中都会使用到的一些理论基础。
124 |
125 | 在后序讲解中针对某一具体问题,还会讲解其对应的理论基础,例如背包问题中的01背包,leetcode上的题目都是01背包的应用,而没有纯01背包的问题,那么就需要在把对应的理论知识讲解一下。
126 |
127 | 大家会发现,我讲解的理论基础并不是教科书上各种动态规划的定义,错综复杂的公式。
128 |
129 | 这里理论基础篇已经是非常偏实用的了,每个知识点都是在解题实战中非常有用的内容,大家要重视起来哈。
130 |
131 | 今天我们开始新的征程了,你准备好了么?
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
--------------------------------------------------------------------------------
/problems/双指针总结.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!
6 |
7 | > 又是一波总结
8 |
9 | 相信大家已经对双指针法很熟悉了,但是双指针法并不隶属于某一种数据结构,我们在讲解数组,链表,字符串都用到了双指针法,所有有必要针对双指针法做一个总结。
10 |
11 | # 双指针总结篇
12 | ## 数组篇
13 |
14 | 在[数组:就移除个元素很难么?](https://programmercarl.com/0027.移除元素.html)中,原地移除数组上的元素,我们说到了数组上的元素,不能真正的删除,只能覆盖。
15 |
16 | 一些同学可能会写出如下代码(伪代码):
17 |
18 | ```
19 | for (int i = 0; i < array.size(); i++) {
20 | if (array[i] == target) {
21 | array.erase(i);
22 | }
23 | }
24 | ```
25 |
26 | 这个代码看上去好像是O(n)的时间复杂度,其实是O(n^2)的时间复杂度,因为erase操作也是O(n)的操作。
27 |
28 | 所以此时使用双指针法才展现出效率的优势:**通过两个指针在一个for循环下完成两个for循环的工作。**
29 |
30 | ## 字符串篇
31 |
32 | 在[字符串:这道题目,使用库函数一行代码搞定](https://programmercarl.com/0344.反转字符串.html)中讲解了反转字符串,注意这里强调要原地反转,要不然就失去了题目的意义。
33 |
34 | 使用双指针法,**定义两个指针(也可以说是索引下标),一个从字符串前面,一个从字符串后面,两个指针同时向中间移动,并交换元素。**,时间复杂度是O(n)。
35 |
36 | 在[替换空格](https://programmercarl.com/剑指Offer05.替换空格.html) 中介绍使用双指针填充字符串的方法,如果想把这道题目做到极致,就不要只用额外的辅助空间了!
37 |
38 | 思路就是**首先扩充数组到每个空格替换成"%20"之后的大小。然后双指针从后向前替换空格。**
39 |
40 | 有同学问了,为什么要从后向前填充,从前向后填充不行么?
41 |
42 | 从前向后填充就是O(n^2)的算法了,因为每次添加元素都要将添加元素之后的所有元素向后移动。
43 |
44 | **其实很多数组(字符串)填充类的问题,都可以先预先给数组扩容带填充后的大小,然后在从后向前进行操作。**
45 |
46 | 那么在[字符串:花式反转还不够!](https://programmercarl.com/0151.翻转字符串里的单词.html)中,我们使用双指针法,用O(n)的时间复杂度完成字符串删除类的操作,因为题目要删除冗余空格。
47 |
48 | **在删除冗余空格的过程中,如果不注意代码效率,很容易写成了O(n^2)的时间复杂度。其实使用双指针法O(n)就可以搞定。**
49 |
50 | **主要还是大家用erase用的比较随意,一定要注意for循环下用erase的情况,一般可以用双指针写效率更高!**
51 |
52 | ## 链表篇
53 |
54 | 翻转链表是现场面试,白纸写代码的好题,考察了候选者对链表以及指针的熟悉程度,而且代码也不长,适合在白纸上写。
55 |
56 | 在[链表:听说过两天反转链表又写不出来了?](https://programmercarl.com/0206.翻转链表.html)中,讲如何使用双指针法来翻转链表,**只需要改变链表的next指针的指向,直接将链表反转 ,而不用重新定义一个新的链表。**
57 |
58 | 思路还是很简单的,代码也不长,但是想在白纸上一次性写出bugfree的代码,并不是容易的事情。
59 |
60 | 在链表中求环,应该是双指针在链表里最经典的应用,在[链表:环找到了,那入口呢?](https://programmercarl.com/0142.环形链表II.html)中讲解了如何通过双指针判断是否有环,而且还要找到环的入口。
61 |
62 | **使用快慢指针(双指针法),分别定义 fast 和 slow指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环。**
63 |
64 | 那么找到环的入口,其实需要点简单的数学推理,我在文章中把找环的入口清清楚楚的推理的一遍,如果对找环入口不够清楚的同学建议自己看一看[链表:环找到了,那入口呢?](https://programmercarl.com/0142.环形链表II.html)。
65 |
66 | ## N数之和篇
67 |
68 | 在[哈希表:解决了两数之和,那么能解决三数之和么?](https://programmercarl.com/0015.三数之和.html)中,讲到使用哈希法可以解决1.两数之和的问题
69 |
70 | 其实使用双指针也可以解决1.两数之和的问题,只不过1.两数之和求的是两个元素的下标,没法用双指针,如果改成求具体两个元素的数值就可以了,大家可以尝试用双指针做一个leetcode上两数之和的题目,就可以体会到我说的意思了。
71 |
72 | 使用了哈希法解决了两数之和,但是哈希法并不适用于三数之和!
73 |
74 | 使用哈希法的过程中要把符合条件的三元组放进vector中,然后在去去重,这样是非常费时的,很容易超时,也是三数之和通过率如此之低的根源所在。
75 |
76 | 去重的过程不好处理,有很多小细节,如果在面试中很难想到位。
77 |
78 | 时间复杂度可以做到O(n^2),但还是比较费时的,因为不好做剪枝操作。
79 |
80 | 所以这道题目使用双指针法才是最为合适的,用双指针做这道题目才能就能真正体会到,**通过前后两个指针不算向中间逼近,在一个for循环下完成两个for循环的工作。**
81 |
82 | 只用双指针法时间复杂度为O(n^2),但比哈希法的O(n^2)效率高得多,哈希法在使用两层for循环的时候,能做的剪枝操作很有限。
83 |
84 | 在[双指针法:一样的道理,能解决四数之和](https://programmercarl.com/0018.四数之和.html)中,讲到了四数之和,其实思路是一样的,**在三数之和的基础上再套一层for循环,依然是使用双指针法。**
85 |
86 | 对于三数之和使用双指针法就是将原本暴力O(n^3)的解法,降为O(n^2)的解法,四数之和的双指针解法就是将原本暴力O(n^4)的解法,降为O(n^3)的解法。
87 |
88 | 同样的道理,五数之和,n数之和都是在这个基础上累加。
89 |
90 |
91 | ## 总结
92 |
93 | 本文中一共介绍了leetcode上九道使用双指针解决问题的经典题目,除了链表一些题目一定要使用双指针,其他题目都是使用双指针来提高效率,一般是将O(n^2)的时间复杂度,降为 $O(n)$ 。
94 |
95 | 建议大家可以把文中涉及到的题目在好好做一做,琢磨琢磨,基本对双指针法就不在话下了。
96 |
97 |
98 |
99 |
100 |
101 |
102 |
--------------------------------------------------------------------------------
/problems/周总结/20201010二叉树周末总结.md:
--------------------------------------------------------------------------------
1 |
2 | # 本周小结!(二叉树系列三)
3 |
4 |
5 | ## 周一
6 |
7 | 在[二叉树:以为使用了递归,其实还隐藏着回溯](https://programmercarl.com/二叉树中递归带着回溯.html)中,通过leetcode [257.二叉树的所有路径这道题目](https://programmercarl.com/0257.二叉树的所有路径.html),讲解了递归如何隐藏着回溯,一些代码会把回溯的过程都隐藏了起来了,甚至刷过这道题的同学可能都不知道自己用了回溯。
8 |
9 | 文章中第一版代码把每一个细节都展示了输出来了,大家可以清晰的看到回溯的过程。
10 |
11 | 然后给出了第二版优化后的代码,分析了其回溯隐藏在了哪里,如果要把这个回溯扣出来的话,在第二版的基础上应该怎么改。
12 |
13 | 主要需要理解:**回溯隐藏在traversal(cur->left, path + "->", result);中的 path + "->"。 每次函数调用完,path依然是没有加上"->" 的,这就是回溯了。**
14 |
15 |
16 | ## 周二
17 |
18 | 在文章[二叉树:做了这么多题目了,我的左叶子之和是多少?](https://programmercarl.com/0404.左叶子之和.html) 中提供了另一个判断节点属性的思路,平时我们习惯了使用通过节点的左右孩子判断本节点的属性,但发现使用这个思路无法判断左叶子。
19 |
20 | 此时需要相连的三层之间构成的约束条件,也就是要通过节点的父节点以及孩子节点来判断本节点的属性。
21 |
22 | 这道题目可以扩展大家对二叉树的解题思路。
23 |
24 |
25 | ## 周三
26 |
27 | 在[二叉树:我的左下角的值是多少?](https://programmercarl.com/0513.找树左下角的值.html)中的题目如果使用递归的写法还是有点难度的,层次遍历反而很简单。
28 |
29 | 题目其实就是要在树的**最后一行**找到**最左边的值**。
30 |
31 | **如何判断是最后一行呢,其实就是深度最大的叶子节点一定是最后一行。**
32 |
33 | 在这篇文章中,我们使用递归算法实实在在的求了一次深度,然后使用靠左的遍历,保证求得靠左的最大深度,而且又一次使用了回溯。
34 |
35 | 如果对二叉树的高度与深度又有点模糊了,在看这里[二叉树:我平衡么?](https://programmercarl.com/0110.平衡二叉树.html),回忆一下吧。
36 |
37 | [二叉树:我的左下角的值是多少?](https://programmercarl.com/0513.找树左下角的值.html)中把我们之前讲过的内容都过了一遍,此外,还用前序遍历的技巧求得了靠左的最大深度。
38 |
39 | **求二叉树的各种最值,就想应该采用什么样的遍历顺序,确定了遍历循序,其实就和数组求最值一样容易了。**
40 |
41 |
42 | ## 周四
43 |
44 | 在[二叉树:递归函数究竟什么时候需要返回值,什么时候不要返回值?](https://programmercarl.com/0112.路径总和.html)中通过两道题目,彻底说清楚递归函数的返回值问题。
45 |
46 | 一般情况下:**如果需要搜索整棵二叉树,那么递归函数就不要返回值,如果要搜索其中一条符合条件的路径,递归函数就需要返回值,因为遇到符合条件的路径了就要及时返回。**
47 |
48 | 特别是有些时候 递归函数的返回值是bool类型,一些同学会疑惑为啥要加这个,其实就是为了找到一条边立刻返回。
49 |
50 | 其实还有一种就是后序遍历需要根据左右递归的返回值推出中间节点的状态,这种需要有返回值,例如[222.完全二叉树](https://programmercarl.com/0222.完全二叉树的节点个数.html),[110.平衡二叉树](https://programmercarl.com/0110.平衡二叉树.html),这几道我们之前也讲过。
51 |
52 | ## 周五
53 |
54 | 之前都是讲解遍历二叉树,这次该构造二叉树了,在[二叉树:构造二叉树登场!](https://programmercarl.com/0106.从中序与后序遍历序列构造二叉树.html)中,我们通过前序和中序,后序和中序,构造了唯一的一棵二叉树。
55 |
56 | **构造二叉树有三个注意的点:**
57 |
58 | * 分割时候,坚持区间不变量原则,左闭右开,或者左闭右闭。
59 | * 分割的时候,注意后序 或者 前序已经有一个节点作为中间节点了,不能继续使用了。
60 | * 如何使用切割后的后序数组来切合中序数组?利用中序数组大小一定是和后序数组的大小相同这一特点来进行切割。
61 |
62 | 这道题目代码实现并不简单,大家啃下来之后,二叉树的构造应该不是问题了。
63 |
64 | **最后我还给出了为什么前序和后序不能唯一构成一棵二叉树,因为没有中序遍历就无法确定左右部分,也就无法分割。**
65 |
66 | ## 周六
67 |
68 | 知道了如何构造二叉树,那么使用一个套路就可以解决文章[二叉树:构造一棵最大的二叉树](https://programmercarl.com/0654.最大二叉树.html)中的问题。
69 |
70 | **注意类似用数组构造二叉树的题目,每次分隔尽量不要定义新的数组,而是通过下标索引直接在原数组上操作,这样可以节约时间和空间上的开销。**
71 |
72 | 文章中我还给出了递归函数什么时候加if,什么时候不加if,其实就是控制空节点(空指针)是否进入递归,是不同的代码实现方式,都是可以的。
73 |
74 | **一般情况来说:如果让空节点(空指针)进入递归,就不加if,如果不让空节点进入递归,就加if限制一下, 终止条件也会相应的调整。**
75 |
76 | ## 总结
77 |
78 | 本周我们深度讲解了如下知识点:
79 |
80 | 1. [递归中如何隐藏着回溯](https://programmercarl.com/二叉树中递归带着回溯.html)
81 | 2. [如何通过三层关系确定左叶子](https://programmercarl.com/0404.左叶子之和.html)
82 | 3. [如何通过二叉树深度来判断左下角的值](https://programmercarl.com/0513.找树左下角的值.html)
83 | 4. [递归函数究竟什么时候需要返回值,什么时候不要返回值?](https://programmercarl.com/0112.路径总和.html)
84 | 5. [前序和中序,后序和中序构造唯一二叉树](https://programmercarl.com/0106.从中序与后序遍历序列构造二叉树.html)
85 | 6. [使用数组构造某一特性的二叉树](https://programmercarl.com/0654.最大二叉树.html)
86 |
87 | **如果大家一路跟下来,一定收获满满,如果周末不做这个总结,大家可能都不知道自己收获满满,啊哈!**
88 |
89 |
90 |
91 |
--------------------------------------------------------------------------------
/problems/周总结/20201017二叉树周末总结.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # 本周小结!(二叉树系列四)
4 |
5 | > 这已经是二叉树的第四周总结了,二叉树是非常重要的数据结构,也是面试中的常客,所以有必要一步一步帮助大家彻底掌握二叉树!
6 |
7 | ## 周一
8 |
9 | 在[二叉树:合并两个二叉树](https://programmercarl.com/0617.合并二叉树.html)中讲解了如何合并两个二叉树,平时我们都习惯了操作一个二叉树,一起操作两个树可能还有点陌生。
10 |
11 | 其实套路是一样,只不过一起操作两个树的指针,我们之前讲过求 [二叉树:我对称么?](https://programmercarl.com/0101.对称二叉树.html)的时候,已经初步涉及到了 一起遍历两棵二叉树了。
12 |
13 | **迭代法中,一般一起操作两个树都是使用队列模拟类似层序遍历,同时处理两个树的节点,这种方式最好理解,如果用模拟递归的思路的话,要复杂一些。**
14 |
15 | ## 周二
16 |
17 | 周二开始讲解一个新的树,二叉搜索树,开始要换一个思路了,如果没有利用好二叉搜索树的特性,就容易把简单题做成了难题了。
18 |
19 | 学习[二叉搜索树的特性](https://programmercarl.com/0700.二叉搜索树中的搜索.html),还是比较容易的。
20 |
21 | 大多是二叉搜索树的题目,其实都离不开中序遍历,因为这样就是有序的。
22 |
23 | 至于迭代法,相信大家看到文章中如此简单的迭代法的时候,都会感动的痛哭流涕。
24 |
25 | ## 周三
26 |
27 | 了解了二搜索树的特性之后, 开始验证[一棵二叉树是不是二叉搜索树](https://programmercarl.com/0098.验证二叉搜索树.html)。
28 |
29 | 首先在此强调一下二叉搜索树的特性:
30 |
31 | * 节点的左子树只包含小于当前节点的数。
32 | * 节点的右子树只包含大于当前节点的数。
33 | * 所有左子树和右子树自身必须也是二叉搜索树。
34 |
35 | 那么我们在验证二叉搜索树的时候,有两个陷阱:
36 |
37 | * 陷阱一
38 |
39 | **不能单纯的比较左节点小于中间节点,右节点大于中间节点就完事了**,而是左子树都小于中间节点,右子树都大于中间节点。
40 |
41 | * 陷阱二
42 |
43 | 在一个有序序列求最值的时候,不要定义一个全局变量,然后遍历序列更新全局变量求最值。因为最值可能就是int 或者 longlong的最小值。
44 |
45 | 推荐要通过前一个数值(pre)和后一个数值比较(cur),得出最值。
46 |
47 | **在二叉树中通过两个前后指针作比较,会经常用到**。
48 |
49 | 本文[二叉树:我是不是一棵二叉搜索树](https://programmercarl.com/0098.验证二叉搜索树.html)中迭代法中为什么没有周一那篇那么简洁了呢,因为本篇是验证二叉搜索树,前提默认它是一棵普通二叉树,所以还是要回归之前老办法。
50 |
51 | ## 周四
52 |
53 | 了解了[二叉搜索树](https://programmercarl.com/0700.二叉搜索树中的搜索.html),并且知道[如何判断二叉搜索树](https://programmercarl.com/0098.验证二叉搜索树.html),本篇就很简单了。
54 |
55 | **要知道二叉搜索树和中序遍历是好朋友!**
56 |
57 | 在[二叉树:搜索树的最小绝对差](https://programmercarl.com/0530.二叉搜索树的最小绝对差.html)中强调了要利用搜索树的特性,把这道题目想象成在一个有序数组上求两个数最小差值,这就是一道送分题了。
58 |
59 | **需要明确:在有序数组求任意两数最小值差等价于相邻两数的最小值差**。
60 |
61 | 同样本题也需要用pre节点记录cur节点的前一个节点。(这种写法一定要掌握)
62 |
63 | ## 周五
64 |
65 | 此时大家应该知道遇到二叉搜索树,就想是有序数组,那么在二叉搜索树中求二叉搜索树众数就很简单了。
66 |
67 | 在[二叉树:我的众数是多少?](https://programmercarl.com/0501.二叉搜索树中的众数.html)中我给出了如果是普通二叉树,应该如何求众数的集合,然后进一步讲解了二叉搜索树应该如何求众数集合。
68 |
69 | 在求众数集合的时候有一个技巧,因为题目中众数是可以有多个的,所以一般的方法需要遍历两遍才能求出众数的集合。
70 |
71 | **但可以遍历一遍就可以求众数集合,使用了适时清空结果集的方法**,这个方法还是很巧妙的。相信仔细读了文章的同学会惊呼其巧妙!
72 |
73 | **所以大家不要看题目简单了,就不动手做了,我选的题目,一般不会简单到不用动手的程度**。
74 |
75 | ## 周六
76 |
77 | 在[二叉树:公共祖先问题](https://programmercarl.com/0236.二叉树的最近公共祖先.html)中,我们开始讲解如何在二叉树中求公共祖先的问题,本来是打算和二叉搜索树一起讲的,但发现篇幅过长,所以先讲二叉树的公共祖先问题。
78 |
79 | **如果找到一个节点,发现左子树出现结点p,右子树出现节点q,或者 左子树出现结点q,右子树出现节点p,那么该节点就是节点p和q的最近公共祖先。**
80 |
81 | 这道题目的看代码比较简单,而且好像也挺好理解的,但是如果把每一个细节理解到位,还是不容易的。
82 |
83 | 主要思考如下几点:
84 |
85 | * 如何从底向上遍历?
86 | * 遍历整棵树,还是遍历局部树?
87 | * 如何把结果传到根节点的?
88 |
89 | 这些问题都需要弄清楚,上来直接看代码的话,是可能想不到这些细节的。
90 |
91 | 公共祖先问题,还是有难度的,初学者还是需要慢慢消化!
92 |
93 | ## 总结
94 |
95 | 本周我们讲了[如何合并两个二叉树](https://programmercarl.com/0617.合并二叉树.html),了解了如何操作两个二叉树。
96 |
97 | 然后开始另一种树:二叉搜索树,了解[二叉搜索树的特性](https://programmercarl.com/0700.二叉搜索树中的搜索.html),然后[判断一棵二叉树是不是二叉搜索树](https://programmercarl.com/0098.验证二叉搜索树.html)。
98 |
99 | 了解以上知识之后,就开始利用其特性,做一些二叉搜索树上的题目,[求最小绝对差](https://programmercarl.com/0530.二叉搜索树的最小绝对差.html),[求众数集合](https://programmercarl.com/0501.二叉搜索树中的众数.html)。
100 |
101 | 接下来,开始求二叉树与二叉搜索树的公共祖先问题,单篇篇幅原因,先单独介绍[普通二叉树如何求最近公共祖先](https://programmercarl.com/0236.二叉树的最近公共祖先.html)。
102 |
103 | 现在已经讲过了几种二叉树了,二叉树,二叉平衡树,完全二叉树,二叉搜索树,后面还会有平衡二叉搜索树。 那么一些同学难免会有混乱了,我针对如下三个问题,帮大家在捋顺一遍:
104 |
105 | 1. 平衡二叉搜索树是不是二叉搜索树和平衡二叉树的结合?
106 |
107 | 是的,是二叉搜索树和平衡二叉树的结合。
108 |
109 | 2. 平衡二叉树与完全二叉树的区别在于底层节点的位置?
110 |
111 | 是的,完全二叉树底层必须是从左到右连续的,且次底层是满的。
112 |
113 | 3. 堆是完全二叉树和排序的结合,而不是平衡二叉搜索树?
114 |
115 | 堆是一棵完全二叉树,同时保证父子节点的顺序关系(有序)。 **但完全二叉树一定是平衡二叉树,堆的排序是父节点大于子节点,而搜索树是父节点大于左孩子,小于右孩子,所以堆不是平衡二叉搜索树**。
116 |
117 | 大家如果每天坚持跟下来,会发现又是充实的一周![机智]
118 |
119 |
120 |
--------------------------------------------------------------------------------
/problems/周总结/20201030回溯周末总结.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | --------------------------
11 |
12 | # 本周小结!(回溯算法系列一)
13 |
14 | ## 周一
15 |
16 | 本周我们正式开始了回溯算法系列,那么首先当然是概述。
17 |
18 | 在[关于回溯算法,你该了解这些!](https://programmercarl.com/回溯算法理论基础.html)中介绍了什么是回溯,回溯法的效率,回溯法解决的问题以及回溯法模板。
19 |
20 | **回溯是递归的副产品,只要有递归就会有回溯**。
21 |
22 | 回溯法就是暴力搜索,并不是什么高效的算法,最多在剪枝一下。
23 |
24 | 回溯算法能解决如下问题:
25 |
26 | * 组合问题:N个数里面按一定规则找出k个数的集合
27 | * 排列问题:N个数按一定规则全排列,有几种排列方式
28 | * 切割问题:一个字符串按一定规则有几种切割方式
29 | * 子集问题:一个N个数的集合里有多少符合条件的子集
30 | * 棋盘问题:N皇后,解数独等等
31 |
32 | 是不是感觉回溯算法有点厉害了。
33 |
34 | 回溯法确实不好理解,所以需要把回溯法抽象为一个图形来理解就容易多了,每一道回溯法的题目都可以抽象为树形结构。
35 |
36 | 针对很多同学都写不好回溯,我在[关于回溯算法,你该了解这些!](https://programmercarl.com/回溯算法理论基础.html)用回溯三部曲,分析了回溯算法,并给出了回溯法的模板。
37 |
38 | 这个模板会伴随整个回溯法系列!
39 |
40 | ## 周二
41 |
42 |
43 | 在[回溯算法:求组合问题!](https://programmercarl.com/0077.组合.html)中,我们开始用回溯法解决第一道题目,组合问题。
44 |
45 | 我在文中开始的时候给大家列举k层for循环例子,进而得出都是同样是暴力解法,为什么要用回溯法。
46 |
47 | **此时大家应该深有体会回溯法的魅力,用递归控制for循环嵌套的数量!**
48 |
49 | 本题我把回溯问题抽象为树形结构,可以直观的看出其搜索的过程:**for循环横向遍历,递归纵向遍历,回溯不断调整结果集**。
50 |
51 | ## 周三
52 |
53 | 针对[回溯算法:求组合问题!](https://programmercarl.com/0077.组合.html)还可以做剪枝的操作。
54 |
55 | 在[回溯算法:组合问题再剪剪枝](https://programmercarl.com/0077.组合优化.html)中把回溯法代码做了剪枝优化,在文中我依然把问题抽象为一个树形结构,大家可以一目了然剪的究竟是哪里。
56 |
57 | **剪枝精髓是:for循环在寻找起点的时候要有一个范围,如果这个起点到集合终止之间的元素已经不够 题目要求的k个元素了,就没有必要搜索了**。
58 |
59 | ## 周四
60 |
61 | 在[回溯算法:求组合总和!](https://programmercarl.com/0216.组合总和III.html)中,相当于 [回溯算法:求组合问题!](https://programmercarl.com/0077.组合.html)加了一个元素总和的限制。
62 |
63 | 整体思路还是一样的,本题的剪枝会好想一些,即:**已选元素总和如果已经大于n(题中要求的和)了,那么往后遍历就没有意义了,直接剪掉**。
64 |
65 | 在本题中,依然还可以有一个剪枝,就是[回溯算法:组合问题再剪剪枝](https://programmercarl.com/0077.组合优化.html)中提到的,对for循环选择的起始范围的剪枝。
66 |
67 | 所以,剪枝的代码,可以把for循环,加上 `i <= 9 - (k - path.size()) + 1` 的限制!
68 |
69 | 组合总和问题还有一些花样,下周还会介绍到。
70 |
71 | ## 周五
72 |
73 | 在[回溯算法:电话号码的字母组合](https://programmercarl.com/0017.电话号码的字母组合.html)中,开始用多个集合来求组合,还是熟悉的模板题目,但是有一些细节。
74 |
75 | 例如这里for循环,可不像是在 [回溯算法:求组合问题!](https://programmercarl.com/0077.组合.html)和[回溯算法:求组合总和!](https://programmercarl.com/0216.组合总和III.html)中从startIndex开始遍历的。
76 |
77 | **因为本题每一个数字代表的是不同集合,也就是求不同集合之间的组合,而[回溯算法:求组合问题!](https://programmercarl.com/0077.组合.html)和[回溯算法:求组合总和!](https://programmercarl.com/0216.组合总和III.html)都是是求同一个集合中的组合!**
78 |
79 | 如果大家在现场面试的时候,一定要注意各种输入异常的情况,例如本题输入1 * #按键。
80 |
81 | 其实本题不算难,但也处处是细节,还是要反复琢磨。
82 |
83 | ## 周六
84 |
85 | 因为之前链表系列没有写总结,虽然链表系列已经是两个月前的事情,但还是有必要补一下。
86 |
87 | 所以给出[链表:总结篇!](https://programmercarl.com/链表总结篇.html),这里对之前链表理论基础和经典题目进行了总结。
88 |
89 | 同时对[链表:环找到了,那入口呢?](https://programmercarl.com/0142.环形链表II.html)中求环入口的问题又进行了补充证明,可以说把环形链表的方方面面都讲的很通透了,大家如果没有做过环形链表的题目一定要去做一做。
90 |
91 | ## 总结
92 |
93 | 相信通过这一周对回溯法的学习,大家已经掌握其题本套路了,也不会对回溯法那么畏惧了。
94 |
95 | 回溯法抽象为树形结构后,其遍历过程就是:**for循环横向遍历,递归纵向遍历,回溯不断调整结果集**。
96 |
97 | 这个是我做了很多回溯的题目,不断摸索其规律才总结出来的。
98 |
99 | 对于回溯法的整体框架,网上搜的文章这块一般都说不清楚,按照天上掉下来的代码对着讲解,不知道究竟是怎么来的,也不知道为什么要这么写。
100 |
101 | 所以,录友们刚开始学回溯法,起跑姿势就很标准了。
102 |
103 | 下周依然是回溯法,难度又要上升一个台阶了。
104 |
105 | 最后祝录友们周末愉快!
106 |
107 | **如果感觉「代码随想录」不错,就分享给身边的同学朋友吧,一起来学习算法!**
108 |
109 |
110 |
111 | ------------------------
112 |
113 | * 微信:[程序员Carl](https://mp.weixin.qq.com/s/b66DFkOp8OOxdZC_xLZxfw)
114 | * B站:[代码随想录](https://space.bilibili.com/525438321)
115 | * 知识星球:[代码随想录](https://mp.weixin.qq.com/s/QVF6upVMSbgvZy8lHZS3CQ)
116 |
117 |
118 |
--------------------------------------------------------------------------------
/problems/周总结/20201112回溯周末总结.md:
--------------------------------------------------------------------------------
1 |
2 | # 本周小结!(回溯算法系列三)
3 |
4 | ## 周一
5 |
6 | 在[回溯算法:求子集问题(二)](https://programmercarl.com/0090.子集II.html)中,开始针对子集问题进行去重。
7 |
8 | 本题就是[回溯算法:求子集问题!](https://programmercarl.com/0078.子集.html)的基础上加上了去重,去重我们在[回溯算法:求组合总和(三)](https://programmercarl.com/0040.组合总和II.html)也讲过了。
9 |
10 | 所以本题对大家应该并不难。
11 |
12 | 树形结构如下:
13 |
14 | 
15 |
16 | ## 周二
17 |
18 | 在[回溯算法:递增子序列](https://programmercarl.com/0491.递增子序列.html)中,处处都能看到子集的身影,但处处是陷阱,值得好好琢磨琢磨!
19 |
20 | 树形结构如下:
21 | 
22 |
23 | [回溯算法:递增子序列](https://programmercarl.com/0491.递增子序列.html)留言区大家有很多疑问,主要还是和[回溯算法:求子集问题(二)](https://programmercarl.com/0090.子集II.html)混合在了一起。
24 |
25 | 详细在[本周小结!(回溯算法系列三)续集](https://mp.weixin.qq.com/s/kSMGHc_YpsqL2j-jb_E_Ag)中给出了介绍!
26 |
27 | ## 周三
28 |
29 | 我们已经分析了组合问题,分割问题,子集问题,那么[回溯算法:排列问题!](https://programmercarl.com/0046.全排列.html) 又不一样了。
30 |
31 | 排列是有序的,也就是说[1,2] 和[2,1] 是两个集合,这和之前分析的子集以及组合所不同的地方。
32 |
33 | 可以看出元素1在[1,2]中已经使用过了,但是在[2,1]中还要在使用一次1,所以处理排列问题就不用使用startIndex了。
34 |
35 | 如图:
36 | 
37 |
38 | **大家此时可以感受出排列问题的不同:**
39 |
40 | * 每层都是从0开始搜索而不是startIndex
41 | * 需要used数组记录path里都放了哪些元素了
42 |
43 | ## 周四
44 |
45 | 排列问题也要去重了,在[回溯算法:排列问题(二)](https://programmercarl.com/0047.全排列II.html)中又一次强调了“树层去重”和“树枝去重”。
46 |
47 | 树形结构如下:
48 |
49 | 
50 |
51 | **这道题目神奇的地方就是used[i - 1] == false也可以,used[i - 1] == true也可以!**
52 |
53 | 我就用输入: [1,1,1] 来举一个例子。
54 |
55 | 树层上去重(used[i - 1] == false),的树形结构如下:
56 |
57 | 
58 |
59 | 树枝上去重(used[i - 1] == true)的树型结构如下:
60 |
61 | 
62 |
63 | **可以清晰的看到使用(used[i - 1] == false),即树层去重,效率更高!**
64 |
65 | ## 性能分析
66 |
67 | 之前并没有分析各个问题的时间复杂度和空间复杂度,这次来说一说。
68 |
69 | 这块网上的资料鱼龙混杂,一些所谓的经典面试书籍根本不讲回溯算法,算法书籍对这块也避而不谈,感觉就像是算法里模糊的边界。
70 |
71 | **所以这块就说一说我个人理解,对内容持开放态度,集思广益,欢迎大家来讨论!**
72 |
73 | 子集问题分析:
74 |
75 | * 时间复杂度:$O(n × 2^n)$,因为每一个元素的状态无外乎取与不取,所以时间复杂度为$O(2^n)$,构造每一组子集都需要填进数组,又有需要$O(n)$,最终时间复杂度:$O(n × 2^n)$。
76 | * 空间复杂度:$O(n)$,递归深度为n,所以系统栈所用空间为$O(n)$,每一层递归所用的空间都是常数级别,注意代码里的result和path都是全局变量,就算是放在参数里,传的也是引用,并不会新申请内存空间,最终空间复杂度为$O(n)$。
77 |
78 | 排列问题分析:
79 |
80 | * 时间复杂度:$O(n!)$,这个可以从排列的树形图中很明显发现,每一层节点为n,第二层每一个分支都延伸了n-1个分支,再往下又是n-2个分支,所以一直到叶子节点一共就是 n * n-1 * n-2 * ..... 1 = n!。每个叶子节点都会有一个构造全排列填进数组的操作(对应的代码:`result.push_back(path)`),该操作的复杂度为$O(n)$。所以,最终时间复杂度为:n * n!,简化为$O(n!)$。
81 | * 空间复杂度:$O(n)$,和子集问题同理。
82 |
83 | 组合问题分析:
84 |
85 | * 时间复杂度:$O(n × 2^n)$,组合问题其实就是一种子集的问题,所以组合问题最坏的情况,也不会超过子集问题的时间复杂度。
86 | * 空间复杂度:$O(n)$,和子集问题同理。
87 |
88 | **一般说道回溯算法的复杂度,都说是指数级别的时间复杂度,这也算是一个概括吧!**
89 |
90 | ## 总结
91 |
92 | 本周我们对[子集问题进行了去重](https://programmercarl.com/0090.子集II.html),然后介绍了和子集问题非常像的[递增子序列](https://programmercarl.com/0491.递增子序列.html),如果还保持惯性思维,这道题就可以掉坑里。
93 |
94 | 接着介绍了[排列问题!](https://programmercarl.com/0046.全排列.html),以及对[排列问题如何进行去重](https://programmercarl.com/0047.全排列II.html)。
95 |
96 | 最后我补充了子集问题,排列问题和组合问题的性能分析,给大家提供了回溯算法复杂度的分析思路。
97 |
98 |
99 |
100 |
101 |
--------------------------------------------------------------------------------
/problems/周总结/20201126贪心周末总结.md:
--------------------------------------------------------------------------------
1 |
2 | # 本周小结!(贪心算法系列一)
3 |
4 | ## 周一
5 |
6 | 本周正式开始了贪心算法,在[关于贪心算法,你该了解这些!](https://programmercarl.com/贪心算法理论基础.html)中,我们介绍了什么是贪心以及贪心的套路。
7 |
8 | **贪心的本质是选择每一阶段的局部最优,从而达到全局最优。**
9 |
10 | 有没有啥套路呢?
11 |
12 | **不好意思,贪心没套路,就刷题而言,如果感觉好像局部最优可以推出全局最优,然后想不到反例,那就试一试贪心吧!**
13 |
14 | 而严格的数据证明一般有如下两种:
15 |
16 | * 数学归纳法
17 | * 反证法
18 |
19 | 数学就不在讲解范围内了,感兴趣的同学可以自己去查一查资料。
20 |
21 | 正是因为贪心算法有时候会感觉这是常识,本就应该这么做! 所以大家经常看到网上有人说这是一道贪心题目,有人说这不是。
22 |
23 | 这里说一下我的依据:**如果找到局部最优,然后推出整体最优,那么就是贪心**,大家可以参考哈。
24 |
25 | ## 周二
26 |
27 |
28 | 在[贪心算法:分发饼干](https://programmercarl.com/0455.分发饼干.html)中讲解了贪心算法的第一道题目。
29 |
30 | 这道题目很明显能看出来是用贪心,也是入门好题。
31 |
32 | 我在文中给出**局部最优:大饼干喂给胃口大的,充分利用饼干尺寸喂饱一个,全局最优:喂饱尽可能多的小孩**。
33 |
34 | 很多录友都是用小饼干优先先喂饱小胃口的。
35 |
36 | 后来我想一想,虽然结果是一样的,但是大家的这个思考方式更好一些。
37 |
38 | **因为用小饼干优先喂饱小胃口的 这样可以尽量保证最后省下来的是大饼干(虽然题目没有这个要求)!**
39 |
40 | 所以还是小饼干优先先喂饱小胃口更好一些,也比较直观。
41 |
42 | 一些录友不清楚[贪心算法:分发饼干](https://programmercarl.com/0455.分发饼干.html)中时间复杂度是怎么来的?
43 |
44 | 就是快排O(nlog n),遍历O(n),加一起就是还是O(nlogn)。
45 |
46 | ## 周三
47 |
48 | 接下来就要上一点难度了,要不然大家会误以为贪心算法就是常识判断一下就行了。
49 |
50 | 在[贪心算法:摆动序列](https://programmercarl.com/0376.摆动序列.html)中,需要计算最长摇摆序列。
51 |
52 | 其实就是让序列有尽可能多的局部峰值。
53 |
54 | 局部最优:删除单调坡度上的节点(不包括单调坡度两端的节点),那么这个坡度就可以有两个局部峰值。
55 |
56 | 整体最优:整个序列有最多的局部峰值,从而达到最长摆动序列。
57 |
58 | 在计算峰值的时候,还是有一些代码技巧的,例如序列两端的峰值如何处理。
59 |
60 | 这些技巧,其实还是要多看多用才会掌握。
61 |
62 |
63 | ## 周四
64 |
65 | 在[贪心算法:最大子序和](https://programmercarl.com/0053.最大子序和.html)中,详细讲解了用贪心的方式来求最大子序列和,其实这道题目是一道动态规划的题目。
66 |
67 | **贪心的思路为局部最优:当前“连续和”为负数的时候立刻放弃,从下一个元素重新计算“连续和”,因为负数加上下一个元素 “连续和”只会越来越小。从而推出全局最优:选取最大“连续和”**
68 |
69 | 代码很简单,但是思路却比较难。还需要反复琢磨。
70 |
71 | 针对[贪心算法:最大子序和](https://programmercarl.com/0053.最大子序和.html)文章中给出的贪心代码如下;
72 | ```cpp
73 | class Solution {
74 | public:
75 | int maxSubArray(vector& nums) {
76 | int result = INT32_MIN;
77 | int count = 0;
78 | for (int i = 0; i < nums.size(); i++) {
79 | count += nums[i];
80 | if (count > result) { // 取区间累计的最大值(相当于不断确定最大子序终止位置)
81 | result = count;
82 | }
83 | if (count <= 0) count = 0; // 相当于重置最大子序起始位置,因为遇到负数一定是拉低总和
84 | }
85 | return result;
86 | }
87 | };
88 | ```
89 | 不少同学都来问,如果数组全是负数这个代码就有问题了,如果数组里有int最小值这个代码就有问题了。
90 |
91 | 大家不要脑洞模拟哈,可以亲自构造一些测试数据试一试,就发现其实没有问题。
92 |
93 | 数组都为负数,result记录的就是最大的负数,如果数组里有int最小值,那么最终result就是int最小值。
94 |
95 |
96 | ## 总结
97 |
98 | 本周我们讲解了[贪心算法的理论基础](https://programmercarl.com/贪心算法理论基础.html),了解了贪心本质:局部最优推出全局最优。
99 |
100 | 然后讲解了第一道题目[分发饼干](https://programmercarl.com/0455.分发饼干.html),还是比较基础的,可能会给大家一种贪心算法比较简单的错觉,因为贪心有时候接近于常识。
101 |
102 | 其实我还准备一些简单的贪心题目,甚至网上很多都质疑这些题目是不是贪心算法。这些题目我没有立刻发出来,因为真的会让大家感觉贪心过于简单,而忽略了贪心的本质:局部最优和全局最优两个关键点。
103 |
104 | **所以我在贪心系列难度会有所交替,难的题目在于拓展思路,简单的题目在于分析清楚其贪心的本质,后续我还会发一些简单的题目来做贪心的分析。**
105 |
106 | 在[摆动序列](https://programmercarl.com/0376.摆动序列.html)中大家就初步感受到贪心没那么简单了。
107 |
108 | 本周最后是[最大子序和](https://programmercarl.com/0053.最大子序和.html),这道题目要用贪心的方式做出来,就比较有难度,都知道负数加上正数之后会变小,但是这道题目依然会让很多人搞混淆,其关键在于:**不能让“连续和”为负数的时候加上下一个元素,而不是 不让“连续和”加上一个负数**。这块真的需要仔细体会!
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
--------------------------------------------------------------------------------
/problems/周总结/20201203贪心周末总结.md:
--------------------------------------------------------------------------------
1 |
2 | # 本周小结!(贪心算法系列二)
3 |
4 | ## 周一
5 |
6 | 一说到股票问题,一般都会想到动态规划,其实有时候贪心更有效!
7 |
8 | 在[贪心算法:买卖股票的最佳时机II](https://programmercarl.com/0122.买卖股票的最佳时机II.html)中,讲到只能多次买卖一支股票,如何获取最大利润。
9 |
10 | **这道题目理解利润拆分是关键点!** 不要整块的去看,而是把整体利润拆为每天的利润,就很容易想到贪心了。
11 |
12 | **局部最优:只收集每天的正利润,全局最优:得到最大利润**。
13 |
14 | 如果正利润连续上了,相当于连续持有股票,而本题并不需要计算具体的区间。
15 |
16 | 如图:
17 |
18 | 
19 |
20 | ## 周二
21 |
22 | 在[贪心算法:跳跃游戏](https://programmercarl.com/0055.跳跃游戏.html)中是给你一个数组看能否跳到终点。
23 |
24 | 本题贪心的关键是:**不用拘泥于每次究竟跳几步,而是看覆盖范围,覆盖范围内一定是可以跳过来的,不用管是怎么跳的**。
25 |
26 | **那么这个问题就转化为跳跃覆盖范围究竟可不可以覆盖到终点!**
27 |
28 | 贪心算法局部最优解:移动下标每次取最大跳跃步数(取最大覆盖范围),整体最优解:最后得到整体最大覆盖范围,看是否能到终点
29 |
30 | 如果覆盖范围覆盖到了终点,就表示一定可以跳过去。
31 |
32 | 如图:
33 |
34 | 
35 |
36 |
37 | ## 周三
38 |
39 | 这道题目:[贪心算法:跳跃游戏II](https://programmercarl.com/0045.跳跃游戏II.html)可就有点难了。
40 |
41 | 本题解题关键在于:**以最小的步数增加最大的覆盖范围,直到覆盖范围覆盖了终点**。
42 |
43 | 那么局部最优:求当前这步的最大覆盖,那么尽可能多走,到达覆盖范围的终点,只需要一步。整体最优:达到终点,步数最少。
44 |
45 | 如图:
46 |
47 | 
48 |
49 | 注意:**图中的移动下标是到当前这步覆盖的最远距离(下标2的位置),此时没有到终点,只能增加第二步来扩大覆盖范围**。
50 |
51 | 在[贪心算法:跳跃游戏II](https://programmercarl.com/0045.跳跃游戏II.html)中我给出了两个版本的代码。
52 |
53 | 其实本质都是超过当前覆盖范围,步数就加一,但版本一需要考虑当前覆盖最远距离下标是不是数组终点的情况。
54 |
55 | 而版本二就比较统一的,超过范围,步数就加一,但在移动下标的范围了做了文章。
56 |
57 | 即如果覆盖最远距离下标是倒数第二点:直接加一就行,默认一定可以到终点。如图:
58 | 
59 |
60 | 如果覆盖最远距离下标不是倒数第二点,说明本次覆盖已经到终点了。如图:
61 | 
62 |
63 | 有的录友认为版本一好理解,有的录友认为版本二好理解,其实掌握一种就可以了,也不用非要比拼一下代码的简洁性,简洁程度都差不多了。
64 |
65 | 我个人倾向于版本一的写法,思路清晰一点,版本二会有点绕。
66 |
67 | ## 周四
68 |
69 | 这道题目:[贪心算法:K次取反后最大化的数组和](https://programmercarl.com/1005.K次取反后最大化的数组和.html)就比较简单了,用简单题来讲一讲贪心的思想。
70 |
71 | **这里其实用了两次贪心!**
72 |
73 | 第一次贪心:局部最优:让绝对值大的负数变为正数,当前数值达到最大,整体最优:整个数组和达到最大。
74 |
75 | 处理之后,如果K依然大于0,此时的问题是一个有序正整数序列,如何转变K次正负,让 数组和 达到最大。
76 |
77 | 第二次贪心:局部最优:只找数值最小的正整数进行反转,当前数值可以达到最大(例如正整数数组{5, 3, 1},反转1 得到-1 比 反转5得到的-5 大多了),全局最优:整个 数组和 达到最大。
78 |
79 | 例外一位录友留言给出一个很好的建议,因为文中是使用快排,仔细看题,**题目中限定了数据范围是正负一百,所以可以使用桶排序**,这样时间复杂度就可以优化为$O(n)$了。但可能代码要复杂一些了。
80 |
81 |
82 | ## 总结
83 |
84 | 大家会发现本周的代码其实都简单,但思路却很巧妙,并不容易写出来。
85 |
86 | 如果是第一次接触的话,其实很难想出来,就是接触过之后就会了,所以大家不用感觉自己想不出来而烦躁。
87 |
88 | 相信此时大家现在对贪心算法又有一个新的认识了,加油💪
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
--------------------------------------------------------------------------------
/problems/周总结/20201210复杂度分析周末总结.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | 正好也给「算法汇总」添加一个新专题-算法性能分析,以后如果有空余时间还会陆续更新这个模块,大家如果经常看「算法汇总」的话,就会发现,「算法汇总」里已经更新的三个模块「编程素养」「求职」「算法性能分析」,内容越来越丰满了,大家现在就可以去看看哈。
13 |
14 | 后面在算法题目之余,我还会继续更新这几个模块的!
15 |
16 | # 周一
17 |
18 | 在[程序员的简历应该这么写!!(附简历模板)](https://programmercarl.com/前序/程序员简历.html)中以我自己的总结经验为例讲一讲大家应该如何写简历。
19 |
20 | 主要有如下几点:
21 |
22 | * 简历篇幅不要过长
23 | * 谨慎使用“精通”
24 | * 拿不准的绝对不要写在简历上
25 | * 项目经验中要突出自己的贡献
26 | * 面试中如何变被动为主动
27 | * 博客的重要性
28 |
29 | 最后还给出我自己的简历模板。
30 |
31 | 每一个点我都在文章中详细讲解了应该怎么写,平时应该如何积累,以及面前如何准备。
32 |
33 | 如果大家把以上几点都注意到了,那就是一份优秀的简历了,至少简历上就没啥毛病,剩下的就看自己的技术功底和临场发挥了。
34 |
35 | 一些录友会问我学校不好怎么办,没有项目经验怎么办之类的问题。
36 |
37 | 其实这就不在简历技巧的范围内了。
38 |
39 | 对于学校的话,某些公司可能有硬性要求,但如果能力特别出众,机会也是很大的。 不过说实话,大家都是普通人,真正技术能力出众的选手毕竟是少数。
40 |
41 | **而且面试其实挺看缘分的**,相信大家应该都遇到过这种情景:同一家公司面别人的时候问题贼简单,然后人家就顺利拿offer,一到自己面的时候难题就上来了。
42 |
43 | 至于项目经验,没有项目,就要自己找找项目来做。
44 |
45 | 我的Github上有一些我曾经写过的一些小项目,大家可以去看看:https://github.com/youngyangyang04
46 |
47 | **最后就是要端正写简历的心态,写简历是在自己真实背景和水平下,把自己各个方面包装到极致!**
48 |
49 |
50 | # 周二
51 |
52 | 在[关于时间复杂度,你不知道的都在这里!](https://programmercarl.com/前序/关于时间复杂度,你不知道的都在这里!.html)中详细讲解了时间复杂度,很多被大家忽略的内容,在文中都做了详细的解释。
53 |
54 | 文中涉及如下问题:
55 |
56 | * 究竟什么是大O?大O表示什么意思?严格按照大O的定义来说,快排应该是$O(n^2)$的算法!
57 | * $O(n^2)$ 的算法为什么有时候比 $O(n)$ 的算法更优?
58 | * 什么时间复杂度为什么可以忽略常数项?
59 | * 如何简化复杂的时间复杂度表达式,原理是什么?
60 | * $O(\log n)$ 中的log究竟是以谁为底?
61 |
62 | 这些问题大家可能懵懵懂懂的了解一些,但一细问又答不上来。
63 |
64 | 相信看完本篇[关于时间复杂度,你不知道的都在这里!](https://programmercarl.com/前序/关于时间复杂度,你不知道的都在这里!.html),以上问题大家就理解的清晰多了。
65 |
66 | 文中最后还运用以上知识通过一道简单的题目具体分析了一下其时间复杂度,给出两种方法究竟谁最优。
67 |
68 | 可以说从理论到实战将时间复杂度讲的明明白白。
69 |
70 |
71 | # 周三
72 |
73 | 在[O(n)的算法居然超时了,此时的n究竟是多大?](https://programmercarl.com/前序/On的算法居然超时了,此时的n究竟是多大?.html)中介绍了大家在leetcode上提交代码经常遇到的一个问题-超时!
74 |
75 | 估计很多录友知道算法超时了,但没有注意过 O(n)的算法,如果1s内出结果,这个n究竟是多大?
76 |
77 | 文中从计算机硬件出发,分析计算机的计算性能,然后亲自做实验,整理出数据如下:
78 |
79 |
80 | 
81 |
82 | **大家有一个数量级上的概念就可以了!**
83 |
84 | 正如文中说到的,**作为一名合格的程序员,至少要知道我们的程序是1s后出结果还是一年后出结果**。
85 |
86 |
87 | # 周四
88 |
89 | 在[通过一道面试题目,讲一讲递归算法的时间复杂度!](https://programmercarl.com/前序/通过一道面试题目,讲一讲递归算法的时间复杂度!.html)中,讲一讲如果计算递归算法的时间复杂度。
90 |
91 | 递归的时间复杂度等于**递归的次数 * 每次递归中的操作次数**。
92 |
93 | 所以了解究竟递归了多少次就是重点。
94 |
95 | 文中通过一道简单的面试题:求x的n次方(**注意:这道面试题大厂面试官经常用!**),还原面试场景,来带大家深入了解一下递归的时间复杂度。
96 |
97 | 文中给出了四个版本的代码实现,并逐一分析了其时间复杂度。
98 |
99 | 此时大家就会发现,同一道题目,同样使用递归算法,有的同学会写出了O(n)的代码,有的同学就写出了 $O(\log n)$ 的代码。
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 |
--------------------------------------------------------------------------------
/problems/周总结/20201217贪心周末总结.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # 本周小结!(贪心算法系列三)
4 |
5 | 对于贪心,大多数同学都会感觉,不就是常识嘛,这算啥算法,那么本周的题目就可以带大家初步领略一下贪心的巧妙,贪心算法往往妙的出其不意。
6 |
7 | ## 周一
8 |
9 | 在[贪心算法:加油站](https://programmercarl.com/0134.加油站.html)中给出每一个加油站的汽油和开到这个加油站的消耗,问汽车能不能开一圈。
10 |
11 | 这道题目咋眼一看,感觉是一道模拟题,模拟一下汽车从每一个节点出发看看能不能开一圈,时间复杂度是O(n^2)。
12 |
13 | 即使用模拟这种情况,也挺考察代码技巧的。
14 |
15 | **for循环适合模拟从头到尾的遍历,而while循环适合模拟环形遍历,对于本题的场景要善于使用while!**
16 |
17 | 如果代码功力不到位,就模拟这种情况,可能写的也会很费劲。
18 |
19 | 本题的贪心解法,我给出两种解法。
20 |
21 | 对于解法一,其实我并不认为这是贪心,因为没有找出局部最优,而是直接从全局最优的角度上思考问题,但思路很巧妙,值得学习一下。
22 |
23 | 对于解法二,贪心的局部最优:当前累加rest[j]的和curSum一旦小于0,起始位置至少要是j+1,因为从j开始一定不行。全局最优:找到可以跑一圈的起始位置。
24 |
25 | 这里是可以从局部最优推出全局最优的,想不出反例,那就试试贪心。
26 |
27 | **解法二就体现出贪心的精髓,同时大家也会发现,虽然贪心是常识,有些常识并不容易,甚至很难!**
28 |
29 | ## 周二
30 |
31 | 在[贪心算法:分发糖果](https://programmercarl.com/0135.分发糖果.html)中我们第一次接触了需要考虑两个维度的情况。
32 |
33 | 例如这道题,是先考虑左边呢,还是考虑右边呢?
34 |
35 | **先考虑哪一边都可以! 就别两边一起考虑,那样就把自己陷进去了**。
36 |
37 | 先贪心一边,局部最优:只要右边评分比左边大,右边的孩子就多一个糖果,全局最优:相邻的孩子中,评分高的右孩子获得比左边孩子更多的糖果
38 |
39 | 如图:
40 |
41 | 
42 |
43 |
44 | 接着在贪心另一边,左孩子大于右孩子,左孩子的糖果就要比右孩子多。
45 |
46 | 此时candyVec[i](第i个小孩的糖果数量,左孩子)就有两个选择了,一个是candyVec[i + 1] + 1(从右孩子这个加1得到的糖果数量),一个是candyVec[i](之前比较右孩子大于左孩子得到的糖果数量)。
47 |
48 | 那么第二次贪心的局部最优:取candyVec[i + 1] + 1 和 candyVec[i] 最大的糖果数量,保证第i个小孩的糖果数量即大于左边的也大于右边的。全局最优:相邻的孩子中,评分高的孩子获得更多的糖果。
49 |
50 | 局部最优可以推出全局最优。
51 |
52 | 如图:
53 | 
54 |
55 |
56 | ## 周三
57 |
58 | 在[贪心算法:柠檬水找零](https://programmercarl.com/0860.柠檬水找零.html)中我们模拟了买柠檬水找零的过程。
59 |
60 | 这道题目刚一看,可能会有点懵,这要怎么找零才能保证完整全部账单的找零呢?
61 |
62 | **但仔细一琢磨就会发现,可供我们做判断的空间非常少!**
63 |
64 | 美元10只能给账单20找零,而美元5可以给账单10和账单20找零,美元5更万能!
65 |
66 | 局部最优:遇到账单20,优先消耗美元10,完成本次找零。全局最优:完成全部账单的找零。
67 |
68 | 局部最优可以推出全局最优。
69 |
70 | 所以把能遇到的情况分析一下,只要分析到具体情况了,一下子就豁然开朗了。
71 |
72 | 这道题目其实是一道简单题,但如果一开始就想从整体上寻找找零方案,就会把自己陷进去,各种情况一交叉,只会越想越复杂了。
73 |
74 | ## 周四
75 |
76 | 在[贪心算法:根据身高重建队列](https://programmercarl.com/0406.根据身高重建队列.html)中,我们再一次遇到了需要考虑两个维度的情况。
77 |
78 | 之前我们已经做过一道类似的了就是[贪心算法:分发糖果](https://programmercarl.com/0135.分发糖果.html),但本题比分发糖果难不少!
79 |
80 | [贪心算法:根据身高重建队列](https://programmercarl.com/0406.根据身高重建队列.html)中依然是要确定一边,然后在考虑另一边,两边一起考虑一定会蒙圈。
81 |
82 | 那么本题先确定k还是先确定h呢,也就是究竟先按h排序呢,还先按照k排序呢?
83 |
84 | 这里其实很考察大家的思考过程,如果按照k来从小到大排序,排完之后,会发现k的排列并不符合条件,身高也不符合条件,两个维度哪一个都没确定下来。
85 |
86 | **所以先从大到小按照h排个序,再来贪心k**。
87 |
88 | 此时局部最优:优先按身高高的people的k来插入。插入操作过后的people满足队列属性。全局最优:最后都做完插入操作,整个队列满足题目队列属性。
89 |
90 | 局部最优可以推出全局最优,找不出反例,那么就来贪心。
91 |
92 | ## 总结
93 |
94 | 「代码随想录」里已经讲了十一道贪心题目了,大家可以发现在每一道题目的讲解中,我都是把什么是局部最优,和什么是全局最优说清楚。
95 |
96 | 虽然有时候感觉贪心就是常识,但如果真正是常识性的题目,其实是模拟题,就不是贪心算法了!例如[贪心算法:加油站](https://programmercarl.com/0134.加油站.html)中的贪心方法一,其实我就认为不是贪心算法,而是直接从全局最优的角度上来模拟,因为方法里没有体现局部最优的过程。
97 |
98 | 而且大家也会发现,贪心并没有想象中的那么简单,贪心往往妙的出其不意,触不及防!
99 |
100 |
101 |
102 |
--------------------------------------------------------------------------------
/problems/周总结/20201224贪心周末总结.md:
--------------------------------------------------------------------------------
1 |
2 | # 本周小结!(贪心算法系列四)
3 |
4 | ## 周一
5 |
6 | 在[贪心算法:用最少数量的箭引爆气球](https://programmercarl.com/0452.用最少数量的箭引爆气球.html)中,我们开始讲解了重叠区间问题,用最少的弓箭射爆所有气球,其本质就是找到最大的重叠区间。
7 |
8 | 按照左边界进行排序后,如果气球重叠了,重叠气球中右边边界的最小值 之前的区间一定需要一个弓箭
9 |
10 | 如图:
11 |
12 | 
13 |
14 | 模拟射气球的过程,很多同学真的要去模拟了,实时把气球从数组中移走,这么写的话就复杂了,从前向后遍历重复的只要跳过就可以的。
15 |
16 | ## 周二
17 |
18 | 在[贪心算法:无重叠区间](https://programmercarl.com/0435.无重叠区间.html)中要去掉最少的区间,来让所有区间没有重叠。
19 |
20 | 我来按照右边界排序,从左向右记录非交叉区间的个数。最后用区间总数减去非交叉区间的个数就是需要移除的区间个数了。
21 |
22 | 如图:
23 |
24 | 
25 |
26 | 细心的同学就发现了,此题和 [贪心算法:用最少数量的箭引爆气球](https://programmercarl.com/0452.用最少数量的箭引爆气球.html)非常像。
27 |
28 | 弓箭的数量就相当于是非交叉区间的数量,只要把弓箭那道题目代码里射爆气球的判断条件加个等号(认为[0,1][1,2]不是相邻区间),然后用总区间数减去弓箭数量 就是要移除的区间数量了。
29 |
30 | 把[贪心算法:用最少数量的箭引爆气球](https://programmercarl.com/0452.用最少数量的箭引爆气球.html)代码稍做修改,就可以AC本题。
31 |
32 | 修改后的C++代码如下:
33 |
34 | ```CPP
35 | class Solution {
36 | public:
37 | // 按照区间左边界从大到小排序
38 | static bool cmp (const vector& a, const vector& b) {
39 | return a[0] < b[0];
40 | }
41 | int eraseOverlapIntervals(vector>& intervals) {
42 | if (intervals.size() == 0) return 0;
43 | sort(intervals.begin(), intervals.end(), cmp);
44 |
45 | int result = 1;
46 | for (int i = 1; i < intervals.size(); i++) {
47 | if (intervals[i][0] >= intervals[i - 1][1]) { // 需要要把> 改成 >= 就可以了
48 | result++; // 需要一支箭
49 | }
50 | else {
51 | intervals[i][1] = min(intervals[i - 1][1], intervals[i][1]); // 更新重叠气球最小右边界
52 | }
53 | }
54 | return intervals.size() - result;
55 | }
56 | };
57 | ```
58 |
59 | ## 周三
60 |
61 | [贪心算法:划分字母区间](https://programmercarl.com/0763.划分字母区间.html)中我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。
62 |
63 | 这道题目leetcode上标的是贪心,其实我不认为是贪心,因为没感受到局部最优和全局最优的关系。
64 |
65 | 但不影响这是一道好题,思路很不错,**通过字符出现最远距离取并集的方法,把出现过的字符都圈到一个区间里**。
66 |
67 | 解题过程分如下两步:
68 |
69 | * 统计每一个字符最后出现的位置
70 | * 从头遍历字符,并更新字符的最远出现下标,如果找到字符最远出现位置下标和当前下标相等了,则找到了分割点
71 |
72 | 如图:
73 |
74 | 
75 |
76 |
77 | ## 周四
78 |
79 | [贪心算法:合并区间](https://programmercarl.com/0056.合并区间.html)中要合并所有重叠的区间。
80 |
81 | 相信如果录友们前几天区间问题的题目认真练习了,今天题目就应该算简单一些了。
82 |
83 | 按照左边界排序,排序之后局部最优:每次合并都取最大的右边界,这样就可以合并更多的区间了,整体最优:合并所有重叠的区间。
84 |
85 | 具体操作:按照左边界从小到大排序之后,如果 intervals[i][0] < intervals[i - 1][1] 即intervals[i]左边界 < intervals[i - 1]右边界,则一定有重复,因为intervals[i]的左边界一定是大于等于intervals[i - 1]的左边界。
86 |
87 | 如图:
88 |
89 | 
90 |
91 |
92 | ## 总结
93 |
94 | 本周的主题就是用贪心算法来解决区间问题,经过本周的学习,大家应该对区间的各种合并分割有一定程度的了解了。
95 |
96 | 其实很多区间的合并操作看起来都是常识,其实贪心算法有时候就是常识,但也别小看了贪心算法。
97 |
98 | 在[贪心算法:合并区间](https://programmercarl.com/0056.合并区间.html)中就说过,对于贪心算法,很多同学都是:「如果能凭常识直接做出来,就会感觉不到自己用了贪心, 一旦第一直觉想不出来, 可能就一直想不出来了」。
99 |
100 | 所以还是要多看多做多练习!
101 |
102 | **「代码随想录」里总结的都是经典题目,大家跟着练就节省了不少选择题目的时间了**。
103 |
104 |
105 |
106 |
--------------------------------------------------------------------------------
/problems/周总结/20210107动规周末总结.md:
--------------------------------------------------------------------------------
1 | # 本周小结!(动态规划系列一)
2 |
3 | 这周我们正式开始动态规划的学习!
4 |
5 | ## 周一
6 |
7 | 在[关于动态规划,你该了解这些!](https://programmercarl.com/动态规划理论基础.html)中我们讲解了动态规划的基础知识。
8 |
9 | 首先讲一下动规和贪心的区别,其实大家不用太强调理论上的区别,做做题,就感受出来了。
10 |
11 | 然后我们讲了动规的五部曲:
12 |
13 | 1. 确定dp数组(dp table)以及下标的含义
14 | 2. 确定递推公式
15 | 3. dp数组如何初始化
16 | 4. 确定遍历顺序
17 | 5. 举例推导dp数组
18 |
19 | 后序我们在讲解动规的题目时候,都离不开这五步!
20 |
21 | 本周都是简单题目,大家可能会感觉 按照这五部来好麻烦,凭感觉随手一写,直接就过,越到后面越会感觉,凭感觉这个事还是不靠谱的。
22 |
23 | 最后我们讲了动态规划题目应该如何debug,相信一些录友做动规的题目,一旦报错也是凭感觉来改。
24 |
25 | 其实只要把dp数组打印出来,哪里有问题一目了然!
26 |
27 | **如果代码写出来了,一直AC不了,灵魂三问:**
28 |
29 | 1. 这道题目我举例推导状态转移公式了么?
30 | 2. 我打印dp数组的日志了么?
31 | 3. 打印出来了dp数组和我想的一样么?
32 |
33 | 专治各种代码写出来了但AC不了的疑难杂症。
34 |
35 | ## 周二
36 |
37 | 这道题目[动态规划:斐波那契数](https://programmercarl.com/0509.斐波那契数.html)是当之无愧的动规入门题。
38 |
39 | 简单题,我们就是用来了解方法论的,用动规五部曲走一遍,题目其实已经把递推公式,和dp数组如何初始化都给我们了。
40 |
41 | ## 周三
42 |
43 | [动态规划:爬楼梯](https://programmercarl.com/0070.爬楼梯.html) 这道题目其实就是斐波那契数列。
44 |
45 | 但正常思考过程应该是推导完递推公式之后,发现这是斐波那契,而不是上来就知道这是斐波那契。
46 |
47 | 在这道题目的第三步,确认dp数组如何初始化,其实就可以看出来,对dp[i]定义理解的深度。
48 |
49 | dp[0]其实就是一个无意义的存在,不用去初始化dp[0]。
50 |
51 | 有的题解是把dp[0]初始化为1,然后遍历的时候i从2开始遍历,这样是可以解题的,然后强行解释一波dp[0]应该等于1的含义。
52 |
53 | 一个严谨的思考过程,应该是初始化dp[1] = 1,dp[2] = 2,然后i从3开始遍历,代码如下:
54 |
55 | ```CPP
56 | dp[1] = 1;
57 | dp[2] = 2;
58 | for (int i = 3; i <= n; i++) { // 注意i是从3开始的
59 | dp[i] = dp[i - 1] + dp[i - 2];
60 | }
61 | ```
62 |
63 | 这个可以是面试的一个小问题,考察候选人对dp[i]定义的理解程度。
64 |
65 | 这道题目还可以继续深化,就是一步一个台阶,两个台阶,三个台阶,直到 m个台阶,有多少种方法爬到n阶楼顶。
66 |
67 | 这又有难度了,这其实是一个完全背包问题,但力扣上没有这种题目,所以后续我在讲解背包问题的时候,今天这道题还会拿从背包问题的角度上来再讲一遍。
68 |
69 | 这里我先给出我的实现代码:
70 |
71 | ```CPP
72 | class Solution {
73 | public:
74 | int climbStairs(int n) {
75 | vector dp(n + 1, 0);
76 | dp[0] = 1;
77 | for (int i = 1; i <= n; i++) {
78 | for (int j = 1; j <= m; j++) { // 把m换成2,就可以AC爬楼梯这道题
79 | if (i - j >= 0) dp[i] += dp[i - j];
80 | }
81 | }
82 | return dp[n];
83 | }
84 | };
85 | ```
86 |
87 | 代码中m表示最多可以爬m个台阶。
88 |
89 | **以上代码不能运行哈,我主要是为了体现只要把m换成2,粘过去,就可以AC爬楼梯这道题,不信你就粘一下试试**。
90 |
91 |
92 | **此时我就发现一个绝佳的大厂面试题**,第一道题就是单纯的爬楼梯,然后看候选人的代码实现,如果把dp[0]的定义成1了,就可以发难了,为什么dp[0]一定要初始化为1,此时可能候选人就要强行给dp[0]应该是1找各种理由。那这就是一个考察点了,对dp[i]的定义理解的不深入。
93 |
94 | 然后可以继续发难,如果一步一个台阶,两个台阶,三个台阶,直到 m个台阶,有多少种方法爬到n阶楼顶。这道题目leetcode上并没有原题,绝对是考察候选人算法能力的绝佳好题。
95 |
96 | 这一连套问下来,候选人算法能力如何,面试官心里就有数了。
97 |
98 | **其实大厂面试最喜欢问题的就是这种简单题,然后慢慢变化,在小细节上考察候选人**。
99 |
100 | 这道绝佳的面试题我没有用过,如果录友们有面试别人的需求,就把这个套路拿去吧。
101 |
102 | 我在[通过一道面试题目,讲一讲递归算法的时间复杂度!](https://programmercarl.com/前序/通过一道面试题目,讲一讲递归算法的时间复杂度!.html)中,以我自己面试别人的真实经历,通过求x的n次方 这么简单的题目,就可以考察候选人对算法性能以及递归的理解深度,录友们可以看看,绝对有收获!
103 |
104 | ## 周四
105 |
106 | 这道题目[动态规划:使用最小花费爬楼梯](https://programmercarl.com/0746.使用最小花费爬楼梯.html)就是在爬台阶的基础上加了一个花费,
107 |
108 | 这道题描述也确实有点魔幻。
109 |
110 | 题目描述为:每当你爬上一个阶梯你都要花费对应的体力值,一旦支付了相应的体力值,你就可以选择向上爬一个阶梯或者爬两个阶梯。
111 |
112 | 示例1:
113 |
114 | 输入:cost = [10, 15, 20]
115 | 输出:15
116 |
117 |
118 | **从题目描述可以看出:要不是第一步不需要花费体力,要不就是第最后一步不需要花费体力,我个人理解:题意说的其实是第一步是要支付费用的!**。因为是当你爬上一个台阶就要花费对应的体力值!
119 |
120 | 所以我定义的dp[i]意思是也是第一步是要花费体力的,最后一步不用花费体力了,因为已经支付了。
121 |
122 | 之后一些录友在留言区说 可以定义dp[i]为:第一步是不花费体力,最后一步是花费体力的。
123 |
124 | 所以代码也可以这么写:
125 |
126 | ```CPP
127 | class Solution {
128 | public:
129 | int minCostClimbingStairs(vector& cost) {
130 | vector dp(cost.size() + 1);
131 | dp[0] = 0; // 默认第一步都是不花费体力的
132 | dp[1] = 0;
133 | for (int i = 2; i <= cost.size(); i++) {
134 | dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
135 | }
136 | return dp[cost.size()];
137 | }
138 | };
139 | ```
140 |
141 | 这么写看上去比较顺,但是就是感觉和题目描述的不太符。也没有必要这么细扣题意了,大家只要知道,题目的意思反正就是要不是第一步不花费,要不是最后一步不花费,都可以。
142 |
143 | ## 总结
144 |
145 | 本周题目简单一些,也非常合适初学者来练练手。
146 |
147 | 下周开始上难度了哈,然后大下周就开始讲解背包问题,好戏还在后面,录友们跟上哈。
148 |
149 | 学算法,认准「代码随想录」就够了,Carl带你打怪升级!
150 |
151 |
152 |
153 |
154 |
--------------------------------------------------------------------------------
/problems/周总结/20210121动规周末总结.md:
--------------------------------------------------------------------------------
1 |
2 | # 本周小结!(动态规划系列三)
3 |
4 | 本周我们正式开始讲解背包问题,也是动规里非常重要的一类问题。
5 |
6 | 背包问题其实有很多细节,如果了解个大概,然后也能一气呵成把代码写出来,但稍稍变变花样可能会陷入迷茫了。
7 |
8 | 开始回顾一下本周的内容吧!
9 |
10 | ## 周一
11 |
12 | [动态规划:关于01背包问题,你该了解这些!](https://programmercarl.com/背包理论基础01背包-1.html)中,我们开始介绍了背包问题。
13 |
14 | 首先对于背包的所有问题中,01背包是最最基础的,其他背包也是在01背包的基础上稍作变化。
15 |
16 | 所以我才花费这么大精力去讲解01背包。
17 |
18 | 关于其他几种常用的背包,大家看这张图就了然于胸了:
19 |
20 | 
21 |
22 | 本文用动规五部曲详细讲解了01背包的二维dp数组的实现方法,大家其实可以发现最简单的是推导公式了,推导公式估计看一遍就记下来了,但难就难在确定初始化和遍历顺序上。
23 |
24 | 1. 确定dp数组以及下标的含义
25 |
26 | dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。
27 |
28 | 2. 确定递推公式
29 |
30 | dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
31 |
32 | 3. dp数组如何初始化
33 |
34 | ```CPP
35 | // 初始化 dp
36 | vector> dp(weight.size() + 1, vector(bagWeight + 1, 0));
37 | for (int j = bagWeight; j >= weight[0]; j--) {
38 | dp[0][j] = dp[0][j - weight[0]] + value[0];
39 | }
40 | ```
41 |
42 | 4. 确定遍历顺序
43 |
44 | **01背包二维dp数组在遍历顺序上,外层遍历物品 ,内层遍历背包容量 和 外层遍历背包容量 ,内层遍历物品 都是可以的!**
45 |
46 | 但是先遍历物品更好理解。代码如下:
47 |
48 | ```CPP
49 | // weight数组的大小 就是物品个数
50 | for(int i = 1; i < weight.size(); i++) { // 遍历物品
51 | for(int j = 0; j <= bagWeight; j++) { // 遍历背包容量
52 | if (j < weight[i]) dp[i][j] = dp[i - 1][j]; // 这个是为了展现dp数组里元素的变化
53 | else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
54 |
55 | }
56 | }
57 | ```
58 |
59 | 5. 举例推导dp数组
60 |
61 | 背包最大重量为4。
62 |
63 | 物品为:
64 |
65 | | | 重量 | 价值 |
66 | | ----- | ---- | ---- |
67 | | 物品0 | 1 | 15 |
68 | | 物品1 | 3 | 20 |
69 | | 物品2 | 4 | 30 |
70 |
71 | 来看一下对应的dp数组的数值,如图:
72 |
73 | 
74 |
75 | 最终结果就是dp[2][4]。
76 |
77 |
78 | ## 周二
79 |
80 | [动态规划:关于01背包问题,你该了解这些!(滚动数组)](https://programmercarl.com/背包理论基础01背包-2.html)中把01背包的一维dp数组(滚动数组)实现详细讲解了一遍。
81 |
82 | 分析一下和二维dp数组有什么区别,在初始化和遍历顺序上又有什么差异?
83 |
84 | 最后总结了一道朴实无华的背包面试题。
85 |
86 | 要求候选人先实现一个纯二维的01背包,如果写出来了,然后再问为什么两个for循环的嵌套顺序这么写?反过来写行不行?再讲一讲初始化的逻辑。
87 |
88 | 然后要求实现一个一维数组的01背包,最后再问,一维数组的01背包,两个for循环的顺序反过来写行不行?为什么?
89 |
90 | 这几个问题就可以考察出候选人的算法功底了。
91 |
92 | 01背包一维数组分析如下:
93 |
94 | 1. 确定dp数组的定义
95 |
96 | 在一维dp数组中,dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j]。
97 |
98 | 2. 一维dp数组的递推公式
99 |
100 | ```
101 | dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
102 | ```
103 |
104 | 3. 一维dp数组如何初始化
105 |
106 | 如果物品价值都是大于0的,所以dp数组初始化的时候,都初始为0就可以了。
107 |
108 | 4. 一维dp数组遍历顺序
109 |
110 | 代码如下:
111 |
112 | ```CPP
113 | for(int i = 0; i < weight.size(); i++) { // 遍历物品
114 | for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
115 | dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
116 |
117 | }
118 | }
119 | ```
120 |
121 | 5. 举例推导dp数组
122 |
123 | 一维dp,分别用物品0,物品1,物品2 来遍历背包,最终得到结果如下:
124 |
125 | 
126 |
127 |
128 | ## 周三
129 |
130 | [动态规划:416. 分割等和子集](https://programmercarl.com/0416.分割等和子集.html)中我们开始用01背包来解决问题。
131 |
132 | 只有确定了如下四点,才能把01背包问题套到本题上来。
133 |
134 | * 背包的体积为sum / 2
135 | * 背包要放入的商品(集合里的元素)重量为 元素的数值,价值也为元素的数值
136 | * 背包如何正好装满,说明找到了总和为 sum / 2 的子集。
137 | * 背包中每一个元素是不可重复放入。
138 |
139 | 接下来就是一个完整的01背包问题,大家应该可以轻松做出了。
140 |
141 | ## 周四
142 |
143 | [动态规划:1049. 最后一块石头的重量 II](https://programmercarl.com/1049.最后一块石头的重量II.html)这道题目其实和[动态规划:416. 分割等和子集](https://programmercarl.com/0416.分割等和子集.html)是非常像的。
144 |
145 | 本题其实就是尽量让石头分成重量相同的两堆,相撞之后剩下的石头最小,这样就化解成01背包问题了。
146 |
147 | [动态规划:416. 分割等和子集](https://programmercarl.com/0416.分割等和子集.html)相当于是求背包是否正好装满,而本题是求背包最多能装多少。
148 |
149 | 这两道题目是对dp[target]的处理方式不同。这也考验的对dp[i]定义的理解。
150 |
151 |
152 | ## 总结
153 |
154 | 总体来说,本周信息量还是比较大的,特别对于对动态规划还不够了解的同学。
155 |
156 | 但如果坚持下来把,我在文章中列出的每一个问题,都仔细思考,消化为自己的知识,那么进步一定是飞速的。
157 |
158 | 有的同学可能看了看背包递推公式,上来就能撸它几道题目,然后背包问题就这么过去了,其实这样是很不牢固的。
159 |
160 | 就像是我们讲解01背包的时候,花了那么大力气才把每一个细节都讲清楚,这里其实是基础,后面的背包问题怎么变,基础比较牢固自然会有自己的一套思考过程。
161 |
162 |
163 |
164 |
--------------------------------------------------------------------------------
/problems/周总结/20210128动规周末总结.md:
--------------------------------------------------------------------------------
1 | # 本周小结!(动态规划系列四)
2 |
3 | ## 周一
4 |
5 | [动态规划:目标和!](https://programmercarl.com/0494.目标和.html)要求在数列之间加入+ 或者 -,使其和为S。
6 |
7 | 所有数的总和为sum,假设加法的总和为x,那么可以推出x = (S + sum) / 2。
8 |
9 | S 和 sum都是固定的,那此时问题就转化为01背包问题(数列中的数只能使用一次): 给你一些物品(数字),装满背包(就是x)有几种方法。
10 |
11 | 1. 确定dp数组以及下标的含义
12 |
13 | **dp[j] 表示:填满j(包括j)这么大容积的包,有dp[j]种方法**
14 |
15 | 2. 确定递推公式
16 |
17 | dp[j] += dp[j - nums[i]]
18 |
19 | **注意:求装满背包有几种方法类似的题目,递推公式基本都是这样的**。
20 |
21 | 3. dp数组如何初始化
22 |
23 | dp[0] 初始化为1 ,dp[j]其他下标对应的数值应该初始化为0。
24 |
25 | 4. 确定遍历顺序
26 |
27 | 01背包问题一维dp的遍历,nums放在外循环,target在内循环,且内循环倒序。
28 |
29 |
30 | 5. 举例推导dp数组
31 |
32 | 输入:nums: [1, 1, 1, 1, 1], S: 3
33 |
34 | bagSize = (S + sum) / 2 = (3 + 5) / 2 = 4
35 |
36 | dp数组状态变化如下:
37 |
38 | 
39 |
40 | ## 周二
41 |
42 | 这道题目[动态规划:一和零!](https://programmercarl.com/0474.一和零.html)算有点难度。
43 |
44 | **不少同学都以为是多重背包,其实这是一道标准的01背包**。
45 |
46 | 这不过这个背包有两个维度,一个是m 一个是n,而不同长度的字符串就是不同大小的待装物品。
47 |
48 | **所以这是一个二维01背包!**
49 |
50 | 1. 确定dp数组(dp table)以及下标的含义
51 |
52 | **dp[i][j]:最多有i个0和j个1的strs的最大子集的大小为dp[i][j]。**
53 |
54 |
55 | 2. 确定递推公式
56 |
57 | dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);
58 |
59 | 字符串集合中的一个字符串0的数量为zeroNum,1的数量为oneNum。
60 |
61 | 3. dp数组如何初始化
62 |
63 | 因为物品价值不会是负数,初始为0,保证递推的时候dp[i][j]不会被初始值覆盖。
64 |
65 | 4. 确定遍历顺序
66 |
67 | 01背包一定是外层for循环遍历物品,内层for循环遍历背包容量且从后向前遍历!
68 |
69 | 5. 举例推导dp数组
70 |
71 | 以输入:["10","0001","111001","1","0"],m = 3,n = 3为例
72 |
73 | 最后dp数组的状态如下所示:
74 |
75 |
76 | 
77 |
78 | ## 周三
79 |
80 | 此时01背包我们就讲完了,正式开始完全背包。
81 |
82 | 在[动态规划:关于完全背包,你该了解这些!](https://programmercarl.com/背包问题理论基础完全背包.html)中我们讲解了完全背包的理论基础。
83 |
84 | 其实完全背包和01背包区别就是完全背包的物品是无限数量。
85 |
86 | 递推公式也是一样的,但难点在于遍历顺序上!
87 |
88 | 完全背包的物品是可以添加多次的,所以遍历背包容量要从小到大去遍历,即:
89 |
90 | ```CPP
91 | // 先遍历物品,再遍历背包
92 | for(int i = 0; i < weight.size(); i++) { // 遍历物品
93 | for(int j = weight[i]; j < bagWeight ; j++) { // 遍历背包容量
94 | dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
95 |
96 | }
97 | }
98 | ```
99 |
100 | 基本网上题的题解介绍到这里就到此为止了。
101 |
102 | **那么为什么要先遍历物品,在遍历背包呢?** (灵魂拷问)
103 |
104 | 其实对于纯完全背包,先遍历物品,再遍历背包 与 先遍历背包,再遍历物品都是可以的。我在文中[动态规划:关于完全背包,你该了解这些!](https://programmercarl.com/背包问题理论基础完全背包.html)也给出了详细的解释。
105 |
106 | 这个细节是很多同学忽略掉的点,其实也不算细节了,**相信不少同学在写背包的时候,两层for循环的先后循序搞不清楚,靠感觉来的**。
107 |
108 | 所以理解究竟是先遍历啥,后遍历啥非常重要,这也体现出遍历顺序的重要性!
109 |
110 | 在文中,我也强调了是对纯完全背包,两个for循环先后循序无所谓,那么题目稍有变化,可就有所谓了。
111 |
112 | ## 周四
113 |
114 | 在[动态规划:给你一些零钱,你要怎么凑?](https://programmercarl.com/0518.零钱兑换II.html)中就是给你一堆零钱(零钱个数无限),为凑成amount的组合数有几种。
115 |
116 | **注意这里组合数和排列数的区别!**
117 |
118 | 看到无限零钱个数就知道是完全背包,
119 |
120 | 但本题不是纯完全背包了(求是否能装满背包),而是求装满背包有几种方法。
121 |
122 | 这里在遍历顺序上可就有说法了。
123 |
124 | * 如果求组合数就是外层for循环遍历物品,内层for遍历背包。
125 | * 如果求排列数就是外层for遍历背包,内层for循环遍历物品。
126 |
127 | 这里同学们需要理解一波,我在文中也给出了详细的解释,下周我们将介绍求排列数的完全背包题目来加深对这个遍历顺序的理解。
128 |
129 |
130 | ## 总结
131 |
132 | 相信通过本周的学习,大家已经初步感受到遍历顺序的重要性!
133 |
134 | 很多对动规理解不深入的同学都会感觉:动规嘛,就是把递推公式推出来其他都easy了。
135 |
136 | 其实这是一种错觉,或者说对动规理解的不够深入!
137 |
138 | 我在动规专题开篇介绍[关于动态规划,你该了解这些!](https://programmercarl.com/动态规划理论基础.html)中就强调了 **递推公式仅仅是 动规五部曲里的一小部分, dp数组的定义、初始化、遍历顺序,哪一点没有搞透的话,即使知道递推公式,遇到稍稍难一点的动规题目立刻会感觉写不出来了**。
139 |
140 | 此时相信大家对动规五部曲也有更深的理解了,同样也验证了Carl之前讲过的:**简单题是用来学习方法论的,而遇到难题才体现出方法论的重要性!**
141 |
142 |
143 |
144 |
--------------------------------------------------------------------------------
/problems/字符串总结.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!
6 |
7 |
8 | # 字符串:总结篇
9 |
10 | 其实我们已经学习了十天的字符串了,从字符串的定义到库函数的使用原则,从各种反转到KMP算法,相信大家应该对字符串有比较深刻的认识了。
11 |
12 | 那么这次我们来做一个总结。
13 |
14 | ## 什么是字符串
15 |
16 | 字符串是若干字符组成的有限序列,也可以理解为是一个字符数组,但是很多语言对字符串做了特殊的规定,接下来我来说一说C/C++中的字符串。
17 |
18 | 在C语言中,把一个字符串存入一个数组时,也把结束符 '\0'存入数组,并以此作为该字符串是否结束的标志。
19 |
20 | 例如这段代码:
21 |
22 | ```
23 | char a[5] = "asd";
24 | for (int i = 0; a[i] != '\0'; i++) {
25 | }
26 | ```
27 |
28 | 在C++中,提供一个string类,string类会提供 size接口,可以用来判断string类字符串是否结束,就不用'\0'来判断是否结束。
29 |
30 | 例如这段代码:
31 |
32 | ```
33 | string a = "asd";
34 | for (int i = 0; i < a.size(); i++) {
35 | }
36 | ```
37 |
38 | 那么vector< char > 和 string 又有什么区别呢?
39 |
40 | 其实在基本操作上没有区别,但是 string提供更多的字符串处理的相关接口,例如string 重载了+,而vector却没有。
41 |
42 | 所以想处理字符串,我们还是会定义一个string类型。
43 |
44 |
45 | ## 要不要使用库函数
46 |
47 | 在文章[344.反转字符串](https://programmercarl.com/0344.反转字符串.html)中强调了**打基础的时候,不要太迷恋于库函数。**
48 |
49 | 甚至一些同学习惯于调用substr,split,reverse之类的库函数,却不知道其实现原理,也不知道其时间复杂度,这样实现出来的代码,如果在面试现场,面试官问:“分析其时间复杂度”的话,一定会一脸懵逼!
50 |
51 | 所以建议**如果题目关键的部分直接用库函数就可以解决,建议不要使用库函数。**
52 |
53 | **如果库函数仅仅是 解题过程中的一小部分,并且你已经很清楚这个库函数的内部实现原理的话,可以考虑使用库函数。**
54 |
55 | ## 双指针法
56 |
57 |
58 | 在[344.反转字符串](https://programmercarl.com/0344.反转字符串.html) ,我们使用双指针法实现了反转字符串的操作,**双指针法在数组,链表和字符串中很常用。**
59 |
60 | 接着在[字符串:替换空格](https://programmercarl.com/剑指Offer05.替换空格.html),同样还是使用双指针法在时间复杂度O(n)的情况下完成替换空格。
61 |
62 | **其实很多数组填充类的问题,都可以先预先给数组扩容带填充后的大小,然后在从后向前进行操作。**
63 |
64 | 那么针对数组删除操作的问题,其实在[27. 移除元素](https://programmercarl.com/0027.移除元素.html)中就已经提到了使用双指针法进行移除操作。
65 |
66 | 同样的道理在[151.翻转字符串里的单词](https://programmercarl.com/0151.翻转字符串里的单词.html)中我们使用O(n)的时间复杂度,完成了删除冗余空格。
67 |
68 | 一些同学会使用for循环里调用库函数erase来移除元素,这其实是O(n^2)的操作,因为erase就是O(n)的操作,所以这也是典型的不知道库函数的时间复杂度,上来就用的案例了。
69 |
70 | ## 反转系列
71 |
72 | 在反转上还可以在加一些玩法,其实考察的是对代码的掌控能力。
73 |
74 | [541. 反转字符串II](https://programmercarl.com/0541.反转字符串II.html)中,一些同学可能为了处理逻辑:每隔2k个字符的前k的字符,写了一堆逻辑代码或者再搞一个计数器,来统计2k,再统计前k个字符。
75 |
76 | 其实**当需要固定规律一段一段去处理字符串的时候,要想想在在for循环的表达式上做做文章**。
77 |
78 | 只要让 i += (2 * k),i 每次移动 2 * k 就可以了,然后判断是否需要有反转的区间。
79 |
80 | 因为要找的也就是每2 * k 区间的起点,这样写程序会高效很多。
81 |
82 | 在[151.翻转字符串里的单词](https://programmercarl.com/0151.翻转字符串里的单词.html)中要求翻转字符串里的单词,这道题目可以说是综合考察了字符串的多种操作。是考察字符串的好题。
83 |
84 | 这道题目通过 **先整体反转再局部反转**,实现了反转字符串里的单词。
85 |
86 | 后来发现反转字符串还有一个牛逼的用处,就是达到左旋的效果。
87 |
88 | 在[字符串:反转个字符串还有这个用处?](https://programmercarl.com/剑指Offer58-II.左旋转字符串.html)中,我们通过**先局部反转再整体反转**达到了左旋的效果。
89 |
90 | ## KMP
91 |
92 | KMP的主要思想是**当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了。**
93 |
94 | KMP的精髓所在就是前缀表,在[KMP精讲](https://programmercarl.com/0028.实现strStr.html)中提到了,什么是KMP,什么是前缀表,以及为什么要用前缀表。
95 |
96 | 前缀表:起始位置到下标i之前(包括i)的子串中,有多大长度的相同前缀后缀。
97 |
98 | 那么使用KMP可以解决两类经典问题:
99 |
100 | 1. 匹配问题:[28. 实现 strStr()](https://programmercarl.com/0028.实现strStr.html)
101 | 2. 重复子串问题:[459.重复的子字符串](https://programmercarl.com/0459.重复的子字符串.html)
102 |
103 | 再一次强调了什么是前缀,什么是后缀,什么又是最长相等前后缀。
104 |
105 | 前缀:指不包含最后一个字符的所有以第一个字符开头的连续子串。
106 |
107 | 后缀:指不包含第一个字符的所有以最后一个字符结尾的连续子串。
108 |
109 | 然后**针对前缀表到底要不要减一,这其实是不同KMP实现的方式**,我们在[KMP精讲](https://programmercarl.com/0028.实现strStr.html)中针对之前两个问题,分别给出了两个不同版本的的KMP实现。
110 |
111 | 其中主要**理解j=next[x]这一步最为关键!**
112 |
113 | ## 总结
114 |
115 | 字符串类类型的题目,往往想法比较简单,但是实现起来并不容易,复杂的字符串题目非常考验对代码的掌控能力。
116 |
117 | 双指针法是字符串处理的常客。
118 |
119 | KMP算法是字符串查找最重要的算法,但彻底理解KMP并不容易,我们已经写了五篇KMP的文章,不断总结和完善,最终才把KMP讲清楚。
120 |
121 | 好了字符串相关的算法知识就介绍到了这里了,明天开始新的征程,大家加油!
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
--------------------------------------------------------------------------------
/problems/数组总结篇.md:
--------------------------------------------------------------------------------
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 | * **数组下标都是从0开始的。**
28 | * **数组内存空间的地址是连续的**
29 |
30 | 正是**因为数组在内存空间的地址是连续的,所以我们在删除或者增添元素的时候,就难免要移动其他元素的地址。**
31 |
32 | 例如删除下标为3的元素,需要对下标为3的元素后面的所有元素都要做移动操作,如图所示:
33 |
34 |
35 |
36 | 而且大家如果使用C++的话,要注意vector 和 array的区别,vector的底层实现是array,严格来讲vector是容器,不是数组。
37 |
38 | **数组的元素是不能删的,只能覆盖。**
39 |
40 | 那么二维数组直接上图,大家应该就知道怎么回事了
41 |
42 |
43 |
44 | **那么二维数组在内存的空间地址是连续的么?**
45 |
46 | 我们来举一个Java的例子,例如: `int[][] rating = new int[3][4];` , 这个二维数组在内存空间可不是一个 `3*4` 的连续地址空间
47 |
48 | 看了下图,就应该明白了:
49 |
50 |
51 |
52 | 所以**Java的二维数组在内存中不是 `3*4` 的连续地址空间,而是四条连续的地址空间组成!**
53 |
54 | ## 数组的经典题目
55 |
56 | 在面试中,数组是必考的基础数据结构。
57 |
58 | 其实数组的题目在思想上一般比较简单的,但是如果想高效,并不容易。
59 |
60 | 我们之前一共讲解了四道经典数组题目,每一道题目都代表一个类型,一种思想。
61 |
62 | ### 二分法
63 |
64 | [数组:每次遇到二分法,都是一看就会,一写就废](https://programmercarl.com/0704.二分查找.html)
65 |
66 | 这道题目呢,考察数组的基本操作,思路很简单,但是通过率在简单题里并不高,不要轻敌。
67 |
68 | 可以使用暴力解法,通过这道题目,如果追求更优的算法,建议试一试用二分法,来解决这道题目
69 |
70 | * 暴力解法时间复杂度:O(n)
71 | * 二分法时间复杂度:O(logn)
72 |
73 | 在这道题目中我们讲到了**循环不变量原则**,只有在循环中坚持对区间的定义,才能清楚的把握循环中的各种细节。
74 |
75 | **二分法是算法面试中的常考题,建议通过这道题目,锻炼自己手撕二分的能力**。
76 |
77 |
78 | ### 双指针法
79 |
80 | * [数组:就移除个元素很难么?](https://programmercarl.com/0027.移除元素.html)
81 |
82 | 双指针法(快慢指针法):**通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。**
83 |
84 | * 暴力解法时间复杂度:O(n^2)
85 | * 双指针时间复杂度:O(n)
86 |
87 | 这道题目迷惑了不少同学,纠结于数组中的元素为什么不能删除,主要是因为以下两点:
88 |
89 | * 数组在内存中是连续的地址空间,不能释放单一元素,如果要释放,就是全释放(程序运行结束,回收内存栈空间)。
90 | * C++中vector和array的区别一定要弄清楚,vector的底层实现是array,封装后使用更友好。
91 |
92 | 双指针法(快慢指针法)在数组和链表的操作中是非常常见的,很多考察数组和链表操作的面试题,都使用双指针法。
93 |
94 | ### 滑动窗口
95 |
96 | * [数组:滑动窗口拯救了你](https://programmercarl.com/0209.长度最小的子数组.html)
97 |
98 | 本题介绍了数组操作中的另一个重要思想:滑动窗口。
99 |
100 | * 暴力解法时间复杂度:O(n^2)
101 | * 滑动窗口时间复杂度:O(n)
102 |
103 | 本题中,主要要理解滑动窗口如何移动 窗口起始位置,达到动态更新窗口大小的,从而得出长度最小的符合条件的长度。
104 |
105 | **滑动窗口的精妙之处在于根据当前子序列和大小的情况,不断调节子序列的起始位置。从而将O(n^2)的暴力解法降为O(n)。**
106 |
107 | 如果没有接触过这一类的方法,很难想到类似的解题思路,滑动窗口方法还是很巧妙的。
108 |
109 |
110 | ### 模拟行为
111 |
112 | * [数组:这个循环可以转懵很多人!](https://programmercarl.com/0059.螺旋矩阵II.html)
113 |
114 | 模拟类的题目在数组中很常见,不涉及到什么算法,就是单纯的模拟,十分考察大家对代码的掌控能力。
115 |
116 | 在这道题目中,我们再一次介绍到了**循环不变量原则**,其实这也是写程序中的重要原则。
117 |
118 | 相信大家有遇到过这种情况: 感觉题目的边界调节超多,一波接着一波的判断,找边界,拆了东墙补西墙,好不容易运行通过了,代码写的十分冗余,毫无章法,其实**真正解决题目的代码都是简洁的,或者有原则性的**,大家可以在这道题目中体会到这一点。
119 |
120 | ### 前缀和
121 |
122 | > 代码随想录后续补充题目
123 |
124 | * [数组:求取区间和](https://programmercarl.com/kamacoder/0058.区间和.html)
125 |
126 | 前缀和的思路其实很简单,但非常实用,如果没接触过的录友,也很难想到这个解法维度,所以 这是开阔思路 而难度又不高的好题。
127 |
128 | ## 总结
129 |
130 | 
131 |
132 | 这个图是 [代码随想录知识星球](https://programmercarl.com/other/kstar.html) 成员:[海螺人](https://wx.zsxq.com/dweb2/index/footprint/844412858822412),所画,总结的非常好,分享给大家。
133 |
134 | 从二分法到双指针,从滑动窗口到螺旋矩阵,相信如果大家真的认真做了「代码随想录」每日推荐的题目,定会有所收获。
135 |
136 | 推荐的题目即使大家之前做过了,再读一遍文章,也会帮助你提炼出解题的精髓所在。
137 |
138 |
139 |
140 |
141 |
142 |
--------------------------------------------------------------------------------
/problems/数组理论基础.md:
--------------------------------------------------------------------------------
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 | * **数组下标都是从0开始的。**
30 | * **数组内存空间的地址是连续的**
31 |
32 | 正是**因为数组在内存空间的地址是连续的,所以我们在删除或者增添元素的时候,就难免要移动其他元素的地址。**
33 |
34 | 例如删除下标为3的元素,需要对下标为3的元素后面的所有元素都要做移动操作,如图所示:
35 |
36 | 
37 |
38 |
39 | 而且大家如果使用C++的话,要注意vector 和 array的区别,vector的底层实现是array,严格来讲vector是容器,不是数组。
40 |
41 | **数组的元素是不能删的,只能覆盖。**
42 |
43 | 那么二维数组直接上图,大家应该就知道怎么回事了
44 |
45 | 
46 |
47 | **那么二维数组在内存的空间地址是连续的么?**
48 |
49 | 不同编程语言的内存管理是不一样的,以C++为例,在C++中二维数组是连续分布的。
50 |
51 | 我们来做一个实验,C++测试代码如下:
52 |
53 | ```CPP
54 | void test_arr() {
55 | int array[2][3] = {
56 | {0, 1, 2},
57 | {3, 4, 5}
58 | };
59 | cout << &array[0][0] << " " << &array[0][1] << " " << &array[0][2] << endl;
60 | cout << &array[1][0] << " " << &array[1][1] << " " << &array[1][2] << endl;
61 | }
62 |
63 | int main() {
64 | test_arr();
65 | }
66 |
67 | ```
68 |
69 | 测试地址为
70 |
71 | ```
72 | 0x7ffee4065820 0x7ffee4065824 0x7ffee4065828
73 | 0x7ffee406582c 0x7ffee4065830 0x7ffee4065834
74 | ```
75 |
76 | 注意地址为16进制,可以看出二维数组地址是连续一条线的。
77 |
78 | 一些录友可能看不懂内存地址,我就简单介绍一下, 0x7ffee4065820 与 0x7ffee4065824 差了一个4,就是4个字节,因为这是一个int型的数组,所以两个相邻数组元素地址差4个字节。
79 |
80 | 0x7ffee4065828 与 0x7ffee406582c 也是差了4个字节,在16进制里8 + 4 = c,c就是12。
81 |
82 | 如图:
83 |
84 |
85 | 
86 |
87 | **所以可以看出在C++中二维数组在地址空间上是连续的**。
88 |
89 | 像Java是没有指针的,同时也不对程序员暴露其元素的地址,寻址操作完全交给虚拟机。
90 |
91 | 所以看不到每个元素的地址情况,这里我以Java为例,也做一个实验。
92 |
93 | ```Java
94 | public static void test_arr() {
95 | int[][] arr = {{1, 2, 3}, {3, 4, 5}, {6, 7, 8}, {9,9,9}};
96 | System.out.println(arr[0]);
97 | System.out.println(arr[1]);
98 | System.out.println(arr[2]);
99 | System.out.println(arr[3]);
100 | }
101 | ```
102 | 输出的地址为:
103 |
104 | ```
105 | [I@7852e922
106 | [I@4e25154f
107 | [I@70dea4e
108 | [I@5c647e05
109 | ```
110 |
111 | 这里的数值也是16进制,这不是真正的地址,而是经过处理过后的数值了,我们也可以看出,二维数组的每一行头结点的地址是没有规则的,更谈不上连续。
112 |
113 | 所以Java的二维数组可能是如下排列的方式:
114 |
115 |
116 | 
117 |
118 | 这里面试中数组相关的理论知识就介绍完了。
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
--------------------------------------------------------------------------------
/problems/栈与队列理论基础.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!
6 |
7 |
8 | > 来看看栈和队列不为人知的一面
9 |
10 | # 栈与队列理论基础
11 |
12 | 我想栈和队列的原理大家应该很熟悉了,队列是先进先出,栈是先进后出。
13 |
14 | 如图所示:
15 |
16 | 
17 |
18 | 那么我这里再列出四个关于栈的问题,大家可以思考一下。以下是以C++为例,使用其他编程语言的同学也对应思考一下,自己使用的编程语言里栈和队列是什么样的。
19 |
20 | 1. C++中stack 是容器么?
21 | 2. 我们使用的stack是属于哪个版本的STL?
22 | 3. 我们使用的STL中stack是如何实现的?
23 | 4. stack 提供迭代器来遍历stack空间么?
24 |
25 | 相信这四个问题并不那么好回答, 因为一些同学使用数据结构会停留在非常表面上的应用,稍稍往深一问,就会有好像懂,好像也不懂的感觉。
26 |
27 | 有的同学可能仅仅知道有栈和队列这么个数据结构,却不知道底层实现,也不清楚所使用栈和队列和STL是什么关系。
28 |
29 | 所以这里我再给大家扫一遍基础知识,
30 |
31 | 首先大家要知道 栈和队列是STL(C++标准库)里面的两个数据结构。
32 |
33 | C++标准库是有多个版本的,要知道我们使用的STL是哪个版本,才能知道对应的栈和队列的实现原理。
34 |
35 | 那么来介绍一下,三个最为普遍的STL版本:
36 |
37 | 1. HP STL
38 | 其他版本的C++ STL,一般是以HP STL为蓝本实现出来的,HP STL是C++ STL的第一个实现版本,而且开放源代码。
39 |
40 | 2. P.J.Plauger STL
41 | 由P.J.Plauger参照HP STL实现出来的,被Visual C++编译器所采用,不是开源的。
42 |
43 | 3. SGI STL
44 | 由Silicon Graphics Computer Systems公司参照HP STL实现,被Linux的C++编译器GCC所采用,SGI STL是开源软件,源码可读性甚高。
45 |
46 | 接下来介绍的栈和队列也是SGI STL里面的数据结构, 知道了使用版本,才知道对应的底层实现。
47 |
48 | 来说一说栈,栈先进后出,如图所示:
49 |
50 |
51 | 
52 |
53 | 栈提供push 和 pop 等等接口,所有元素必须符合先进后出规则,所以栈不提供走访功能,也不提供迭代器(iterator)。 不像是set 或者map 提供迭代器iterator来遍历所有元素。
54 |
55 | **栈是以底层容器完成其所有的工作,对外提供统一的接口,底层容器是可插拔的(也就是说我们可以控制使用哪种容器来实现栈的功能)。**
56 |
57 | 所以STL中栈往往不被归类为容器,而被归类为container adapter(容器适配器)。
58 |
59 | 那么问题来了,STL 中栈是用什么容器实现的?
60 |
61 | 从下图中可以看出,栈的内部结构,栈的底层实现可以是vector,deque,list 都是可以的, 主要就是数组和链表的底层实现。
62 |
63 |
64 | 
65 |
66 | **我们常用的SGI STL,如果没有指定底层实现的话,默认是以deque为缺省情况下栈的底层结构。**
67 |
68 | deque是一个双向队列,只要封住一段,只开通另一端就可以实现栈的逻辑了。
69 |
70 | **SGI STL中 队列底层实现缺省情况下一样使用deque实现的。**
71 |
72 | 我们也可以指定vector为栈的底层实现,初始化语句如下:
73 |
74 | ```cpp
75 | std::stack > third; // 使用vector为底层容器的栈
76 | ```
77 |
78 | 刚刚讲过栈的特性,对应的队列的情况是一样的。
79 |
80 | 队列中先进先出的数据结构,同样不允许有遍历行为,不提供迭代器, **SGI STL中队列一样是以deque为缺省情况下的底部结构。**
81 |
82 | 也可以指定list 为起底层实现,初始化queue的语句如下:
83 |
84 | ```cpp
85 | std::queue> third; // 定义以list为底层容器的队列
86 | ```
87 |
88 | 所以STL 队列也不被归类为容器,而被归类为container adapter( 容器适配器)。
89 |
90 | 我这里讲的都是C++ 语言中的情况, 使用其他语言的同学也要思考栈与队列的底层实现问题, 不要对数据结构的使用浅尝辄止,而要深挖其内部原理,才能夯实基础。
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
--------------------------------------------------------------------------------
/problems/背包总结篇.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!
6 |
7 |
8 | # 听说背包问题很难? 这篇总结篇来拯救你了
9 |
10 | 年前我们已经把背包问题都讲完了,那么现在我们要对背包问题进行总结一番。
11 |
12 | 背包问题是动态规划里的非常重要的一部分,所以我把背包问题单独总结一下,等动态规划专题更新完之后,我们还会在整体总结一波动态规划。
13 |
14 | 关于这几种常见的背包,其关系如下:
15 |
16 | 
17 |
18 | 通过这个图,可以很清晰分清这几种常见背包之间的关系。
19 |
20 | 在讲解背包问题的时候,我们都是按照如下五部来逐步分析,相信大家也体会到,把这五部都搞透了,算是对动规来理解深入了。
21 |
22 | 1. 确定dp数组(dp table)以及下标的含义
23 | 2. 确定递推公式
24 | 3. dp数组如何初始化
25 | 4. 确定遍历顺序
26 | 5. 举例推导dp数组
27 |
28 | **其实这五部里哪一步都很关键,但确定递推公式和确定遍历顺序都具有规律性和代表性,所以下面我从这两点来对背包问题做一做总结**。
29 |
30 | ## 背包递推公式
31 |
32 | 问能否能装满背包(或者最多装多少):dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]); ,对应题目如下:
33 | * [动态规划:416.分割等和子集](https://programmercarl.com/0416.分割等和子集.html)
34 | * [动态规划:1049.最后一块石头的重量 II](https://programmercarl.com/1049.最后一块石头的重量II.html)
35 |
36 | 问装满背包有几种方法:dp[j] += dp[j - nums[i]] ,对应题目如下:
37 | * [动态规划:494.目标和](https://programmercarl.com/0494.目标和.html)
38 | * [动态规划:518. 零钱兑换 II](https://programmercarl.com/0518.零钱兑换II.html)
39 | * [动态规划:377.组合总和Ⅳ](https://programmercarl.com/0377.组合总和Ⅳ.html)
40 | * [动态规划:70. 爬楼梯进阶版(完全背包)](https://programmercarl.com/0070.爬楼梯完全背包版本.html)
41 |
42 | 问背包装满最大价值:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); ,对应题目如下:
43 | * [动态规划:474.一和零](https://programmercarl.com/0474.一和零.html)
44 |
45 | 问装满背包所有物品的最小个数:dp[j] = min(dp[j - coins[i]] + 1, dp[j]); ,对应题目如下:
46 | * [动态规划:322.零钱兑换](https://programmercarl.com/0322.零钱兑换.html)
47 | * [动态规划:279.完全平方数](https://programmercarl.com/0279.完全平方数.html)
48 |
49 |
50 | ## 遍历顺序
51 |
52 | ### 01背包
53 |
54 | 在[动态规划:关于01背包问题,你该了解这些!](https://programmercarl.com/背包理论基础01背包-1.html)中我们讲解二维dp数组01背包先遍历物品还是先遍历背包都是可以的,且第二层for循环是从小到大遍历。
55 |
56 | 和[动态规划:关于01背包问题,你该了解这些!(滚动数组)](https://programmercarl.com/背包理论基础01背包-2.html)中,我们讲解一维dp数组01背包只能先遍历物品再遍历背包容量,且第二层for循环是从大到小遍历。
57 |
58 | **一维dp数组的背包在遍历顺序上和二维dp数组实现的01背包其实是有很大差异的,大家需要注意!**
59 |
60 | ### 完全背包
61 |
62 | 说完01背包,再看看完全背包。
63 |
64 | 在[动态规划:关于完全背包,你该了解这些!](https://programmercarl.com/背包问题理论基础完全背包.html)中,讲解了纯完全背包的一维dp数组实现,先遍历物品还是先遍历背包都是可以的,且第二层for循环是从小到大遍历。
65 |
66 | 但是仅仅是纯完全背包的遍历顺序是这样的,题目稍有变化,两个for循环的先后顺序就不一样了。
67 |
68 | **如果求组合数就是外层for循环遍历物品,内层for遍历背包**。
69 |
70 | **如果求排列数就是外层for遍历背包,内层for循环遍历物品**。
71 |
72 | 相关题目如下:
73 |
74 | * 求组合数:[动态规划:518.零钱兑换II](https://programmercarl.com/0518.零钱兑换II.html)
75 | * 求排列数:[动态规划:377. 组合总和 Ⅳ](https://mp.weixin.qq.com/s/Iixw0nahJWQgbqVNk8k6gA)、[动态规划:70. 爬楼梯进阶版(完全背包)](https://programmercarl.com/0070.爬楼梯完全背包版本.html)
76 |
77 | 如果求最小数,那么两层for循环的先后顺序就无所谓了,相关题目如下:
78 |
79 | * 求最小数:[动态规划:322. 零钱兑换](https://programmercarl.com/0322.零钱兑换.html)、[动态规划:279.完全平方数](https://programmercarl.com/0279.完全平方数.html)
80 |
81 |
82 | **对于背包问题,其实递推公式算是容易的,难是难在遍历顺序上,如果把遍历顺序搞透,才算是真正理解了**。
83 |
84 |
85 | ## 总结
86 |
87 |
88 | **这篇背包问题总结篇是对背包问题的高度概括,讲最关键的两部:递推公式和遍历顺序,结合力扣上的题目全都抽象出来了**。
89 |
90 | **而且每一个点,我都给出了对应的力扣题目**。
91 |
92 | 最后如果你想了解多重背包,可以看这篇[动态规划:关于多重背包,你该了解这些!](https://programmercarl.com/背包问题理论基础多重背包.html),力扣上还没有多重背包的题目,也不是面试考察的重点。
93 |
94 | 如果把我本篇总结出来的内容都掌握的话,可以说对背包问题理解的就很深刻了,用来对付面试中的背包问题绰绰有余!
95 |
96 | 背包问题总结:
97 |
98 | 
99 |
100 | 这个图是 [代码随想录知识星球](https://programmercarl.com/other/kstar.html) 成员:[海螺人](https://wx.zsxq.com/dweb2/index/footprint/844412858822412),所画结的非常好,分享给大家。
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
--------------------------------------------------------------------------------
/problems/贪心算法理论基础.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!
6 |
7 |
8 | # 关于贪心算法,你该了解这些!
9 |
10 | 题目分类大纲如下:
11 |
12 |
13 |
14 | ## 算法公开课
15 |
16 | **[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[贪心算法理论基础!](https://www.bilibili.com/video/BV1WK4y1R71x/),相信结合视频再看本篇题解,更有助于大家对本题的理解**。
17 |
18 | ## 什么是贪心
19 |
20 | **贪心的本质是选择每一阶段的局部最优,从而达到全局最优**。
21 |
22 | 这么说有点抽象,来举一个例子:
23 |
24 | 例如,有一堆钞票,你可以拿走十张,如果想达到最大的金额,你要怎么拿?
25 |
26 | 指定每次拿最大的,最终结果就是拿走最大数额的钱。
27 |
28 | 每次拿最大的就是局部最优,最后拿走最大数额的钱就是推出全局最优。
29 |
30 | 再举一个例子如果是 有一堆盒子,你有一个背包体积为n,如何把背包尽可能装满,如果还每次选最大的盒子,就不行了。这时候就需要动态规划。动态规划的问题在下一个系列会详细讲解。
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 | 举一个不太恰当的例子:我要用一下1+1 = 2,但我要先证明1+1 为什么等于2。严谨是严谨了,但没必要。
61 |
62 | 虽然这个例子很极端,但可以表达这么个意思:**刷题或者面试的时候,手动模拟一下感觉可以局部最优推出整体最优,而且想不到反例,那么就试一试贪心**。
63 |
64 | **例如刚刚举的拿钞票的例子,就是模拟一下每次拿做大的,最后就能拿到最多的钱,这还要数学证明的话,其实就不在算法面试的范围内了,可以看看专业的数学书籍!**
65 |
66 | 所以这也是为什么很多同学通过(accept)了贪心的题目,但都不知道自己用了贪心算法,**因为贪心有时候就是常识性的推导,所以会认为本应该就这么做!**
67 |
68 | **那么刷题的时候什么时候真的需要数学推导呢?**
69 |
70 | 例如这道题目:[链表:环找到了,那入口呢?](https://programmercarl.com/0142.环形链表II.html),这道题不用数学推导一下,就找不出环的起始位置,想试一下就不知道怎么试,这种题目确实需要数学简单推导一下。
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 |
--------------------------------------------------------------------------------
/problems/链表总结篇.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!
6 |
7 | # 链表总结篇
8 |
9 |
10 | ## 链表的理论基础
11 |
12 | 在这篇文章[关于链表,你该了解这些!](https://programmercarl.com/链表理论基础.html)中,介绍了如下几点:
13 |
14 | * 链表的种类主要为:单链表,双链表,循环链表
15 | * 链表的存储方式:链表的节点在内存中是分散存储的,通过指针连在一起。
16 | * 链表是如何进行增删改查的。
17 | * 数组和链表在不同场景下的性能分析。
18 |
19 | **可以说把链表基础的知识都概括了,但又不像教科书那样的繁琐**。
20 |
21 | ## 链表经典题目
22 |
23 | ### 虚拟头结点
24 |
25 | 在[链表:听说用虚拟头节点会方便很多?](https://programmercarl.com/0203.移除链表元素.html)中,我们讲解了链表操作中一个非常重要的技巧:虚拟头节点。
26 |
27 | 链表的一大问题就是操作当前节点必须要找前一个节点才能操作。这就造成了,头结点的尴尬,因为头结点没有前一个节点了。
28 |
29 | **每次对应头结点的情况都要单独处理,所以使用虚拟头结点的技巧,就可以解决这个问题**。
30 |
31 | 在[链表:听说用虚拟头节点会方便很多?](https://programmercarl.com/0203.移除链表元素.html)中,我给出了用虚拟头结点和没用虚拟头结点的代码,大家对比一下就会发现,使用虚拟头结点的好处。
32 |
33 | ### 链表的基本操作
34 |
35 | 在[链表:一道题目考察了常见的五个操作!](https://programmercarl.com/0707.设计链表.html)中,我们通过设计链表把链表常见的五个操作练习了一遍。
36 |
37 | 这是练习链表基础操作的非常好的一道题目,考察了:
38 |
39 | * 获取链表第index个节点的数值
40 | * 在链表的最前面插入一个节点
41 | * 在链表的最后面插入一个节点
42 | * 在链表第index个节点前面插入一个节点
43 | * 删除链表的第index个节点的数值
44 |
45 | **可以说把这道题目做了,链表基本操作就OK了,再也不用担心链表增删改查整不明白了**。
46 |
47 | 这里我依然使用了虚拟头结点的技巧,大家复习的时候,可以去看一下代码。
48 |
49 | ### 反转链表
50 |
51 | 在[链表:听说过两天反转链表又写不出来了?](https://programmercarl.com/0206.翻转链表.html)中,讲解了如何反转链表。
52 |
53 | 因为反转链表的代码相对简单,有的同学可能直接背下来了,但一写还是容易出问题。
54 |
55 | 反转链表是面试中高频题目,很考察面试者对链表操作的熟练程度。
56 |
57 | 我在[文章](https://programmercarl.com/0206.翻转链表.html)中,给出了两种反转的方式,迭代法和递归法。
58 |
59 | 建议大家先学透迭代法,然后再看递归法,因为递归法比较绕,如果迭代还写不明白,递归基本也写不明白了。
60 |
61 | **可以先通过迭代法,彻底弄清楚链表反转的过程!**
62 |
63 | ### 删除倒数第N个节点
64 |
65 | 在[链表:删除链表倒数第N个节点,怎么删?](https://programmercarl.com/0019.删除链表的倒数第N个节点.html)中我们结合虚拟头结点 和 双指针法来移除链表倒数第N个节点。
66 |
67 |
68 | ### 链表相交
69 |
70 | [链表:链表相交](https://programmercarl.com/面试题02.07.链表相交.html)使用双指针来找到两个链表的交点(引用完全相同,即:内存地址完全相同的交点)
71 |
72 | ### 环形链表
73 |
74 | 在[链表:环找到了,那入口呢?](https://programmercarl.com/0142.环形链表II.html)中,讲解了在链表如何找环,以及如何找环的入口位置。
75 |
76 | 这道题目可以说是链表的比较难的题目了。 但代码却十分简洁,主要在于一些数学证明。
77 |
78 | ## 总结
79 |
80 | 
81 |
82 | 这个图是 [代码随想录知识星球](https://programmercarl.com/other/kstar.html) 成员:[海螺人](https://wx.zsxq.com/dweb2/index/footprint/844412858822412),所画,总结的非常好,分享给大家。
83 |
84 | 考察链表的操作其实就是考察指针的操作,是面试中的常见类型。
85 |
86 | 链表篇中开头介绍[链表理论知识](https://programmercarl.com/0203.移除链表元素.html),然后分别通过经典题目介绍了如下知识点:
87 |
88 | 1. [关于链表,你该了解这些!](https://programmercarl.com/链表理论基础.html)
89 | 2. [虚拟头结点的技巧](https://programmercarl.com/0203.移除链表元素.html)
90 | 3. [链表的增删改查](https://programmercarl.com/0707.设计链表.html)
91 | 4. [反转一个链表](https://programmercarl.com/0206.翻转链表.html)
92 | 5. [删除倒数第N个节点](https://programmercarl.com/0019.删除链表的倒数第N个节点.html)
93 | 6. [链表相交](https://programmercarl.com/面试题02.07.链表相交.html)
94 | 7. [有否环形,以及环的入口](https://programmercarl.com/0142.环形链表II.html)
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
--------------------------------------------------------------------------------