10月1号D1数据结构(PPT2)

10月1号D1数据结构(PPT2)

接下来我们来到树形树状结构...

priority_queue这个才是神!!!!

默认大根堆...

例一:

  • 插入一个元素

  • 删除一个元素

  • 询问最小值

  • 怎么用priority_queue

很简单,我们可以维护两个小根堆,一个存储插入元素,一个存储删除元素。

查询时,如果两个top相等就两个都不断push,直到不一样,如果不相等就直接输出。

这就是换个角度想问题,我们假装他被删了,但在序列中其实没有删,一定要养成这种思想!!!

例二:

  • n个任务,每个任务有需要的时间ti和截止时间limi

  • 求至多能完成多少任务

这是一道贪心的题目,很容易想到,我们按照limi从小到大排序,如果可以做就直接做,如果不能做就搞死已做完中需要时间最长的任务然后换成它,这样一定是更优的。

例四:

  • 定义一个区间(l,r)的长度为rl
  • 给定数轴上n个区间,选择其中的k个点,求最大交集。

假如我们已经确定了最终区间的左端点L,那么我们选择的区间一定是左端点在L左边,且右端点最右的K个点。所以我们将所有区间按左端点排序,用小根堆维护左端点在左边,且右端点最大的K个点。每次用第K大值更新答案即可。

维护以一个大小不变为k的小根堆,每来一个数看一下能不能放进堆(x>=que.top()),能来的话就弹掉栈顶放进来。

#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 个结点(都做叶子结点且都有各自的权值)试图构建一棵树时,如果构建的这棵树的带权路径长度最小,称这棵树为“最优二叉树”,有时也叫“哈夫曼树”。

什么是这棵树的带权路径长度呢?
结点的带权路径长度:指的是从根结点到该结点之间的路径长度与该结点的权的乘积。
树的带权路径长度为树中所有叶子结点的带权路径长度之和。
根结点是不存储具体结点的,根据这两个儿子生成一个新的父结点,父节点的权值是这两个儿子权值之和

在构建哈弗曼树时,要使树的带权路径长度最小,只需要遵循一个原则,那就是:权重越大的结点离树根越近。

哈夫曼树主要解决这样一个问题:

  • n个元素,第i个元素的出现概率为p[i]

  • 设计一种二进制编码使得文本的期望编码长度最短

  • 形式化而言,就是最小化p[i]len[i]

  • 为了能从编码还原回字母,一个显然的要求是不能有一个字母的编码是另一个的前缀

  • 换句话说,字符之间的编码要形成一棵二叉树

  • 但什么样的编码能使得期望长度最短呢?

观察:我们相当于要最小化p[i]dep[i]

一个显然的结论是当我们确定二叉树的形态后,频率最下的两个字母必然放在树的最底层

不妨把它们调到同一个父亲底下

因此,在实现中我们需要每次找到频率最小的两个元素,把他们删除并且合并成一个新元素,不断向上建立出哈夫曼树

过程:使用一个堆来维护当前仍存在的元素

树状数组

就是用数组来模拟树形结构呗。那么衍生出一个问题,为什么不直接建树?答案是没必要,造就了空间少,常数小的好处

考虑这样一个问题:
有一种信息,可以O(1)将两个信息合并成一个信息

有1~n一共n个位置,每个位置有一些信息

两种操作:

  • 在某个位置添加一个信息
  • 求1~k中所有信息合并的结果

这是一道典型的树状数组题目,虽然说树状数组能做的事情线段树都能做,但是树状数组的常数小啊,而且可能在我不知道的一些东西中,有一些是线段树做不了的呢?

  • s&(-s)的含义是二进制下最小的一个bit

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);    //减去操作,即为加上相反数
}

这就是最简单的点更新区间求和了。

一些简单的案例:

  • 单点修改,区间求和
  • 单点修改,求区间xor
  • 区间整体加x,求单点值(用差分数组)
  • 单点变大。求前缀max(对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:树状数组求逆序对

我们设a[i]表示i这个字有没有在序列中出现过,如果出现过我们就把他设为1,然后我们只要找到所有比i小的数,剩下的自然都是比i大的。

#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();
} 

额...树状数组就这样结束吧,毕竟还是把重心放在线段树上。

线段树我们单独再讲,毕竟线段树是非常重要的。

posted @   QLWybz  阅读(8)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?
点击右上角即可分享
微信分享提示