赞
踩
快速排序是一种分治算法,它通过从数组中选择一个**“枢轴”元素并根据它们是否小于或大于枢轴将其他元素分成两个子数组**。然后递归地对子数组进行排序。快速排序算法的平均时间复杂度为 O ( n l o g n ) O(n log n) O(nlogn) ,最坏时间复杂度为 O ( n 2 ) O(n^2) O(n2)。
分界点即为主要思想中提到的“枢纽”元素,一般选择中点对应的数为分界点。
使用两个指针 i ,j分别指向数组的左右两端,然后向中间靠拢。如果指针对应的数小于分界点,则移到左部,反之移到右部。当两个指针相遇的时候,则停止移动指针,开始对左右两部分继续前序操作。当一开始两个指针的位置相同时,则表明这部分只有一个数,就返回。
void quick_sort(int q[], int l , int r) // 快速排序
{
if(l >= r) return;
int x = q[(l + r) / 2], i = l - 1, j = r + 1; // 先移动 再判断
while(i < j){
while(q[++i] < x); // 先移动 保证第一个/最后一个数能被考察
while(q[--j] > x);
if(i < j) swap(q[i],q[j]);
}
quick_sort(q,l,j);
quick_sort(q,j + 1, r);
}
运用快速排序算法,但不对整个数组进行排序,仅排序第k个小的数所在的数组。因为快速排序算法将数组分为较小和较大的两部分,所以仅需比较分开位置与k的大小,就能确定下次排序的数组。其时间复杂度为 O ( n ) O(n) O(n) 。
首先利用快速排序算法将数组分为两部分,计算左侧部分的元素数量(sl),并将其与k进行比较。如果k小于或等于sl,则意味着第k小的元素在左侧部分,因此它会递归该部分。否则,它意味着第k小的元素在右侧部分,因此它会递归该部分,并通过减去sl来调整k。当它在子数组中找到第k小的元素时,该函数返回。
int quick_select(int l , int r, int k) // 快速选择 { if(l >= r) return q[l]; int x = q[(l + r) / 2], i = l - 1, j = r + 1; while(i < j) { while(q[++i] < x); while(q[--j] > x); if(i < j) swap(q[i], q[j]); } int sl = j - l + 1; // 左边元素个数 if(k <= sl) return quick_select(l, j, k); else return quick_select(j + 1, r, k - sl); }
采用分治法,将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。其时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)。该算法为稳定的算法,也就是说值相同的元素不会被交换位置。
首先将序列分成左右两部分,然后再将各部分继续分解,直到序列只有一个数,然后开始向上合并。合并两序列时,使用双指针,再使用一个临时数组存储当前合并的序列,当某指针指向的值更小时,则将其放入临时数组中,指针向后移动,重复指到两个指针均指向子序列尾部。
void merge_sort(int q[], int l, int r) // 归并排序 { if(l >= r) return; int mid = l + r >> 1; merge_sort(q, l, mid); // 分成左右两部分 merge_sort(q, mid + 1, r); int i = l, j = mid + 1, k = 0; while(i <= mid && j <= r) { if(q[i] <= q[j]) tmp[k ++] = q[i ++]; else tmp[k ++] = q[j ++]; } while(i <= mid) tmp[k ++] = q[i ++]; while(j <= r) tmp[k ++] = q[j ++]; for(i = l, j = 0; i <= r; i++, j++) q[i] = tmp[j]; }
归并排序归并阶段是将两个有序的子序列合并成一个完整序列。也就是说,当在左部序列中找到比右部序列当前指向的值大的数之后,左部序列之后所有的元素都会与之形成逆序对。所以只需在归并阶段加上对逆序对数量的增加,即可算出总的逆序对数量。
分为三种情况,假设分为 Left 和 Right 两部分,左右两边指针分别为 i 和 j。
mid-i+1
。LL merge_sort(int q[], int l, int r) { if(l >= r) return 0; int mid = l + r >> 1; LL res = merge_sort(q, l, mid) + merge_sort(q, mid + 1, r); // 逆序对在左或右 int k = 0, i = l, j = mid + 1; while(i <= mid && j <= r) // 逆序对两元素分别在不同侧 { if(q[i] <= q[j]) tmp[k ++] = q[i ++]; else { tmp[k ++] = q[j ++]; res += mid - i + 1; } } while(i <= mid) tmp[k ++] = q[i ++]; while(j <= r) tmp[k ++] = q[j ++]; for(i = l, j = 0; i <= r; i++ , j++) q[i] = tmp[j]; return res; }
通过二分分别查找左边界和右边界,但左右边界的临界情况不同,需要分开讨论。
#include <iostream> using namespace std; const int N = 100010; int n, m; int q[N]; int main() { cin >> n >> m; for (int i = 0; i < n; i ++ ) cin >> q[i]; while (m -- ) { int x; cin >> x; int l = 0, r = n - 1; // 寻找左边界 模板1 while(l < r) { int mid = l + r >> 1; if(q[mid] >= x) r = mid; else l = mid + 1; } // 如果连左边界都没有查找到 if(q[l] != x) cout << "-1 -1" << endl; // 寻找右边界 模板2 注意求中点+1是防止进入死循环 else { cout << l << " "; l = 0, r = n - 1; while(l < r) { int mid = l + r + 1 >> 1; if(q[mid] <= x) l = mid; else r = mid - 1; } cout << l << endl; } } return 0; }
对高精度的数使用两个数组存储每一位的值,再在每一位上分别进行运算。
vector<int> add(vector<int> &A, vector<int> &B) { vector<int> C; int t = 0; // 进位 for(int i = 0; i < A.size() || i < B.size(); i++) { if(i < A.size()) t += A[i]; if(i < B.size()) t += B[i];; C.push_back(t % 10); t /= 10; } if(t) C.push_back(1); return C; }
减法时需要注意借位情况,可以设置一个t表示借位。每次进行运算时先减去这个借位,然后再进行减法运算。减法运算完后,将t进行加10再取模就能保证为非负数。最后比较一下t是否为负数,如果为负数就表明需要借位,将t设置为1,方便下次运算时先进行-1。需要注意的是,在进行运算前需要对两数进行大小判断,如果是小-大就要额外加上负号。
// 已经比较过大小 A为更大的数 vector<int> sub(vector<int> &A, vector<int> &B) { vector<int> C; int t = 0; // 进位 for(int i = 0; i < A.size(); i++) { t = A[i] - t; // 减去借位 if(i < B.size()) t -= B[i]; C.push_back((t + 10) % 10); if(t < 0) t = 1; // 需要借位 else t = 0; } // 消去前导0 如“002” while(C.size() > 1 && C.back() == 0) C.pop_back(); return C; }
vector<int> mul(vector<int> &A, int B) { vector<int> C; int t = 0; for(int i = 0; i < A.size(); i++) { t = A[i] * B + t; C.push_back(t % 10); t /= 10; } if(t != 0) C.push_back(t); // 消去前导0 while(C.size() > 1 && C.back() == 0) C.pop_back(); return C; }
进行除法时,模拟人的运算方法,从高位开始。如果该数小于除数,那么将该数(可以看成余数)乘10,然后和之后的数相加,再进行相除。
vector<int> div(vector<int> &A, int B, int &r) { vector<int> C; r = 0; for(int i = A.size() - 1; i >= 0; i--) { r = r * 10 + A[i]; C.push_back(r / B); // 不够除C会在后面加上0 r = r % B; } // 因为从高位开始,并且输出时数组相反,所以需要翻转 reverse(C.begin(), C.end()); // 消去前导0 while(C.size() > 1 && C.back() == 0) C.pop_back(); return C; }
令第i个数 a i a_i ai的前缀和为 S i S_i Si,那么 S i = a 1 + a 2 + . . . + a i S_i = a_1 + a_2 + ... + a_i Si=a1+a2+...+ai 。
利用前缀和可以快速求出一个区间的和。例如求 [ l , r ] [l,r] [l,r]区间的和,那么 S l , r = S r − S l − 1 S_{l,r} = S_r - S_{l-1} Sl,r=Sr−Sl−1。
#include <iostream> #include <cstring> #include <algorithm> using namespace std; const int N = 1e5 + 10; int n,m; int q[N],s[N]; int main() { cin >> n >> m; q[0] = 0, s[0] = 0; for (int i = 1; i <= n; i ++ ) { cin >> q[i]; s[i] = s[i - 1] + q[i]; } while (m -- ) { int l,r; cin >> l >> r; cout << s[r] - s[l-1] << endl; } return 0; }
二维数组的前缀和如下图所示, ( i , j ) (i,j) (i,j)点的前缀和为橙色部分。
若要求 S i , j S_{i,j} Si,j,黄色部分已知,那么 S i , j = S i − 1 , j + S i , j − 1 − S i − 1 , j − 1 + a i , j S_{i,j} = S_{i-1,j} + S_{i,j-1} - S_{i-1,j-1} + a_{i,j} Si,j=Si−1,j+Si,j−1−Si−1,j−1+ai,j 。
#include <iostream> #include <cstring> #include <algorithm> using namespace std; const int N = 1010; int n, m, q; int a[N][N], s[N][N]; int main() { cin >> n >> m >> q; for(int i = 1; i <= n; i++) for(int j = 1; j <= m; j++) { cin >> a[i][j]; s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + a[i][j]; } while(q --) { int x1,y1,x2,y2; cin >> x1 >> y1 >> x2 >> y2; cout << s[x2][y2] - s[x1 - 1][y2] - s[x2][y1 - 1] + s[x1 - 1][y1 - 1] << endl; } return 0; }
差分相当于前缀和的逆运算。令数组 a , b a,b a,b ,若 b b b 是 a a a 的差分数组,则满足 a i = b 1 + b 2 + . . . + b i a_i = b_1 + b_2 + ... +b_i ai=b1+b2+...+bi 。
假设在 a a a 的 [ l , r ] [l,r] [l,r] 区间内,将每个数都加上c。那么只需要进行 b l + c b_l +c bl+c 与 b r + 1 − c b_{r+1} - c br+1−c 两个操作,前者保证 l l l 之后的每个 a i a_i ai 都加上c,后者保证 r r r 之后的每个 a i a_i ai 都保持不变。
#include <iostream> #include <cstring> #include <algorithm> using namespace std; const int N = 1e5 + 10; int n,m; int a[N],b[N]; int main() { cin >> n >> m; for(int i = 1; i <= n; i++) { cin >> a[i]; b[i] = a[i] - a[i - 1]; } while (m -- ) { int l, r, c; cin >> l >> r >> c; b[l] += c; b[r + 1] -= c; } for(int i = 1 ; i <= n; i++) a[i] = a[i - 1] + b[i]; for(int i = 1 ; i <= n; i++) cout << a[i] << " "; return 0; }
同二维前缀和,二维差分中 a a a 数组为 b b b 数组的前缀和。
a , b a,b a,b 数组的转换同前缀和理,主要讨论对区间的操作。令两个元素坐标分别为 ( x 1 , y 1 ) (x_1,y_1) (x1,y1) 和 ( x 2 , y 2 ) (x_2,y_2) (x2,y2) ,将以这两元素形成的矩阵中,每个元素加上c。那么可以对 b b b 数组进行操作。假设为图中黄色填充部分为需要操作的部分,那么对 ( x 1 , y 1 ) (x1,y1) (x1,y1) 加上c,会导致红框部分都会加上,那么需要减去蓝色线条与绿色线条部分,并加上多减去的相交部分。
#include <iostream> #include <cstring> #include <algorithm> using namespace std; const int N = 1010; int n, m, q; int a[N][N], b[N][N]; int main() { cin >> n >> m >> q; for (int i = 1; i <= n; i++) for (int j = 1; j <= m; j++) { cin >> a[i][j]; b[i][j] = a[i][j] - a[i-1][j] - a[i][j-1] + a[i-1][j-1]; } while(q --) { int x1,y1,x2,y2,c; cin >> x1 >> y1 >> x2 >> y2 >> c; b[x1][y1] += c; b[x1][y2+1] -= c; b[x2+1][y1] -= c; b[x2+1][y2+1] += c; } for (int i = 1; i <= n; i++) { for (int j = 1; j <= m; j++) { a[i][j] = a[i-1][j] + a[i][j-1] - a[i-1][j-1] + b[i][j]; cout << a[i][j] << " "; } cout << endl; } return 0; }
额外使用一个数组记录数出现的次数,如果在这个连续子序列里出现了一次以上,就说明需要移动左端指针,调整子序列。在移动左指针的同时对子序列中数的出现次数也需要进行调整,当这个子序列中没有重复的数后,再次移动右端指针。
#include <iostream> #include <cstring> #include <algorithm> using namespace std; const int N = 1e5 + 10; int n; int q[N],r[N]; int main() { cin >> n; for (int i = 0; i < n; i++) cin >> q[i]; int res = 0; for (int i = 0, j = 0; i < n; i++) { r[q[i]]++; while (r[q[i]] > 1) r[q[j++]]--; res = max(res, i - j + 1); } cout << res << endl; return 0; }
暴力搜索可以得出两个元素都有单调性,即可降维使用双指针。因为两个数组都是单增序列,所以一个数组指向前端,一个指向后端。当两数和大于目标值时,移动指向后端的指针。否则移动前端指针。
#include <iostream> #include <cstring> #include <algorithm> using namespace std; const int N = 1e5 + 10; int n, m, x; int a[N], b[N]; int main() { cin >> n >> m >> x; for (int i = 0; i < n; i++) cin >> a[i]; for (int i = 0; i < m; i++) cin >> b[i]; for (int i = 0, j = m - 1; i < n; i++) { while(j >= 0 && a[i] + b[j] > x) j --; if(a[i] + b[j] == x) { cout << i << " " << j; break; } } return 0; }
离散化是指将间隔很大的点,映射到相邻的数组元素中。减少对空间的需求,也减少计算量。如同区间和一题(原题链接),数组范围太大,而需要用到的数据范围很小,就使用离散化存储在一小段数组中。
在这道题中存储的是需要用到的点的坐标,包括需要增加的点和查询的点。因为可能有多个相同的点,所以需要将其进行去重,并排序。然后再使用前缀和的方式求出查询的和。
#include <iostream> #include <vector> #include <algorithm> using namespace std; typedef pair<int, int> PII; // 增加需要10w,查询需要2*10w,共30w const int N = 300010; int n, m; int a[N], s[N]; vector<int> alls; // 存储离散化后的坐标 vector<PII> add, query; // 使用一对存储增加和查询的操作 int find(int x) { int l = 0, r = alls.size() - 1; while (l < r) { int mid = l + r >> 1; if (alls[mid] >= x) r = mid; else l = mid + 1; } return r + 1; // 前缀和下标从1开始 } int main() { cin >> n >> m; for (int i = 0; i < n; i ++) { int x, c; cin >> x >> c; add.push_back({x, c}); alls.push_back(x); } for (int i = 0; i < m; i ++) { int l, r; cin >> l >> r; query.push_back({l, r}); alls.push_back(l); alls.push_back(r); } // 排序加去重 sort(alls.begin(), alls.end()); alls.erase(unique(alls.begin(), alls.end()), alls.end()); // 对相应坐标进行增加 for (auto item : add) { int x = find(item.first); a[x] += item.second; } // 求前缀和操作 for (int i = 1; i <= alls.size(); i ++) s[i] = s[i - 1] + a[i]; for (auto item : query) { int l = find(item.first); int r = find(item.second); cout << s[r] - s[l - 1] << endl; } return 0; }
将所有区间按照从小到大排序,从最小的区间开始逐一遍历。定义一个 start 和 end,如果较大区间的开始在 start - end 之内,就更新区间,反之将前一个区间加入到合并完成的数组中,从当前区间继续遍历。
#include <iostream> #include <cstring> #include <algorithm> using namespace std; typedef pair<int, int> PII; const int N = 100010; int n; vector<PII> segs; // 存储区间 vector<PII> merge(vector<PII> &segs) { vector<PII> res; sort(segs.begin(), segs.end()); // 先将端点设置极限值 int st = -2e9, ed = -2e9; for (auto item : segs) if (ed < item.first) { if (ed != -2e9) res.push_back({st, ed}); st = item.first, ed = item.second; } else ed = max(ed, item.second); // 更新区间end if (st != -2e9) res.push_back({st, ed}); // 遍历完成后还需要加入一次 segs = res; return segs; } int main() { cin >> n; for (int i = 0; i < n; i ++) { int l, r; cin >> l >> r; segs.push_back({l, r}); } merge(segs); cout << segs.size() << endl; return 0; }
使用数组模拟链表,会比指针更快,因为 C++ 的 new 方法效率太低,但不太能考虑内存的情况。
head 变量存储头节点的下标,idx 变量存储目前添加到的下标,e[] 数组存储链表的值,ne[] 数组存储当前下标对应的节点指向的节点下标。
// 头插法 void add_to_head(int x) { e[idx] = x; ne[idx] = head; head = idx; idx ++; } // 删除下标为 k 的节点指向的节点 void remove(int k) { ne[k] = ne[ne[k]]; } // 在下标为 k 的节点之后插入值为 x 的节点 void add(int k, int x) { e[idx] = x; ne[idx] = ne[k]; ne[k] = idx; idx ++; }
l[] 数组存储当前节点左侧的节点下标,r[] 数组存储当前节点右侧的节点下标。
void init() { // 0下标为head,1下标为tail r[0] = 1; l[1] = 0; idx = 2; } // 在第 k 个插入的数右边插入 // 链表左侧插入时 k = 0 // 链表右侧插入时 k = l[1] // 如果表示在第 k 个插入的数左边插入 那么 k = l[k] void add(int k, int x) { e[idx] = x; l[idx] = k; r[idx] = r[k]; l[r[k]] = idx; r[k] = idx; idx ++; } void remove(int k) { r[l[k]] = r[k]; l[r[k]] = l[k]; }
使用数组模拟栈,如果起始下标为0,那么栈顶指针就要从-1开始。
**栈空:**栈顶指针小于0。
**查询:**输出栈顶指针所指的值。
push:栈顶指针先+1,再赋当前位置的值。
**pop:**栈顶指针-1。
int tt = -1; // 栈顶下标 int stk[N]; void push(int x) { stk[++ tt] = x; } void pop() { tt --; } void query() { cout << stk[tt] << endl; } void empty() { if (tt < 0) cout << "YES" << endl; else cout << "NO" << endl; }
使用一个运算符栈和一个数字栈。扫描表达式,是数字就入栈。是运算符:左括号直接入栈;是右括号就进行计算,直到运算符栈的栈顶为左括号;是加减乘除就进行判断,如果当前运算符优先级<=栈顶运算符优先级,则先计算,直到当前运算符优先级更高,否则直接入栈。
#include <iostream> #include <cstring> #include <stack> #include <unordered_map> #include <algorithm> using namespace std; const int N = 10010; stack<int> num; stack<char> op; void eval() { // 靠后的数会先出栈 int b = num.top(); num.pop(); int a = num.top(); num.pop(); char opt = op.top(); op.pop(); int res = 0; if(opt == '+') res = a + b; if(opt == '-') res = a - b; if(opt == '*') res = a * b; if(opt == '/') res = a / b; num.push(res); } int main() { unordered_map<char, int> pr{{'+', 1}, {'-', 1}, {'*', 2}, {'/', 2}}; string str; cin >> str; for (int i = 0; i < str.size(); i ++) { char c = str[i]; if (isdigit(c)) { int x = 0, j = i; while (j < str.size() && isdigit(str[j])) x = x * 10 + str[j ++] - '0'; num.push(x); i = j - 1; } // 左括号无优先级 直接入栈 else if (c == '(') op.push(c); // 有括号直接开始计算 直到出现左括号 else if (c == ')') { while (op.top() != '(') eval(); op.pop(); } // 其他运算符 如果栈顶优先级大于此时 则运算 // 如果栈顶优先级小于此时 直接入栈 else { while (!op.empty() && pr[op.top()] >= pr[c]) eval(); op.push(c); } } // 如果栈中还有 则计算 while (!op.empty()) eval(); cout << num.top() << endl; // 结果存储在栈顶 return 0; }
使用数组模拟队列。需要一个队首指针fr和队尾指针bc。
**队列空:**fr = bc。
**查询:**输出fr指向的值。
**push:**先赋bc指向的值,再将bc+1。
**pop:**fr+1。
int q[N]; int fr = 0,bc = 0; // fr 队首,bc 队尾 void push(int x) { q[bc ++] = x; } void pop() { fr ++; } void query() { cout << q[fr] << endl; } void empty() { if (fr == bc) cout << "YES" << endl; else cout << "NO" <<endl; }
找到数组每个元素左侧离它最近的比它小的元素。组成一个单调栈,如果都没有比其小的,说明之后所有元素都不可能找到在其之前的元素位置了。
#include <iostream> #include <cstring> #include <algorithm> using namespace std; const int N = 10010; int stk[N]; int n,tt = 0; int main() { cin >> n; for (int i = 0; i < n; i ++) { int x; cin >> x; // 如果栈顶元素更大 则pop while (tt && stk[tt] >= x) tt --; // 如果栈不空 则找到比当前元素小的 输出当前栈顶元素 if (tt) cout << stk[tt] << " "; // 如果栈空 则左侧均比他大 输出-1 else cout << -1 << " "; // 当前元素入栈 stk[++ tt] = x; } return 0; }
滑动窗口使用单调队列构建递增的序列。因为存在先进先出的性质,所以队首总是指向最小/大的值。每次循环(一次滑动)需要解决四件事:
#include <iostream> #include <cstring> #include <algorithm> using namespace std; const int N = 1000010; int a[N], q[N]; int n, k; int main() { cin >> n >> k; for (int i = 0; i < n; i ++) cin >> a[i]; int hh = 0, tt = -1; for (int i = 0; i < n; i ++) // 输出窗口内最小值 { if (i - k + 1 > q[hh]) hh ++; // 队首已出窗口 while (hh <= tt && a[i] <= a[q[tt]]) tt --; // 当前元素小于等于队尾元素 q[++ tt] = i; // 将当前元素坐标加入队列 if(i + 1 >= k) cout << a[q[hh]] << " "; } cout << endl; return 0; }
主要思想是,找出串中相同的两段,在后续字符匹配不上时,直接将串移至前序相同串的末尾,这样可以减少循环次数。
next数组:1-j这段区间的串中,前缀和后缀相同的最大长度。例如abcab这组串中,当指针指向最后的5时,前缀为:{a,ab,abc,abca},后缀为:{b,ab,cab,bcab},故 next[5] = 2。
#include <iostream> #include <cstring> #include <algorithm> using namespace std; const int N = 100010, M = 1000010; int n, m; char p[N], s[M]; int ne[N]; int main() { cin >> n; cin >> p + 1; // 从下标1开始 // 构造next数组 for (int i = 2, j = 0; i <= n; i ++) { while (j && p[i] != p[j + 1]) j = ne[j]; if (p[i] == p[j + 1]) j ++; ne[i] = j; } cin >> m; cin >> s + 1; // 从下标1开始 // 进行字符串匹配 for (int i = 1, j = 0; i <= m; i ++) { // 当后续匹配不上时,直接转向前序相同序列的 while (j && s[i] != p[j + 1]) j = ne[j]; if (s[i] == p[j + 1]) j ++; if (j == n) // 匹配完成 { cout << i - n << " "; j = ne[j]; } } return 0; }
主要思想是利用树,将仅含一种数据类型的字符串进行存储(例如全为小写字母,全为数字)。
存储:利用树对每个字符进行存储,每个字符串完成后,对最后一个字符进行标识(即存储以它结尾的字符串的个数)。
匹配:逐一字符进行查找,如果当前字符没有的话直接返回 0 。如果全部匹配完成后,返回标识数据。
void insert(char str[]) { int p = 0; for (int i = 0; str[i]; i ++) { int u = str[i] - 'a'; if (!son[p][u]) son[p][u] = ++ idx; // 如果当前位置没有存储东西 p = son[p][u]; } cnt[p] ++; // 对字符串末尾进行标识 } int query(char str[]) { int p = 0; for (int i = 0; str[i]; i ++) { int u = str[i] - 'a'; if (!son[p][u]) return 0; p = son[p][u]; } return cnt[p]; }
原题链接
将数字以二进制形式从高位开始存储,整型二进制最多31位,故从 i=30 开始递减,将每位二进制放进Trie树中。从高位开始存储利于找到最大值。查找时,将给定的从高位开始,向高位取反的方向进行查找。
void insert(int x) { int p = 0; for (int i = 30; i >= 0; i --) { int u = x >> i & 1; // 获取第i位的二进制 if (!son[p][u]) son[p][u] = ++ idx; p = son[p][u]; } } int query(int x) { int p = 0, res = 0; for (int i = 30; i >= 0; i --) { int u = x >> i & 1; if (son[p][!u]) // 最优情况向取反方向走 { p = son[p][!u]; res = res * 2 + !u; } else // 否则往另一方向走 { p = son[p][u]; res = res * 2 + u; } } return res; }
**解决:**将两个集合合并;询问两个元素是否在一个集合中
基本原理:将每个集合用一棵树来表示。树根的编号就是整个集合的编号。每个节点存储它的父节点,p[x]表示x的父节点。
问题:
堆本质是一棵完全二叉树(除叶结点外,其余结点均非空,叶结点从左至右排列)。以小根堆为例,每个结点均小于等于其左右儿子,故根节点为最小值。
存储:使用一维数组存储,从下标 1 1 1 开始。每个结点的左儿子下标为 2 x 2x 2x,右儿子下标为 2 x + 1 2x + 1 2x+1。
操作:
解决问题:
思路是每次输出数组首部,然后删除。循环规定的m次。
#include <iostream> #include <algorithm> using namespace std; const int N = 100010; int n, m; int h[N], size1; void down(int x) { int t = x; if (x * 2 <= size1 && h[x * 2] < h[t]) t = x * 2; // 判断左儿子 if (x * 2 + 1 <= size1 && h[x * 2 + 1] < h[t]) t = x * 2 + 1; // 判断右儿子 if (t != x) // 不是down的下标 则交换 { swap(h[x], h[t]); down(t); } } int main() { cin >> n >> m; for (int i = 1; i <= n; i ++) cin >> h[i]; size1 = n; for (int i = n/2; i; i --) down(i); while (m --) { cout << h[1] << " "; h[1] = h[size1]; size1 --; down(1); } return 0; }
题目需要的求的是第 k k k 个插入的数,所以添加两个指针数组hp,ph。hp为h指向p,ph为p指向h。ph[k]表示第 k k k 个插入元素目前在堆中的下标。hp[k]表示堆中下标为 k k k 的元素,是第几个插入的元素,主要用来定位ph数组的位置。
#include <iostream> #include <cstring> #include <algorithm> using namespace std; const int N = 100010; int n, m; // m表示第m个插入的数 int h[N], ph[N], hp[N], sz; void heapswap(int a, int b) { swap(ph[hp[a]], ph[hp[b]]); swap(hp[a], hp[b]); swap(h[a], h[b]); } void up(int x) { while (x/2 && h[x/2] > h[x]) { heapswap(x/2, x); x >>= 1; } } void down(int x) { int t = x; if (x * 2 <= sz && h[x * 2] < h[t]) t = x * 2; if (x * 2 + 1 <= sz && h[x * 2 + 1] < h[t]) t = x * 2 + 1; if (t != x) { heapswap(t, x); down(t); } } int main() { cin >> n; m = 0; while (n --) { string op; int k, x; cin >> op; if (op == "I") { cin >> x; sz ++; m ++; h[sz] = x; ph[m] = sz, hp[sz] = m; up(sz); } else if (op == "PM") cout << h[1] << endl; else if (op == "DM") { heapswap(1, sz); // 需要将指针也进行交换,否则会指向错误 sz --; down(1); } else if (op == "D") { cin >> k; k = ph[k]; heapswap(sz, k); sz --; up(k); down(k); } else { cin >> k >> x; k = ph[k]; h[k] = x; up(k); down(k); } } return 0; }
根-左-右,相当于沿着树的外围跑一圈,最后回到根节点。
左-根-右,相当于垂直投影,即可得到答案。
左-右-根,相当于葡萄剪枝,一颗颗剪下则需先剪叶子结点。
相当于广度优先搜索,把一层从左到右依次遍历完成后,再转到下一层。
给先序遍历,输出中序遍历。
通过题目输入的先序遍历,可得到二叉树。先序遍历依照根-左-右进行,那么每个非空节点都会作为一个根节点,下一个节点即为其左子树的根,直到左子树考察完毕后,转到右子树,即可得到如下二叉树。
运用结构体,使用指针方式存储根节点,注意此题使用先序遍历创建树。
#include <bits/stdc++.h> using namespace std; string str; int k = 0, len; typedef struct node { char data; struct node *lchild; struct node *rchild; } BTNode, *tree; // 定义结构体 tree root = NULL; // 根节点 void create_Tree(tree &BitTree) { if (k >= len) return; char r = str[k++]; if (r == '#') BitTree = NULL; else { BitTree = new node; // 创建根节点 分配空间 BitTree->data = r; create_Tree(BitTree->lchild); // 首先创建左子树 create_Tree(BitTree->rchild); } } void LDR(tree BitTree) { if (BitTree == NULL) return; LDR(BitTree->lchild); cout << BitTree->data << " "; LDR(BitTree->rchild); } int main() { cin >> str; len = str.length(); create_Tree(root); LDR(root); return 0; }
直接使用DFS,如果是空,则返回,输出其父节点,再考察其右子树。相当于针对每个节点都进行左-根-右的操作。
#include <bits/stdc++.h>
using namespace std;
void dfs(){
char r = getchar();
if(r == '#') return; // 空的直接返回
dfs(); // 遍历左子树
cout << r << " "; // 中序遍历
dfs(); // 遍历右子树
}
int main()
{
dfs();
return 0;
}
这两个问题的区别为:01背包每个背包只能使用一次,完全背包每个背包可以使用无数次。
两个问题都可以确定的一个规则是:该背包选或不选。再确定是否可以多次选择。
那么使用动态规划可以进行求解,他们分别的状态转换方程则为:
01背包:dp[i][j] = max(dp[i-1][j], dp[i-1][j-v[i]] + w[i])
完全背包:dp[i][j] = max(dp[i-1][j], dp[i][j-v[i]] + w[i])
01背包与完全背包的前一项均相同,代表着不选择该背包。后一项唯一区别是选择之后是否忽略该背包,01背包中减一操作代表着,当前考察的背包不再选择,而完全背包则不需要进行该操作。
#include <bits/stdc++.h> using namespace std; int n, v; int main() { cin >> n >> v; int size[n + 1], worth[n + 1]; for (int i = 1; i <= n; i++) cin >> size[i] >> worth[i]; int dp[n + 1][v + 1]; memset(dp, 0, sizeof dp); for (int i = 1; i <= n; i++) { for (int j = 0; j <= v; j++) { if (j >= size[i]) dp[i][j] = max(dp[i - 1][j], dp[i-1][j - size[i]] + worth[i]); else dp[i][j] = dp[i - 1][j]; } } cout << dp[n][v] << endl; return 0; }
利用滚动数组,结合状态转换方程可知,需要逆序考察,因为还未更新过的dp数组值可能会被使用
#include <bits/stdc++.h> using namespace std; int n, m; int main() { cin >> n >> m; int v[n + 1], w[n + 1]; for (int i = 1; i <= n; i++) cin >> v[i] >> w[i]; int dp[m + 1]; memset(dp, 0, sizeof dp); for (int i = 1; i <= n; i++) { for (int j = m; j >= 0; j--) { if (j >= v[i]) dp[j] = max(dp[j], dp[j - v[i]] + w[i]); } } cout << dp[m] << endl; return 0; }
#include <bits/stdc++.h> using namespace std; int n, v; int main() { cin >> n >> v; int size[n + 1], worth[n + 1]; for (int i = 1; i <= n; i++) cin >> size[i] >> worth[i]; int dp[n + 1][v + 1]; memset(dp, 0, sizeof dp); for (int i = 1; i <= n; i++) { for (int j = 0; j <= v; j++) { if (j >= size[i]) dp[i][j] = max(dp[i - 1][j], dp[i][j - size[i]] + worth[i]); // 这里有变化 else dp[i][j] = dp[i - 1][j]; } } cout << dp[n][v] << endl; return 0; }
因为未更新的数据不需要使用,所以直接正向进行循环
#include <bits/stdc++.h> using namespace std; int n, m; int main() { cin >> n >> m; int v[n + 1], w[n + 1]; for (int i = 1; i <= n; i++) cin >> v[i] >> w[i]; int dp[m + 1]; memset(dp, 0, sizeof dp); for (int i = 1; i <= n; i++) { for (int j = m; j >= 0; j--) // 这里有变化 { if (j >= v[i]) dp[j] = max(dp[j], dp[j - v[i]] + w[i]); } } cout << dp[m] << endl; return 0; }
#include <bits/stdc++.h> using namespace std; int n, v; int main() { cin >> n >> v; int size[n + 1], worth[n + 1]; for (int i = 1; i <= n; i++) cin >> size[i] >> worth[i]; int dp[n + 1][v + 1]; memset(dp, 0, sizeof dp); for (int i = 1; i <= n; i++) { for (int j = 0; j <= v; j++) { if (j >= size[i]) dp[i][j] = max(dp[i - 1][j], dp[i][j - size[i]] + worth[i]); // 这里有变化 else dp[i][j] = dp[i - 1][j]; } } cout << dp[n][v] << endl; return 0; }
因为未更新的数据不需要使用,所以直接正向进行循环
#include <bits/stdc++.h> using namespace std; int n, m; int main() { cin >> n >> m; int v[n + 1], w[n + 1]; for (int i = 1; i <= n; i++) cin >> v[i] >> w[i]; int dp[m + 1]; memset(dp, 0, sizeof dp); for (int i = 1; i <= n; i++) { for (int j = m; j >= 0; j--) // 这里有变化 { if (j >= v[i]) dp[j] = max(dp[j], dp[j - v[i]] + w[i]); } } cout << dp[m] << endl; return 0; }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。