前言
重学算法第6天,希望能坚持打卡不间断,从基础课开始直到学完提高课。
预计时长三个月内,明天再来!肝就完了
2月18日,day06 打卡
今日已学完y总的
算法基础课-2.4-Week3 习题课
共3题,知识点如下
Trie(单词查找树):最大异或对
并查集:食物链
复习了
堆:模拟堆
额外补充1题合计4题
栈:表达式求值
Trie树
AcWing 143. 最大异或对
思路:
异或:相同为0,不同为1,俗称不进位加法
暴力做法
优化
每次从高位往前找,每次尽量往与
当前位不同
的分支走(相同为0,不同为1)
如果有0和1都可选,就选高位开始不同的(异或后更大),相同的划掉
如果只有1个可以走那就没得选,
我们当前走的分支每次都比划去的大,所以直达走到最低位时,当前走的分支就是
异或后得到最大值
的分支
可以用 trie 来存储每个数
把每个整数看成31位的二进制串
从根节点开始,每次尽量从与当前位不同的上面走,走到叶结点就找到了
trie 不光能存字符串,还能存整数,二进制数,
计算机内所有信息都是二进制表示,因此 trie 能存储所有信息
样例
可以边插入边查找
每次写代码前最好先把思路梳理一下
从前往后枚举,插入一个数,查询一个数
每次查询的是a[i]前面
和a[i]
异或 值最大的是啥
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010, M = 31 * N;
int n;
int a[N];
int son[M][2], idx;
void insert(int x) {
int p = 0;
for (int i = 30; i >= 0; i--) { //从最高位开始,每次取出当前位
int u = x >> i & 1; // 取出x的第i位是多少,前面位运算讲过
if (!son[p][u]) son[p][u] = ++ idx; // 如果不存在就创建
p = son[p][u]; // p走到儿子上去
}
}
int query(int x) {
int p = 0, res = 0;
for (int i = 30; i >= 0; i--) {
int u = x >> i & 1; // 0或1
if (son[p][!u]) {
p = son[p][!u]; // 另一个方向(与该位不等的)如果存在就走上去
res = res * 2 + !u;
} else {
p = son[p][u];
res = res * 2 + u; // *2 相当于左移一位,最后1位 置0,如果u为0加0,为1就加1
}
}
return res;
}
int main() {
scanf("%d", &n);
for (int i = 0; i < n; i++) scanf("%d", &a[i]);
int res = 0;
for (int i = 0; i < n; i++) {
insert(a[i]); // 先插入再查询可以避免为空,为空要进行特殊处理
int t = query(a[i]); // t为与a[i] 异或 值最大的
res = max(res, a[i] ^ t);
}
printf("%d\n", res);
return 0;
}
res
并查集
AcWing 240. 食物链
样例
环形,3被2吃,2被1吃,1被3吃
只要知道两个动物的关系,就放到一个集合里面,
一定可以推出同一集合中所有对应的关系(只有三种动物)
如:xy是同类,y被z吃,则x也被z吃
x被y吃,y被z吃,则z被x吃
如何确定关系?
记录每个点与根节点的关系
用每个点到根节点的距离来记录关系
可以分为3大类
距离的含义:表示第几代
y是0代,x吃y的就是1代,z吃x就是2代,如果k吃z就是3代
该题只有ABC三种,是循环的
A是0代,B吃A是1代,C吃B是2代,
第三代吃C,而A吃C,所以第三代就是第0代,即A
第四代吃A,而B吃A,所以第四代就是第2代,即B
所以用模表示即可 ABC
余1吃A —第1代,是B
余2吃B —第2代,是C
余0吃C —同类,是A
本来是存的到父节点的距离,路径压缩的时候更新成到根节点的距离即可
而只要知道到根节点的距离,就能根据余数判断出关系
#include <iostream>
using namespace std;
const int N = 50010;
int n, m; // m表示说话的次数
int p[N], d[N]; // p: 父节点, d: 距离
int find(int x) {
if (p[x] != x) {
// 用t存储的原因:find之后d[p[x]里面存的就不是p[x]这个点了,而是根节点
int t = find(p[x]); //定义1个变量存一下p[x]的根节点是谁
d[x] += d[p[x]]; // 两段相加就是总距离
p[x] = t; // 加完再让p[x] 指向根节点
}
return p[x];
}
int main() {
scanf("%d%d", &n, &m);
//把每个点先初始化
for (int i = 1; i <= n; i++) p[i] = i; //d=0,不需要初始化了,全局变量默认值为0
int res = 0;
while (m--) {
int t, x, y; // t表示询问的种类(D)
scanf("%d%d%d", &t, &x, &y);
if (x > n || y > n) res++; // 当前的话中x或y比n大,是假话
else {
int px = find(x), py = find(y); // x, y的根节点
if (t == 1) {// 若X和Y是同类
// 若 x 与 y 在同一个集合中
if (px == py && (d[x] - d[y]) % 3) res++; //两数到根节点距离之差 %3不为 0,说明不是同一类,是假话
// 若 x 与 y 不在同一个集合中
else if (px != py) {
p[px] = py; // 让py成为px的父节点,即把x所在集合的根节点插入到y所在集合根节点上
d[px] = d[y] - d[x];
}
}
else { // 若X吃Y , 即%3后,x要比y多1
if (px == py && (d[x] - d[y] - 1) % 3) res++; // 若距离之差-1 %3不为 0,说明吃不掉,是假话
else if (px != py) {
p[px] = py;
d[px] = d[y] + 1 - d[x];
}
}
}
}
printf("%d\n", res);
return 0;
}
距离图
不在一个集合中时
xy为同类,不在一个集合中时,(d[p(x)] + d(x) - d(y) ) %3 = 0
需要成立
即d[p(x)] = d(y) - d(x)
凡是涉及集合合并的都可以用并查集
考虑什么时候用啥数据结构时,
先看题目中的哪些操作可以进行优化,操作有什么样的特点
如:
寻找一堆数里的最小值或最大值-----用堆来做
维护有序链表—用平衡树来做
维护区间最大值,区间和等—可能要用树状数组或者线段树来做
并查集的题目不太容易看出来,需要多做题
堆
AcWing 839. 模拟堆
该题难在需要支持随便的修改和插入,修改堆里的某一个元素
就需要存映射,即难在映射
交换的时候每个点的映射也需要交换
ph,左边指向右边
hp,右边指向左边
hp[a]
,hp[b]
就是a b指向左边点的,绿线
ph[hp[a]
]就是左边点hp[a]
指向 a 的
ph[hp[b]]
就是左边点hp[b]
指向 b 的
交换两条蓝色的线的指向 swap(ph[hp[a]], ph[hp[b]]);
,变为虚线
交换绿色线的指向swap(hp[a],hp[b]);
,变成虚线
最后交换
a
和b
的值swap(h[a],h[b]);
最难的就是交换操作,其他的与堆没有区别
down操作和up操作略,看上一篇即可
#include <iostream>
#include <algorithm>
#include <string.h>
using namespace std;
const int N = 100010;
int n, m;
int h[N], sizes;
// ph[k]存的是第k个插入的数下标是多少
// hp[k]堆里的某个点是第几个插入点
// 既要从第几个插入点找堆内到元素,又要堆里的元素找回来,所以要一一对应
int ph[N], hp[N];
// 存储映射,不常用
void heap_swap(int a, int b) {
// 需要把所有指向都交换,继续一一对应
swap(ph[hp[a]], ph[hp[b]]); // ph:从下标映射到堆
swap(hp[a],hp[b]); //hp:从堆里映射回下标
swap(h[a],h[b]); //交换堆内元素
}
void down(int u) {
int t = u;
if (u * 2 <= sizes && h[u * 2] < h[t]) t= u * 2;
if (u * 2 + 1 <= sizes && h[u * 2 + 1] < h[t]) t= u * 2 + 1;
if (u != t) {
heap_swap(u, t);
down(t);
}
}
void up(int u) {
while (u / 2 && h[u / 2] > h[u]) {
heap_swap(u / 2, u);
u /= 2;
}
}
int main() {
int n, m = 0;
scanf("%d", &n);
while (n --) {
char op[10];
int k, x;
scanf("%s",op);
if (!strcmp(op, "I")) { // 插入一个数 x;
scanf("%d", &x);
sizes++; //堆里要多一个元素
m++; // m表示第几个插入的数
ph[m] = sizes; // 第m个插入的数在堆中的sizes位置
hp[sizes] = m; // 堆里面sizes这个数对应的是m
h[sizes] = x;
up(sizes); // 插入到堆尾,需要向上走
}
// strcmp(str1,str2),若str1=str2,则返回零;若str1>str2,则返回正, <返回负数
else if (!strcmp(op, "PM")) printf("%d\n", h[1]);//输出当前集合中的最小值
else if (!strcmp(op, "DM")) {
heap_swap(1, sizes); // 将最后一个的值与最小值交换
sizes--; // 删除最后一个
down(1); // 下沉
}
else if (!strcmp(op, "D")) { //删除第 k 个插入的数;
scanf("%d", &k);
k = ph[k]; // 找到k在堆里的位置
heap_swap(k, sizes);
sizes--;
down(k), up(k); // 最多只会执行一个
} else { // 修改第 k 个插入的数,将其变为 x;
scanf("%d%d", &k, &x);
k = ph[k]; // 先找个第k个插入的数 在堆里的位置
h[k] = x; // 直接修改成x
down(k), up(k);
}
}
return 0;
}
栈
3302. 表达式求值
中缀表达式需存运算符
可以用栈来达到用递归求解的效果
一开始所有父节点都存的是运算符,算完之后将值存在运算符处
左中右将整棵树遍历完之后,就可以从下往上算了
用下面两个的值算出父节点处的值
默认根节点的操作符优先级最低,越往下优先级越高
如何判断某颗子树被遍历完?
由于先操作优先级高的,低的后操作
往上走,如果前运算符的优先级低于上一个运算符
说明当前节点的左右子树都操作完了
如果在括号内,必然是往下走的,需要从右往左算
运算符可以任意定,所以该题是个模板题
#include <iostream>
#include <cstring>
#include <algorithm>
#include <stack>
#include <unordered_map>
using namespace std;
// 需要存数字和运算符
stack<int> num;
stack<char> op;
//eval() 即操作最后一个运算符(末尾运算符操作一下末尾两个数)
void eval() {
auto b = num.top(); num.pop();// b是倒数第一个数,右子树
auto a = num.top(); num.pop();// a是倒数第二个数,左子树
auto c = op.top(); op.pop(); //取出运算符(a b的父节点)
int x;
if (c == '+') x = a + b;
else if (c == '-') x = a - b;
else if (c == '*') x = a * b;
else x = a / b;
num.push(x); // 将数存到栈内(原来运算符位置)
}
int main() {
// 定义优先级,数越大优先级越大
unordered_map<char, int> pr{{'+', 1}, {'-', 1}, {'*', 2}, {'/', 2}};
string str;
cin >> str;
for (int i = 0; i < str.size(); i++) {
auto c = str[i];
if (isdigit(c)) { // 如果当前字符为数字
int x = 0, j = i;
while (j < str.size() && isdigit(str[j])) // 括号别漏了或者写错了
x = x * 10 + str[j ++] - '0';
i = j - 1;
num.push(x); // 当前数为x
}
else if (c == '(') op.push(c); // 左括号直接入栈
else if (c == ')') {
// 将栈内所有运算符,从右往左操作一遍直到遇到(
while (op.top() != '(') eval();
op.pop(); // 左括号弹出
}
else { // 一般运算符
// 栈不为空且栈顶元素优先级大于等于当前元素优先级,操作栈顶元素
while (op.size() && pr[op.top()] >= pr[c]) eval();
// 否则将当前元素插入栈里
op.push(c);
}
}
// 将所有没有操作的运算符,从右往左操作一遍
while (op.size()) eval();
// 最后栈顶元素就是答案
cout << num.top() << endl;
return 0;
}
评论区