【笔记】数据结构

》》b站视频链接-Day6《《

》》b站视频链接-Day12《《

OP

\

后进先出

PS:对空栈 .pop 有可能导致RTE,所以 .pop 前应该确定一下栈中有元素;

单调栈

题目来源:洛谷P5788

题目描述

给出项数为 n 的整数数列 a 1 … n a_{1…n} a1n
定义函数 f(i) 代表数列中第 i 个元素之后第一个大于 a i a_i ai的元素的下标,即 f ( i ) = min ⁡ i < j ≤ n , a j > a i { j } f(i)=\min_{i<j\leq n, a_j > a_i} \{j\} f(i)=mini<jn,aj>ai{j}

若不存在,则 f(i)=0
试求出 f ( 1 … n ) f(1\dots n) f(1n)

我们即从后向前维持一个向栈底递增的单调栈,并对每个元素按要求入栈;

具体来说,对于样例

5
1 4 2 3 5

处理过程中,栈的情况可以如图表示:
在这里插入图片描述
注:为方便理解,图中栈存储的是 ai ,而答案要求的是 i ;

代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int a[3000006],ans[3000006];
int main()
{
    int n;
    scanf("%d",&n);
    for(int i=1;i<=n;i++)scanf("%d",&a[i]);
    stack<int>stk;
    for(int i=n;i>=1;i--)
    {
        while(stk.size()&&a[stk.top()]<=a[i])stk.pop();//只要栈顶元素比此元素小,就弹出
        if(!stk.size())
        {
            ans[i]=0;//如果栈空,则此元素右侧没有比此元素大的元素
        }
        else
        {
            ans[i]=stk.top();//此时栈顶为此元素右侧第一个比此元素大的元素
        }
        stk.push(i);//此元素入栈
    }
    for(int i=1;i<=n;i++)printf("%d ",ans[i]);
    return 0;
}

时间复杂度 O(n) ,因为每一个元素仅有一次入栈机会,也仅有一次出栈机会;

队列

先进先出

单调队列

题目来源:洛谷P1886

题目描述

有一个长为 n 的序列 a,以及一个大小为 k 的窗口。现在这个从左边开始向右滑动,每次滑动一个单位,求出每次滑动后窗口中的最大值和最小值。

以寻找区间最大值为例,我们用双端队列维护一个向队尾递减的单调队列,并将元素按要求从队尾入队;

具体来说,对于样例

8 3
1 3 -1 -3 5 3 6 7

求区间最大值时,队列情况可以如下图表示:
在这里插入图片描述

代码
#include<bits/stdc++.h>
using namespace std;
int n,k,i,a[1000006]={0},ans[1000006]={0};
deque<int>dq;
int main()
{
    cin>>n>>k;
    for(i=1;i<=n;i++)scanf("%d",&a[i]);
    for(i=1;i<=k;i++)//构造初始单调队列 //获取最小值时,维护向队尾递增的单调队列
    {
        while(dq.size()&&a[dq.back()]>a[i])dq.pop_back();
        dq.push_back(i);
    }
    ans[0]=a[dq.front()];
    for(;i<=n;i++)
    {
        while(dq.size()&&i-dq.front()>=k)dq.pop_front();//将越界元素清出队列
        while(dq.size()&&a[dq.back()]>a[i])dq.pop_back();//只要队尾元素比此元素大,就出队
        dq.push_back(i);//元素入队
        ans[i-k]=a[dq.front()];//队首为区间内最小元素
    }
    for(i=0;i<=n-k;i++)printf("%d%c",ans[i],(i==n-k)?'\n':' ');
    //----//
    dq.clear();
    for(i=1;i<=k;i++)
    {
        while(dq.size()&&a[dq.back()]<a[i])dq.pop_back();
        dq.push_back(i);
    }
    ans[0]=a[dq.front()];
    for(;i<=n;i++)
    {
        while(dq.size()&&i-dq.front()>=k)dq.pop_front();
        while(dq.size()&&a[dq.back()]<a[i])dq.pop_back();
        dq.push_back(i);
        ans[i-k]=a[dq.front()];
    }
    for(i=0;i<=n-k;i++)printf("%d%c",ans[i],(i==n-k)?'\n':' ');
    return 0;
}

时间复杂度 O(n) ,同样,在一次遍历中,每个元素仅有一次入队与出队机会;

