10月1号D1数据结构(PPT2)
10月1号D1数据结构(PPT2)
接下来我们来到树形树状结构...
堆
这个才是神!!!!
默认大根堆...
例一:
-
插入一个元素
-
删除一个元素
-
询问最小值
-
怎么用做
很简单,我们可以维护两个小根堆,一个存储插入元素,一个存储删除元素。
查询时,如果两个相等就两个都不断push,直到不一样,如果不相等就直接输出。
这就是换个角度想问题,我们假装他被删了,但在序列中其实没有删,一定要养成这种思想!!!
例二:
-
个任务,每个任务有需要的时间和截止时间
-
求至多能完成多少任务
这是一道贪心的题目,很容易想到,我们按照从小到大排序,如果可以做就直接做,如果不能做就搞死已做完中需要时间最长的任务然后换成它,这样一定是更优的。
例四:
- 定义一个区间的长度为
- 给定数轴上个区间,选择其中的个点,求最大交集。
假如我们已经确定了最终区间的左端点L,那么我们选择的区间一定是左端点在L左边,且右端点最右的K个点。所以我们将所有区间按左端点排序,用小根堆维护左端点在左边,且右端点最大的K个点。每次用第K大值更新答案即可。
维护以一个大小不变为k的小根堆,每来一个数看一下能不能放进堆,能来的话就弹掉栈顶放进来。
#include <cstdio>
#include <cstring>
#include <iostream>
#include <queue>
#include <algorithm>
using namespace std;
const int maxn=1000010;
int n,k,ans;
struct node
{
int l,r,org;
node() {}
node(int a,int b) {r=a,org=b;}
bool operator < (const node &a) const {return r>a.r;}
}p[maxn];
priority_queue<node> q;
bool cmp(const node &a,const node &b)
{
return a.l<b.l;
}
inline int rd()
{
int ret=0,f=1; char gc=getchar();
while(gc<'0'||gc>'9') {if(gc=='-') f=-f; gc=getchar();}
while(gc>='0'&&gc<='9') ret=ret*10+(gc^'0'),gc=getchar();
return ret*f;
}
int main()
{
n=rd(),k=rd();
int i;
for(i=1;i<=n;i++) p[i].l=rd(),p[i].r=rd(),p[i].org=i;
sort(p+1,p+n+1,cmp);
for(i=1;i<=n;i++)
{
q.push(p[i]);
if(i>k) q.pop();
if(i>=k) ans=max(ans,q.top().r-p[i].l);
}
while(!q.empty()) q.pop();
printf("%d\n",ans);
for(i=1;i<=n;i++)
{
q.push(p[i]);
if(i>k) q.pop();
if(i>=k&&ans==q.top().r-p[i].l)
{
while(!q.empty()) printf("%d ",q.top().org),q.pop();
return 0;
}
}
}
二叉搜索树
结点的左子树的任意一个结点的值都小于父亲
结点的右子树的任意一个结点的值都大于父亲
显然的结论:中序遍历是正序的(从小到大的排列)
这个结论可以用于把树形转化为区间,作DP!!!
所以看到二叉搜索树一定要想着转化为区间,这个可能就是你解题的关键!!!
例一:
-
给定一课n个节点的二叉搜索树
-
求使得树的形态形同的字典序最小的插入序列
根肯定是最先插入的,然后下面会有两个相互独立的东西,然后分治一下,得到两个最小插入序列,然后两个按最小合并一下,这道题就解决了。
STL
*(x)是让你改的,不会CE,但是不能改
*(x)在后面有讲解
set/multiset
set是把相同元素只留下来一个的数据结构
multiset保存所有相同元素的set
set<int/struct> mp;
struct node{//如果用struct的话
...
bool operator<(node a,node b){}//这个一定要有,这个就不会删掉相同的元素
};
mp.insert(x)//插入一个元素//可以是一个元素也可以是iterator
mp.erase()//删除一个元素//可以是一个元素也可以是iterator
set<node>::iterator it=mp.find(x)//iterator是一个迭代器
//如果没有找到,it=mp.end();
mp.begin()//最小的元素的iterator
mp.end()//虚拟的无限大的元素的iterator
--mp.end()//最大的元素
it++ //表示迭代器往后移动一位
it-- //往前
int cnt=mp.count(x)//表示有没有x这个值,返回0/1
for(set<int>::iterator it=mp.begin();it!=mp.end();it++)//set的遍历
**********************************************************************
lower_bound(x)//返回一个iterator指向第一个>=x的元素(如果没有就返回mp.end())
upper_bound(x)//返回一个iterator指向第一个>x的元素(如果没有就返回mp.end())
*(--mp.lower_bound(x))//第一个小于x的元素
*(--mp.upper_bound(x))//第一个小于等于x的元素
**********************************************************************
map
map<node,int> mp;
map[x]==2 <==> mp.insert(make_pair(x,2)) //可以直接用下标存储
mp.erase(x)//删除元素
用map记录次数的时候
增加次数mp[x]++
减少次数mp[x]-- if(mp[x]==0) mp.erase(x)//删除这个空元素,防止出锅
map<node,int>::iterator it=mp.find(x)
//map的iterator有两个值
it->first //代表key(key一定不能改,改了就炸)
it->second //代表value(value是可以改的,因为value只是存了个数据)
其他的都和set几乎一样
例题:
实现一个离散化
(在考试时离散化还是要用sort来实现)
//非常慢,考试的时候不要用,这只是为了练习。
for(int i=1;i<=n;i++){
read(a[i]);
mp[a[i]]=0;
}
cnt=0;
for(map<int,int>::iterator it=mp.begin();it!=mp.end();it++) it->second=++cnt;
for(int i=1;i<=n;i++)print(mp[a[i]]);
哈夫曼树
当用 n 个结点(都做叶子结点且都有各自的权值)试图构建一棵树时,如果构建的这棵树的带权路径长度最小,称这棵树为“最优二叉树”,有时也叫“哈夫曼树”。
什么是这棵树的带权路径长度呢?
结点的带权路径长度:指的是从根结点到该结点之间的路径长度与该结点的权的乘积。
树的带权路径长度为树中所有叶子结点的带权路径长度之和。
根结点是不存储具体结点的,根据这两个儿子生成一个新的父结点,父节点的权值是这两个儿子权值之和
在构建哈弗曼树时,要使树的带权路径长度最小,只需要遵循一个原则,那就是:权重越大的结点离树根越近。
哈夫曼树主要解决这样一个问题:
-
有个元素,第个元素的出现概率为
-
设计一种二进制编码使得文本的期望编码长度最短
-
形式化而言,就是最小化
-
为了能从编码还原回字母,一个显然的要求是不能有一个字母的编码是另一个的前缀
-
换句话说,字符之间的编码要形成一棵二叉树
-
但什么样的编码能使得期望长度最短呢?
观察:我们相当于要最小化
一个显然的结论是当我们确定二叉树的形态后,频率最下的两个字母必然放在树的最底层
不妨把它们调到同一个父亲底下
因此,在实现中我们需要每次找到频率最小的两个元素,把他们删除并且合并成一个新元素,不断向上建立出哈夫曼树
过程:使用一个堆来维护当前仍存在的元素
树状数组
就是用数组来模拟树形结构呗。那么衍生出一个问题,为什么不直接建树?答案是没必要,造就了空间少,常数小的好处
考虑这样一个问题:
有一种信息,可以将两个信息合并成一个信息
有1~n一共n个位置,每个位置有一些信息
两种操作:
- 在某个位置添加一个信息
- 求1~k中所有信息合并的结果
这是一道典型的树状数组题目,虽然说树状数组能做的事情线段树都能做,但是树状数组的常数小啊,而且可能在我不知道的一些东西中,有一些是线段树做不了的呢?
- s&(-s)的含义是二进制下最小的一个
code:
int n;
int a[1005],c[1005];//对应原数组和树状数组
int lowbit(int x){
return x&(-x);
}
void updata(int i,int k){ //在i位置加上k
while(i<=n){
c[i]+=k;
i+=lowbit(i);
}
}
int getsum(int i){ //求A[1~i]的和
int res=0;
while(i>0){
res+=c[i];
i-=lowbit(i);
}
return res;
}
int main(){
...;
memset(a,0,sizeof(a));//全部都要初始化
memset(c,0,sizeof(c));
for(int i=1;i<=n;i++){
cin>>a[i];
updata(i,a[i]); //输入就相当于赋初值
}
int sum = getsum(y) - getsum(x-1);//x~y区间和也就等于1~y区间和减去1~(x-1)区间和
updata(x,-y); //减去操作,即为加上相反数
}
这就是最简单的点更新区间求和了。
一些简单的案例:
- 单点修改,区间求和
- 单点修改,求区间
- 区间整体加x,求单点值(用差分数组)
- 单点变大。求前缀(对x取max,这个信息是可合并的,改成修改就不可以了)
code:
区间修改,求单点值
int n,m;
int a[5005],c[5005];//对应原数组和树状数组(树状数组是差分数组)
int lowbit(int x){
return x&(-x);
}
void updata(int i,int k){ //在i位置加上k
while(i<=n){
c[i]+=k;
i+=lowbit(i);
}
}
int getsum(int i){ //求c[1~i]的和,即A[i]的值
int res=0;
while(i>0){
res+=c[i];
i-=lowbit(i);
}
}
for(int i=1;i<=n;i++){
cin>>a[i];
updata(i,a[i-1]-a[i]);//赋初值相当于更新
}
//[x,y]区间加上k
updata(x,k); //a[x]-a[x-1]增加k
updata(y+1,-k); //a[y+1]-a[y]减少k
//查询i位置上的值
sum=getsum(i);
区间修改,求区间值
我觉得这个用线段树最好1了,我就不展示了...
例1:树状数组求逆序对
我们设表示这个字有没有在序列中出现过,如果出现过我们就把他设为1,然后我们只要找到所有比小的数,剩下的自然都是比大的。
#include <bits/stdc++.h>
using namespace std;
int a[10000],c[10000];
int n;
int lowbit(int x){
return x&(-x);
}
void updata(int i,int k){
while(i<=n){
c[i]+=k;
i+=lowbit(i);
}
}
int getsum(int x){
int res=0;
while(x>0){
res+=c[x];
x-=lowbit(x);
}
return res;
}
int ans;
int main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
updata(a[i],1);
ans+=i-getsum(a[i]);
}
cout<<ans<<endl;
return 0;
}
例2:
-
给你一个数组,你每次交换两个相邻元素
-
目标是把数组变成单峰的
-
求最少操作次数
我们把小草从小到大排序,我们看看每棵小草左边有几个比他大的,右边有几个比他大的,往少的那边移。
code:
#include<iostream>
#include<cstdio>
#define ll long long
using namespace std;
const int maxn = 1e6 + 50;
ll z[maxn]={0};
ll y[maxn]={0};
int n;
ll a[maxn];
ll lowbit(ll x){
return x&(-x);
}
ll query(ll i,ll t[]){
ll ans = 0;
while(i){
ans += t[i];
i-=lowbit(i);
}
return ans;
}
void add(ll i,ll x,ll t[]){
while(i < maxn){
t[i]+=x;
i+=lowbit(i);
}
}
ll num1[maxn];
ll num2[maxn];
void sol(){
for(int i = n-1;i >= 0;--i){
num1[i] = query(maxn-a[i]-1,y);
add(maxn-a[i],1,y);
}
ll ans = 0;
for(int i = 0;i < n;++i){
num2[i] = query(maxn-a[i]-1,z);
add(maxn - a[i],1,z);
}
for(int i = 0;i < n;++i) ans+=min(num1[i],num2[i]);
cout<<ans<<endl;
}
int main(){
cin>>n;
for(int i = 0;i < n;++i) scanf("%lld",&a[i]);
sol();
}
额...树状数组就这样结束吧,毕竟还是把重心放在线段树上。
线段树我们单独再讲,毕竟线段树是非常重要的。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?