链表
当平时使用数组时,有时会遇到一些问题:
- 在删除一个数据时,为了保证数据的连续性,
- 需要将删除点右边的数据、全部往左移一格。
- 在插入一个数据时,为了保证数据的连续性,
- 需要将插入点右边的数据、全部往右移一格。
- 每次移动的时间复杂度为O(N),如果元素较多时会时间超限。
链表一个位置包括了自己位置上的值和下一个值所在的位置。
这样链表就可以实现O(1)的插入/删除,同时保证数据的连续性。
一般使用数组模拟。v[i]表示当前位置的数字,nex[i]指向下个位置的数组坐标。
- 在使用链表时可能需要知道上个位置在哪里,于是有了双向链表。
- 双向链表多使用一个pre[i]表示上个位置的数组坐标。
栈和队列
栈在一端的出口同时控制加入和踢出,另一端的出口为封闭的, 满足后进先出原则。
队列在尾控制加入,在头控制踢出,满足先进先出原则。
- 栈:用一个数组S代表栈,l为元素数量,加入元素x。
- S[++l]=x;弹出栈顶元素到x;x=S[l--];
- 队列:用一个数组Q代表队列,l为队列头,r为队列尾,加入元素x。
- Q[++r]=x;弹出队头元素到x;x=Q[l++]。
单调栈和单调队列
单调栈除了满足普通栈的性质外,还满足从栈顶到栈底的元素具有严格的单调性。
假设我们维护的是一个单调递减的栈: (找到合适的位置)
• 若进栈的元素为x,栈顶元素为S[l]。
• 那么当x>=S[l]时弹出栈顶元素,直到x<S[l]为止,压入元素x。
单调队列除了满足普通队列的性质外,还满足从队列头到队列尾的元素具有严格的单调性。
假设我们维护的是一个单调递减的队列:
• 若进队的元素为x,队尾元素为Q[r]。
• 那么当x>=Q[r]时弹出队尾元素,直到x<Q[r]为止,加入元素x。
例题1 - 洛谷P1886 滑动窗口 (单调队列)
例题2 - 洛谷P1725 琪露诺(单调队列优化dp)
- 格子编号为0到N,每次移动可以到[i+L,i+R]中的一格,
- 然后收集格子上的冰冻值A[i],最后一次移动时只要大于N 就算到达。
- 求经过的格子最大冰冻值和。
SOLUTION
代码实现
#include<iostream>
#include<cstdio>
#include<iomanip>
#include<cstring>
#include<cmath>
using namespace std;
int n,l,r,a[200005],f[400005];
int q[200005],head=1,tail=0;
int main(){
cin>>n>>l>>r;
for(int i=0;i<=n;++i) scanf("%d",&a[i]);
for(int i=n+l;i>=l;--i){
//反向算,方便确定来源,dp填表法
while(f[q[tail]]<=f[i]&&head<=tail) tail--;
//↑↑↑单调队列找插入位置,栈底的元素大
q[++tail]=i; //找到位置,把i加进单调队列
f[i-l]=max(f[i-l],f[q[head]]+a[i-l]);
//区间最优选择进入dp比较
if(q[head]==i+r&&head<=tail) head++;
}
cout<<f[0]<<endl;
return 0;
}
例题3 - 洛谷P1419 寻找段落(前缀和+二分答案)
- 给定一个长度为n的序列a[i],定义a[i]为第i个元素的价值。
- 现在需要找出序列中最有价值的“段落”。段落的定义是长度在[S,T]之间的连续序列。
- 最有价值段落是指平均值最大的段落,段落的平均值=段落总价值/段落长度。
SOLUTION
#include<cstdio>
#include<cstring>
#include<iostream>
using namespace std;
const int maxn = 100000+10;
int A[maxn],n,s,t;
double sum[maxn];
int q[maxn],front,rear;
bool can(double x) {
sum[0]=0;
for(int i=1;i<=n;i++) //处理前缀和
sum[i] = sum[i-1]+A[i]-x;
front=1; rear=0; //初始化单调队列
for (int i = 1; i <= n; i++) {
if (i >= s) { //足够s个
while (rear >= front && sum[i - s] < sum[q[rear]])
rear--; //↑↑↑ front为在i-t..i-s区间内的最小值
q[++rear] = i - s; //入队区间起点-1
}
if (front <= rear && q[front] < i - t)
front++; //维护区间i-t
if (front <= rear && sum[i] - sum[q[front]] >= 0)
return true; //有大于0的区间和说明最大平均值还可以更大
}
return false;
}
int main() {
scanf("%d%d%d",&n,&s,&t);
double L=0,R=0;
for(int i=1;i<=n;i++) {
scanf("%d",&A[i]);
R=max(R,(double)A[i]); //预处理找出max
}
while(R-L>1e-4) { //二分答案
double M=L+(R-L)/2;
if(can(M)) L=M;
else R=M;
}
printf("%.3lf\n",L);
return 0;
}
例题4 - 洛谷P1823 音乐会的等待(单调栈维护)
- N个人正在排队进入一个音乐会。人们想在队伍里寻找自己的熟人。
- 队列中任意两个人A和B,如果他们相邻或他们之间没有人比A或B高,
- 那么他们是可以互相看得见的。写一个程序计算出有多少对人可以互相看见。
SOLUTION
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <vector>
#define ll long long
using namespace std;
ll n,ans;
ll z[500010],now,tmp,i;
inline int read(){ //快读
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
return x*f;
}
int main(){
scanf("%lld",&n);
tmp=read(); z[1]=tmp; now=1;
for(i=1;i<n;i++){
tmp=read();
if(tmp<z[now]) { ans++; z[++now]=tmp; }
else{
int l=1,r=now,m;
while(l<r){
m=(l+r)>>1; //取mid
if(r==l+1) m=r;
if(z[m]>tmp) l=m;
else r=m-1;
}
ans+=now-l+1; //增加的人数
while(now>0&&z[now]<tmp) now--;
z[++now]=tmp;
}
}
printf("%lld",ans);
return 0;
}