并查集

用来维护不相交集合的数据结构~

	//1. 并查集的存储
    int fa[SIZE];
    //2. 并查集的初始化
    //初始每个元素都构成一个独立的集合
    for (int i = 1; i <= SIZE; i++)
        fa[i] = i;
    //3. 查询操作
    int find(int x)
    {
        if (x == fa[x])
            return x;
        return fa[x] = find(fa[x]);
    }
    //4. 合并操作
    void merge(int a, int b)
    {
        fa[find(a)] = find(b);
    }
扩展域并查集

题目来源:洛谷P2024 食物链

在此题中,若 b 与 a + n 被划入同一集合,即可代表 b 被 a 吃;
若 b 与 a 划入同一集合,即代表 a 与 b 为同类;

代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int fa[150004];
int find(int x)
{
    if(fa[x]==x)return x;
    else return find(fa[x]);
}
int main()
{
    int n,k,ans=0,o,a,b,i;
    cin>>n>>k;
    for(i=1;i<=3*n;i++)fa[i]=i;//初始化
    while(k--)
    {
        scanf("%d%d%d",&o,&a,&b);
        if(a>n||b>n){ans++;continue;}
        if(o==1)
        {
            if(find(a+n)==find(b)||find(a+2*n)==find(b))ans++;//如果非同类,假话++
            else//否则构建同类
            {
                fa[find(a)]=find(b);
                fa[find(a+n)]=find(b+n);
                fa[find(a+2*n)]=find(b+2*n);
            }
        }
        else if(o==2)
        {
            if(find(a)==find(b)||find(a)==find(b+n))ans++;//如果是同类,或者b吃a,假话++
            else//否则构建捕食关系
            {
                fa[find(a+n)]=find(b);
                fa[find(a+2*n)]=find(b+n);
                fa[find(a)]=find(b+2*n);
            }
        }
    }
    cout<<ans;
}
删点并查集

例题:HDUOJ.2473 Junk-Mail Filter

题目大意

共对n个元素进行m次操作,操作共有两种,M a b为定义a与b为同一类元素,S a定义为将a节点与其他节点分离;
输出共有几类元素;

我们可以将元素用代表元代替:
每一次该元素被分离是,就给它定义一个新的代表元,并定义该代表元的根为其自己;
同时,对于旧的树状结构,下级元素的根依然为该元素的旧代表元,所以不会影响旧关系的传递;

这样就可以将不同的关系之间分离开,不会互相干扰;

代码
#include <bits/stdc++.h>
using namespace std;

const int N = 1e5 + 5;
int rep[N];
int fa[1100006];//Nmax+Mmax
int fg[1100006];
int find(int x)
{
    if(fa[x]==x)return x;
    else return fa[x]=find(fa[x]);
}
int main()
{
    int n,m,a,b,faa,fbb,ccs=1,crep;
    char o;
    while(~scanf("%d%d",&n,&m)&&(n||m))
    {
        crep=n+1;
        for(int i=0;i<n;i++)//初始化
        {
            rep[i]=i;
            fa[i]=i;
        }
        for(int i=0;i<m;i++)
        {
            cin>>o;
            if(o=='M')
            {
                scanf("%d%d",&a,&b);
                faa=find(rep[a]);
                fbb=find(rep[b]);
                if(faa!=fbb)//操作代表元
                {
                    fa[faa]=fbb;
                }  
            }
            else if(o=='S')
            {
                scanf("%d",&a);
                rep[a]=crep;//更新代表元
                fa[crep]=crep;
                crep++;
            }
        }
        int ans=0;
        memset(fg,0,sizeof fg);
        for(int i=0;i<n;i++)
        {
            if(!fg[find(rep[i])])//fg标记该集合是否统计过
            {
                fg[find(rep[i])]=1;
                ans++;
            }
        }
        printf("Case #%d: %d\n",ccs++,ans);
    }
    return 0;
}

STL

这段文字参考了这篇文章

STL封装了优先队列priority_queue

定义:priority_queue<Type, Container, Functional>

Type 就是数据类型,Container 就是容器类型(Container必须是用数组实现的容器,比如vector,deque等等,但不能用 list。STL里面默认用的是vector),Functional 就是比较的方式,当需要用自定义的数据类型时才需要传入这三个参数,使用基本数据类型时,只需要传入数据类型,默认是大根堆;

