对近期的一些整理(刚好情绪不太好,看啥都想哭,所以写个博客缓缓
1. Trie树(字典树)
可用于:高效地存储和查找字符串集合(整数也可以存(都可以存
(之前新学的,不太记得了,所以原理讲得稍微详细一些
AcWing: 835. Trie字符串统计
以上题为例
例如:插入 abc ab ab cdf cdee 这几个字符串
建完之后如下(+1是cnt数组)
因为有26个英文小写字母,所以每个节点最多有26个儿子
即:如下a节点下连接最多26个儿子
将字符映射成整数,即id = ch - 'a'
即可对应数组下标实现快速的随机访问
所以开一个二维数组:int son[N][26];
//N是一共有多少个节点,即多少个所有的字符串一共用多少个字符
我们来看一下insert的操作(全是注释(我一开始也一直不理解这个idx,希望在这里讲明白了
int p = 0;//将根节点设为0,从根节点开始找
for(int i = 0; i < s[i].size(); i++)//遍历整个字符串
{
int u = s[i] - 'a';//将小写字母映射成0~25的整数
if(!son[p][u])//如果这个节点不存在(即值为0,表示空节点)
son[p][u] = idx++;//相当于动态储存时开辟一块空间来存这个节点
//但是因为是静态储存的,所以用idx作为标识,从1开始,将已有的空间分配给需要的,分配完之后向后移一个,指向新的未分配的空间
p = son[p][u];//存在这个节点(此时必然存在的,因为没存在我也先建立了节点
//相当于将父节点设为这个字符所在的节点的地址,即它的idx
}
cnt[p]++;//以这个单词为结尾的数量增加一个
//为什么只有一维的,因为idx为每个节点分配了唯一的标识(相当于动态分配的空间地址),在上面的循环结束后p必然是最后一个节点的idx
查询操作就很容易了
int p = 0;//从根节点开始找
for (int i = 0;i < s.size();i++)
{
int u = str[i] - 'a';//获取映射后的下标
if (!son[p][u])return 0;//如果当前节点的idx值为0,表示为空节点,即不存在这个字符,也就没有这个字符串,直接返回0个
p = son[p][u];//同上述insert时
}
return cnt[p];//返回以这个单词为结尾的数量
//因为存在一种情况就是这个trie里可以找到要查询的字符,但是以这个结尾字符上并没有标记,那也说明没有
//比如插入abcd而查找ab//d上cnt有值,但是b上就为0
AC代码:
#include<iostream>
using namespace std;
const int N = 100005;
int son[N][26];//每个节点的所有儿子
int cnt[N];//以当前这个点为结尾的单词有多少个
int idx;//当前用到了哪个下标//下标是0的点即是根节点也是空节点
void insert(const char str[])
{
int p = 0;
for (int i = 0;str[i];i++)//因为字符串是以\0结尾。所以可以用str[i]来判断是否走到了结尾
{
int u = str[i] - 'a';//将小写字母映射成0~25
if (!son[p][u])//如果这个节点不存在,那么新建节点
son[p][u] = ++idx;
p = son[p][u];
}
cnt[p]++;//以这个单词结尾的数量增加一个
}
//查询字符串出现的次数
int query(const 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];
}
int main()
{
int n;cin >> n;
char op;
char str[N];
while (n--)
{
cin >> op >> str;
if (op == 'I') {
insert(str);
}
else
cout << query(str) << endl;
}
}
扩展:AcWing143. 最大异或对
提示:将整数以二进制的形式存入trie树中
x>>k&1
取一个整数x的第k位二进制数是多少(我才不会告诉你我不知道这个操作的时候是怎么写了个屎山代码取二进制的第k位的
坑:记得统一二进制位数为最多位
(可持久化trie以后再补,今天没看得下去(就是我不会捏//
2.并查集
(我之前并查集学假了吧(要不是因为网络赛我都不知道
可用于:1.将两个集合合并
2.询问两个元素是否在一个集合中
先来默写一下基本板子~
int p[N];
void init(int n)//我永远记得n次把初始化写在函数里然后不在主函数里调用()
{
for(int i = 1; i <= n; i++)
p[i] = i;
}
int find(int x)
{
if(p[x]!=x)
p[x] = find(p[x]);
return p[x];
}
void merge(int a, int b)
{
int ra = find(a),rb = find(b);
if(ra != rb)
{
p[ra] = rb;
}
}
然后记录一下可以维护的额外信息:
- 可以同时维护每个集合中点的个数
由于集合中点的个数这个属性为整个集合的元素所共有,所以我们只需要维护根节点上的size就可以了,每次查询的时候只需要获取到size[find(a)]
- 可以同时维护每个集合中边的个数
同点的个数一样,只需要维护根节点上的
参考2023icpc第一场网络赛的D题
#include<iostream>
#include<vector>
using namespace std;
#define inf 0x3f3f3f3f
typedef pair<int, int> pii;
typedef long long ll;
const int N = 1e6 + 2;
int p[N];
int s[N];//size
int e[N];//edge
void init(int n)
{
for (int i = 1; i <= n; i++)
{
p[i] = i;//每个点都属于自己的一个集合
s[i] = 1;//顶点数只有1个就是它自己
e[i] = 0;//边数为0
}
}
int find(int x)
{
if (x != p[x]) p[x] = find(p[x]);
return p[x];
}
void merge(int x, int y)
{
int rx = find(x), ry = find(y);
if (rx != ry) {
s[ry] += s[rx];//根节点的顶点数加上合并过来的顶点数
e[ry]++;//从根节点到要合并过来的根节点之间要加一条边
e[ry] += e[rx];//根节点的集合边数加上要合并过来的集合的边数
p[rx] = ry;//合并
}
else
e[rx]++;
}
int main()
{
int n, m;cin >> n >> m;
init(n);
int u, v;
vector<int>edge;
while (m--)
{
cin >> u >> v;
edge.push_back(u);
merge(u, v);
}
ll m1 = inf, m2 = inf;//最小的两个集合
ll ans = 0;
vector<bool>vis(n + 1);
for (int i = 1; i <= n; i++)
{
int p = find(i);
if (vis[p])continue;//不重复遍历集合
vis[p] = 1;
ll v = s[p];//顶点数
ll ed = e[p];//边数
ans += (v - 1) * v / 2 - ed;//缺少的边数
if (v <= m1)//找到最小的顶点数
{
m2 = m1;
m1 = v;
}
else if (v <= m2)//次小的顶点数
{
m2 = v;
}
}
if (ans == 0)
{
ans = m1 * m2;
}
cout << ans << '\n';
return 0;
}
- 维护一些权值(有别于上面的属性是整个集合所共有的,这里的权值仅只子节点到它的直接父节点的距离
所以在路径压缩时有必要跟新权值,提一点注意事项(之前自己自作聪明犯的错
int find(int x)
{
if(p[x]!=x){
int t = p[x];
p[x] = find(p[x]);
d[x]+=d[t];//要从后往前更新,因为当前的父节点不一定是整个集合的父节点
//或者
// int root = find(p[x]);
// d[x]+=d[p[x]];
// p[x] = root;
}
return p[x];
}
上述的权值只是简单的 + 操作,具体要按题目意思来跟新权值
参考题目:
食物链
奇偶游戏
(放一下AC代码,当时写的可能有点混乱不过还好()
食物链
带权并查集
#include<iostream>
#include<cmath>
using namespace std;
const int N = 50005;
int p[N],d[N];
//不可取:三个集合,维护根节点的类型(×
//正解:维护每个节点到其父节点的距离
//1<-2<-3<-4<-5<-6<-7...
//表示5吃4,4吃3,3吃2,2吃1
//5吃4,5和4的距离是1:5吃4
//4吃3,4和3的距离是1
//3吃2,3和2的距离是1
//那么,5和3的距离是2:表示3吃5,即5被3吃
// 5和2的距离是3:表示同类,因为是环 取模%3后等于0,上述都用%3后的余数来判断即可
//余数为0表示和根节点是同类
//余数为1表示可以吃掉根节点
//余数为2表示被根节点吃掉
void init(int n)
{
for(int i = 1; i <= n; i++)
{
p[i] = i;
d[i] = 0;//自己和自己是一类
}
}
int find(int x)
{
if(p[x]!=x)
{
int t = p[x];
p[x] = find(p[x]);
d[x]+=d[t];
}
return p[x];
}
bool merge(int x, int y, int op)
{
int px = find(x),py = find(y);
if(op == 1)
{
if(px == py){
if(abs(d[x]-d[y])%3)
return true;
else
return false;
}
d[px] = d[y]-d[x];
p[px] = py;
return false;
}
else{
if(x == y)return true;
if(px == py){
if((d[x]-1-d[y])%3)//呃呃,呃呃,呃呃呃
return true;
else
return false;
}
d[px] = d[y]+1-d[x];
p[px] = py;
}
return false;
}
int main()
{
int n,k;cin>>n>>k;
init(n);
int sum = 0;
int op,a,b;
while(k--)
{
cin>>op>>a>>b;
if(a > n || b > n)
{
sum++;
continue;
}
sum+=merge(a,b,op);
}
cout<<sum;
}
带扩展域的并查集
#include<iostream>
#include<cmath>
using namespace std;
const int N = 50005*3;
const int Base = 50004;
int p[N];
//扩展域写法
//X 表示x为A类动物
//x+Base表示B类动物
//x+2*Base表示C类动物
void init()
{
for(int i = 1; i < N; i++)
{
p[i] = i;
}
}
int find(int x)
{
if(p[x]!=x)
{
p[x] = find(p[x]);
}
return p[x];
}
bool merge(int x, int y, int op)
{
if(op == 2)
{
if(x ==y)
return true;
if(find(x) == find(y)||find(x) == find(y+2*Base))
return true;
p[find(x)] = find(y+Base);//x是A类动物,y是B类动物
p[find(x+Base)] = find(y+2*Base);//X是B类动物,y是C类动物
p[find(x+2*Base)] = find(y);//x是C类动物,y是A类动物
return false;
}
else
{
if(find(x) == find(y+Base) || find(x) == find(y+2*Base))
return true;
p[find(x)] = find(y);//x是A类动物,y是A类动物
p[find(x+Base)] = find(y+Base);//x是B类动物,y是B类动物
p[find(x+2*Base)] = find(y+2*Base);//x是C类动物, y是C类动物
return false;
}
return false;
}
int main()
{
int n,k;cin>>n>>k;
init();
int sum = 0;
int op,a,b;
while(k--)
{
cin>>op>>a>>b;
if(a > n || b > n)
{
sum++;
continue;
}
sum+=merge(a,b,op);
}
cout<<sum;
}
奇偶游戏
(这个用前缀和的转换就已经妙不可言了好吧
带权并查集写法
#include<iostream>
#include<unordered_map>
using namespace std;
const int N = 10005;
int p[N],d[N];
unordered_map<int,int>has;
int idx = 0;
//s为前缀和
//1,2 even 等价于 s2-s0 为偶数//等价于 s2 和 s0 是同偶或同奇
//3,4 odd 等价于 s4-s2 为奇数//等价于 s4 和 s2 奇偶相异
//若si 合法,是否一定能够构造出满足所有si条件的01串呢?
//答案是肯定的,令a[i] = s[i]-s[i-1]即可,因为每次奇偶性的改变只会加上1
//d[i]存储到根节点的距离,d[i]%2 == 0代表与根节点同类,反之异类
//d[x]+d[y] 如果是偶数,那么x和有同类:
//1.d[x] = 0,则d[y] = 0, 则x,y,根节点同类
//2.d[x] = 1,则d[y] = 1, 则x与根节点异类,y与根节点异类,则x和y同类
//反之如果d[x]+d[y]为奇数,那么x和y异类
int get(int x)//离散化
{
if(has.count(x) == 0)
has[x] = idx++;
return has[x];
}
int find(int x)
{
if(x != p[x])
{
int t = p[x];
p[x] = find(p[x]);
d[x]+=d[t];
d[x]%=2;
}
return p[x];
}
int main()
{
int n;cin>>n;
int m;cin>>m;
for(int i = 0; i <= N; i++)
p[i] = i;
int l,r;
string odd;
int res = m;
for(int i = 1; i <= m; i++)
{
cin>>l>>r>>odd;
int x = get(l-1);//用到的是r和l-1
int y = get(r);
int ra = find(x),rb = find(y);
if(odd == "odd")
{
if(ra == rb)
{
if(!(d[x] + d[y])&1)
{
res = i -1;
break;
}
}
else
{
d[ra] = (1-d[x]-d[y]+2)%2;
p[ra] = rb;
}
}
else
{
if(ra == rb)
{
if((d[x]+d[y])&1)
{
res = i-1;
break;
}
}
else{
d[ra] = (2-d[x]-d[y])%2;
p[ra] = rb;
}
}
}
cout<<res;
}
扩展域写法
#include<iostream>
#include<unordered_map>
using namespace std;
const int N = 10005*2;
const int Base = 10005;
int p[N];
unordered_map<int,int>has;
int idx = 0;
//扩展域写法:集合里存的是条件,比如s1是奇数,s2是偶数,采用的类似枚举的思想
//x是奇数,那么x+Base是偶数
int get(int x)//离散化
{
if(has.count(x) == 0)
has[x] = idx++;
return has[x];
}
int find(int x)
{
if(x != p[x])
{
p[x] = find(p[x]);
}
return p[x];
}
int main()
{
int n;cin>>n;
int m;cin>>m;
for(int i = 0; i <= N; i++)
p[i] = i;
int l,r;
string odd;
int res = m;
for(int i = 1; i <= m; i++)
{
cin>>l>>r>>odd;
int x = get(l-1);//用到的是r和l-1
int y = get(r);
if(odd == "odd")
{
if(find(x) == find(y))
{
res = i-1;
break;
}
p[find(x)] = find(y+Base);
p[find(x+Base)] = find(y);
}
else
{
if(find(x) == find(y+Base))
{
res = i-1;
break;
}
p[find(x)] = find(y);
p[find(x+Base)] = find(y+Base);
}
}
cout<<res;
}
3.树状数组
可用于:1. 单点修改+维护前缀和
2.区间修改+单点查询(基于差分数组(本质还是单点修改+区间查询
3.区间修改+区间查询(维护两个树状数组(推个式子(×
先默写一下板子~
#define lowbit(x) ((x)&(-x))
int t[N];
void updata(int x, int d)
{
while(x<=n)
{
t[x]+=d;
x+=lowbit(x);
}
}
int sum(int x)
{
int ans = 0;
while(x>0)
{
ans+=t[x];
x-=lowbit(x);
}
return ans;
}
然后记录一下比较令我震惊的(哇去这也能用树状数组(?! 的。。
- 可以用树状数组统计一个序列中,位于某个点之前/后 的数有多少个数比它大/小
主要思路:之前都是将序列的下标传入updata,但是也可以将其值传入作为树状数组的下标
从前向后遍历,到序列的第 i 个数 x, 如果要求其前小于 x 的数的个数,那么sum(x-1)
如果要求其前大于x的数的个数,那么sum(n)-sum(x)
,然后再updata(x,1)
//有时候可能需要离散化
参考题目:AcWing 241. 楼兰图腾
参考代码:
#include<iostream>
#include<cstring>
using namespace std;
#define lowbit(x) ((x)&(-x))
const int N = 200005;
typedef long long ll;
int t[N],a[N];
int G[N],L[N];//greater,lower
int n;
void updata(int x, int d)
{
while(x<=n)
{
t[x]+=d;
x+=lowbit(x);
}
}
ll sum(int x)
{
ll ans = 0;
while(x>0)
{
ans+=t[x];
x-=lowbit(x);
}
return ans;
}
int main()
{
cin>>n;
for(int i = 1; i <= n; i++)
cin>>a[i];
for(int i = 1; i <= n; i++)
{
int y = a[i];
G[i] = sum(n)-sum(y);//在前面比当前y大的数的个数,即这些数的范围是y+1~n
L[i] = sum(y-1);//在前面比当前y小的数的个数,即这些数的范围是1~y-1;
//为什么是前面,因为在算这个点之前只有前面的点被更新了(i从前向后遍历),后面还都是0
updata(y,1);
}
ll res1 = 0,res2 = 0;
//memset(t,0,sizeof(t));//清空树状数组//不清空的话下面的sum减掉一开始加的所有1//清空的话就不用了
for(int i = n; i >= 1; i--)
{
int y = a[i];
res1 += G[i]*(sum(n)-sum(y)-n+y);//G[i]是在y前面比y大的数的个数,sum(n)-sum(y),是在y后面比y大的个数
res2 += L[i]*(sum(y-1)-y+1);
//为什么是后面,因为再算这个点之前只有后面被更新了(i从后向前遍历),前面的都是1(没清空是1,清空了是0
updata(y,1);
}
cout<<res1<<' '<<res2;
}
- 在上述思想上加上二分,可以求得序列里的第k小数
参考题目:AcWing 244. 谜一样的牛
参考代码:
#include<iostream>
using namespace std;
#define lowbit(x) ((x)&(-x))
const int N = 1e5+5;
int a[N],h[N];
int t[N];
int n;
//初始时1~n每一个身高都还没有被用过,都被标记为1
//从最后一头牛开始,若它前面有k头牛比它矮,那么它的身高就是k+1
//然后再看倒数第二头牛,在剩下的没有被使用的身高里,如果它前面有k头牛比它矮
//那它的身高就是在剩下的身高里,第k+1小的数
//所以我们的操作只有两个:
//1.找到在没有被使用的身高里第k小的数
//(对应前缀和,因为只有1累加,所以前缀和就是身下身高的个数,其和等于k时
//这个数就是第k小的数(二分找到第一个数使得前缀和等于k
//2.将这个数删除(对应updata操作-1
void updata(int x, int d)
{
while(x <= n)
{
t[x]+=d;
x+=lowbit(x);
}
}
int sum(int x)
{
int ans = 0;
while(x > 0)
{
ans+=t[x];
x-=lowbit(x);
}
return ans;
}
int main()
{
cin>>n;
for(int i = 1; i <= n; i++)
updata(i,1);
for(int i = 2; i <= n; i++)
{
cin>>a[i];
}
for(int i = n; i >= 1; i--)
{
int l = 1,r = n;
while(l < r)
{
int mid = l+r>>1;
if(sum(mid) >= a[i]+1)
{
r = mid;
}
else
l = mid + 1;
}
h[i] = l;
updata(l,-1);
}
for(int i = 1; i <= n; i++)
cout<<h[i]<<'\n';
return 0;
}
- 我自己遇到的一个更新差分的问题(当时想问别人的,结果理了一下思路自己给整明白了(×
4.线段树
记录几个注意事项吧,写不动了(
- 线段树内维护的信息,是否足以用子区间算出父区间
比如求区间最大值,那么父区间的max = max(左儿子,右儿子)
,是成立的,所以只需要维护一个区间max就够了
但是如果求 区间最大连续子段和父区间的最大连续字段和 = max(左儿子,右儿子)
吗?
显然不是,因为父区间的最大连续子段和有可能跨过两个子区间,所以就要再好好考虑一下还要维护额外的哪些信息才能维护好最大连续子段和了:区间总和,区间最大前缀和,区间最大后缀和 - 维护懒标记时,如果维护多个,它们之间是否右先后次序关系
比如同时维护 给区间 + 和给区间 × 的懒标记,先+ 后 × 和 先 × 后 + 是不一样的
但是!可以合并这两个操作,统一为先乘后加(方便更新懒标记,也方便下沉(pushdown)
当操作为加时,给乘传参为1,给加传参为d,当操作为乘时,给乘传参为d,给加传参为0 - 再丢一个啃扫描线啃到的右端点偏移映射和线段树的去重(妙哉~
晚安安~
(虽然已经早上四点半了()