》》b站视频链接-Day6《《
》》b站视频链接-Day12《《
目录:
OP
\
栈
后进先出
PS:对空栈 .pop 有可能导致RTE,所以 .pop 前应该确定一下栈中有元素;
单调栈
题目来源:洛谷P5788
题目描述
给出项数为 n 的整数数列 a 1 … n a_{1…n} a1…n
定义函数 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<j≤n,aj>ai{j}若不存在,则 f(i)=0
试求出 f ( 1 … n ) f(1\dots n) f(1…n)
我们即从后向前维持一个向栈底递增的单调栈,并对每个元素按要求入栈;
具体来说,对于样例
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() 函数有两条主要性质:
- 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))⩾2⋅lowbit(x),(x−lowbit(x)=0)
- 如果
x
−
l
o
w
b
i
t
(
x
)
=
k
,
(
k
≠
0
)
x-lowbit(x)=k,(k\not=0)
x−lowbit(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 y−lowbit(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 y−lowbit(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=x−lowbit(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 7−lowbit(7)=6,6−lowbit(6)=4,4−lowbit(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=lr∑y=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 i≪1,(i≪1)+1 ,其父节点的下标为 i ≫ 1 i\gg1 i≫1 ;
下面以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
\