基本操作

top 访问队头元素
empty 队列是否为空
size 返回队列内元素个数
push 插入元素到队尾 (并排序)
emplace 原地构造一个元素并插入队列
pop 弹出队头元素
swap 交换内容

//升序队列
priority_queue <int,vector<int>,greater<int> > q;
//降序队列
priority_queue <int,vector<int>,less<int> >q;
//默认大根堆
priority_queue<int> q; 

对于自定义数据类型:

//方法1
struct tmp1 //运算符重载<
{
    int x;
    tmp1(int a) {x = a;}
    bool operator<(const tmp1& a) const
    {
        return x < a.x; //大顶堆
    }
};

//方法2
struct tmp2 //重写仿函数
{
    bool operator() (tmp1 a, tmp1 b) 
    {
        return a.x < b.x; //大顶堆
    }
};

int main() 
{
    tmp1 a(1);
    tmp1 b(2);
    tmp1 c(3);
    priority_queue<tmp1> d;
    d.push(b);
    d.push(c);
    d.push(a);
    while (!d.empty()) 
    {
        cout << d.top().x << '\n';
        d.pop();
    }
    cout << endl;

    priority_queue<tmp1, vector<tmp1>, tmp2> f;
    f.push(c);
    f.push(b);
    f.push(a);
    while (!f.empty()) 
    {
        cout << f.top().x << '\n';
        f.pop();
    }
}

代码实现

这段文字参考了这篇文章

注1:由于堆是一棵完全二叉树,对于以1号节点为根的堆,节点 i 的父节点是 i/2,左子节点是 i*2,右子节点是 i*2+1;
对于以0号节点为根的二叉树,节点 i 的父节点是 (i-1)/2,左子节点是 i*2+1,右子节点是 i*2+2;
注2:满足任意结点的值都小于其子树中结点的值即为小根堆,大根堆反之;

接下来以以0为根的小根堆为例说明;

堆的向下调整

一般用于堆顶元素删除;

对于一个元素向下调整的过程:
1.获取该元素的两个儿子元素,并将较小的元素标记;
2.如果被标记元素小于该元素,则不满足小根堆的定义,对换此元素与被标记元素,并继续此循环;
否则,此处符合小根堆的定义,不需要调整,对于此元素的向下调整结束;

代码
void shiftDown(int* arr, int n, int cur)
{
	int child = 2 * cur + 1;
	while (child < n)//child默认为左子节点
	{
		if (child + 1 < n &&arr[child + 1]<arr[child])//child保存较小子节点
		{	
			++child;
		}
		if (arr[child] < arr[cur])
		{
			swap(arr[child], arr[cur]);
			cur = child;//对换后更新当前节点
			child = 2 * cur + 1;//更新子节点
		}
		else
		{
			break;
		}
	}
}

堆的向上调整

一般用于堆的创建;

对于一个元素向上调整的过程:
1.比较此元素与其父元素的大小
2.若此元素小于父元素,则不满足小根堆的定义,对换此元素与父元素,继续此循环;
否则满足定义,此元素位置合法,停止向上调整;

代码
void shiftUp(int* arr, int n, int cur)
{
	int parent = (cur - 1) / 2;
	while (parent >= 0 && arr[cur] < arr[parent])
	{
		swap(arr[cur], arr[parent]);
		cur = parent;
		parent = (cur - 1) / 2;
	}
}

堆的元素添加

向堆中添加一个元素时,先把这个元素添加至堆尾,并将其向上调整;

堆的元素删除

删除已知位置的指定元素可以按照以下策略:

1.将此元素与堆尾元素对换;
2.删除堆尾元素(被对换来的待删除元素);
3.将原位置的元素(被对换来的队尾元素)与其父元素比较:
如果父元素存在且该元素小于父元素,则向上调整;
否则,向下调整;

堆的创建

指将一个无序数组转化为堆的过程;

1.从倒数第二层开始,向上层序遍历;
2.对于每一个元素,仅对其向下调整至合适位置(可能无需调整),不考虑向上调整;

树状数组

首先定义一下 l o w b i t ( ) lowbit() lowbit() 函数, l o w b i t ( i ) lowbit(i) lowbit(i) 代表最低位的1所代表二进制位的值;
例如 l o w b i t ( 6 ) = 2 , l o w b i t ( 4 ) = 4 , l o w b i t ( 7 ) = 7 lowbit(6)=2,lowbit(4)=4,lowbit(7)=7 lowbit(6)=2,lowbit(4)=4,lowbit(7)=7
其代码实现:

int lowbit(int x){return x&-x;}

l o w b i t ( ) lowbit() lowbit() 函数有两条主要性质:

  1. l o w b i t ( x ± l o w b i t ( x ) ) ⩾ 2 ⋅ l o w b i t ( x ) , ( x − l o w b i t ( x ) ≠ 0 ) lowbit(x\pm lowbit(x))\geqslant2\cdotp lowbit(x),(x-lowbit(x)\not=0) lowbit(x±lowbit(x))2lowbit(x),(xlowbit(x)=0)
  2. 如果 x − l o w b i t ( x ) = k , ( k ≠ 0 ) x-lowbit(x)=k,(k\not=0) xlowbit(x)=k,(k=0)
    ∀ y ∈ [ x + 1 , x + l o w b i t ( x ) − 1 ] \forall y\in[x+1,x+lowbit(x)-1] y[x+1,x+lowbit(x)1] ,有 y − l o w b i t ( y ) > k y-lowbit(y)>k ylowbit(y)>k
    y = x + l o w b i t ( x ) y=x+lowbit(x) y=x+lowbit(x) 时,有 y − l o w b i t ( y ) ⩽ k y-lowbit(y)\leqslant k ylowbit(y)k

树状数组定义有,对于树状数组元素 c x c_x cx ,有 c x = ∑ i = x − l o w b i t ( x ) + 1 x a i c_x=\sum_{i=x-lowbit(x)+1}^xa_i cx=i=xlowbit(x)+1xai ,也就是说,树状数组只存储该位及该位之前共 l o w b i t ( i ) lowbit(i) lowbit(i) 位;

对于树状数组 c ,存储数组 a 时,可有如下示意:( ( i , j ) = 1 (i,j)=1 (i,j)=1 代表 a j a_j aj 参与了 c i c_i ci 的构成)
在这里插入图片描述

区间查询

在上图中我们可以发现 ∑ i = 0 7 a i = c 7 + c 6 + c 4 \sum_{i=0}^7a_i=c_7+c_6+c_4 i=07ai=c7+c6+c4 ,其中又有 7 − l o w b i t ( 7 ) = 6 , 6 − l o w b i t ( 6 ) = 4 , 4 − l o w b i t ( 4 ) = 0 7-lowbit(7)=6,6-lowbit(6)=4,4-lowbit(4)=0 7lowbit(7)=6,6lowbit(6)=4,4lowbit(4)=0

我们可以依照此规律快速求出 a 数组的给定位的前缀和和给定区间的区间和;

int sum(int x)
{
	int ans=0;
	for(;x;x-=lowbit(x))ans+=c[x];
	return ans;
}
单点修改

在上图中我们发现,含有 a 3 a_3 a3 的项有 c 3 , c 4 , c 8 c_3,c_4,c_8 c3,c4,c8 ,其中又有 c + l o w b i t ( 3 ) = 4 , 4 + l o w b i t ( 4 ) = 8 c+lowbit(3)=4,4+lowbit(4)=8 c+lowbit(3)=4,4+lowbit(4)=8

我们可以依照此规律快速进行单点修改;

void add(int x,int v)//a[x]+=v;
{
	for(;x<=N;x+=lowbit(x))c[x]+=v;
}
其他

对于区间修改和单点查询问题,我们可以将树状数组中存储的区间和该位区间的差分和,其余同理;

可以用前缀处理的运算,均可以用树状数组存储(如加法,异或) ;

二维树状数组
int c[1025][1025], X = 1024,Y=1024;

int lowbit(int x) { return x & -x; }

int sum(int x, int y)
{
    int ans = 0;
    for (int i = x; i > 0; i -= lowbit(i))
        for (int j = y; j > 0; j -= lowbit(j))
            ans += c[i][j];
    return ans;
}

void add(int x, int y, int v) //a[x][y]+=v;
{
    for (int i = x; i <= X; i += lowbit(i))
        for (int j = y; j <= Y; j += lowbit(j))
            c[i][j] += v;
}

求任意区间和( ∑ x = l r ∑ y = b t a [ x ] [ y ] \sum_{x=l}^r\sum_{y=b}^ta[x][y] x=lry=bta[x][y])时,类似于二维前缀和,可以用sum(r, t) - sum(r, b - 1) + sum(l - 1, b - 1) - sum(l - 1, t)表示;

线段树

线段树是一种基于分治结构的二叉树;

对于一个8个元素的数组 a ,在线段树 c 中可以表示为:

我们若将根节点标记为 1,则对于节点 i ,其左右孩子下标分别为 i ≪ 1 , ( i ≪ 1 ) + 1 i\ll1,(i\ll1)+1 i1,(i1)+1 ,其父节点的下标为 i ≫ 1 i\gg1 i1 ;

下面以sum线段树为例;

存储方式
const int N = 1e5;
struct
{
    int l, r;//l,r为节点的左右界
    ll sum, lt;//sum为节点值,lt为懒标记
} segt[N << 2 | 1];
void pushdown(int p)//对懒标记进行下传
{
    if (segt[p].lt)
    {
        segt[p << 1].sum += segt[p].lt * (segt[p << 1].r - segt[p << 1].l + 1);
        segt[p << 1 | 1].sum += segt[p].lt * (segt[p << 1 | 1].r - segt[p << 1 | 1].l + 1);
        segt[p << 1].lt += segt[p].lt;
        segt[p << 1 | 1].lt += segt[p].lt;
        segt[p].lt = 0;
    }
}
建树

调用build(1,1,n);

void build(int p, int cl, int cr)
{
    segt[p].l = cl, segt[p].r = cr, segt[p].lt = 0, segt[p].sum = 0;
    if (cl == cr)
    {
        scanf("%lld", &segt[p].sum);
        return;
    }
    int mid = cl + cr >> 1;
    build(p << 1, cl, mid);
    build(p << 1 | 1, mid + 1, cr);
    segt[p].sum = segt[p << 1].sum + segt[p << 1 | 1].sum;
}
区间修改

[ c l , c r ] [cl,cr] [cl,cr]区间进行+d操作,调用icg(1,cl,cr,d);

void icg(int p, int cl, int cr, ll d)
{
    if (cl <= segt[p].l && segt[p].r <= cr)
    {
        segt[p].sum += d * (segt[p].r - segt[p].l + 1);
        segt[p].lt += d;
        return;
    }
    pushdown(p);
    int mid = segt[p].r + segt[p].l >> 1;
    if (cl <= mid)
        icg(p << 1, cl, cr, d);
    if (cr >= mid + 1)
        icg(p << 1 | 1, cl, cr, d);
    segt[p].sum = segt[p << 1].sum + segt[p << 1 | 1].sum;
}
区间查询

[ c l , c r ] [cl,cr] [cl,cr]区间查询,调用ick(1,cl,cr);

ll ick(int p, int cl, int cr)
{
    if (cl <= segt[p].l && segt[p].r <= cr)
        return segt[p].sum;
    pushdown(p);
    int mid = segt[p].r + segt[p].l >> 1;
    ll val = 0;
    if (cl <= mid)
        val += ick(p << 1, cl, cr);
    if (cr >= mid + 1)
        val += ick(p << 1 | 1, cl, cr);
    return val;
}
单点修改

实际上,已经有区间更新方法的情况下,再探讨单点更新是无必要的;

a [ x ] = v a[x]=v a[x]=v调用pcg(1,x,v);

ll pcg(int p, int x, ll v)//兼容懒标记
{
    if (segt[p].l == segt[p].r)
    {
        ll del = v - segt[p].sum;
        segt[p].sum += v;
        return del;
    }
    int mid = segt[p].l + segt[p].r >> 1;
    ll del = 0;
    if (x <= mid)
        del = pcg(p << 1, x, v);
    else
        del = pcg(p << 1 | 1, x, v);
    segt[p].sum += del;
    return del;
}

void pcg(int p, int x, ll v)//不兼容懒标记
{
    if (segt[p].l == segt[p].r)
    {
        segt[p].sum = v;
        return;
    }
    int mid = segt[p].l + segt[p].r >> 1;
    if (x <= mid)
        pcg(p << 1, x, v);
    else
        pcg(p << 1 | 1, x, v);
    segt[p].sum = segt[p << 1].sum + segt[p << 1 | 1].sum;
}

ED

\

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值