【全程NOIP计划】初级数据结构1
【全程NOIP计划】初级数据结构1
在线:每次询问之后,立马可以得到查询结果
离线:知道所有需要查询的值,然后一次输出查询结果
STL中set的用法
用迭代器
并查集
可以支持一些不相交集合的合并和查询
我们可以树形结构来组织数据
同一个集合的元素组成一棵树
因为集合没有交,所以最终构建得到的是一个森林
将树的根节点作为集合的代表元
开始的时候为n个孤立的点
查询一个点所属集合
int get(int x)
{
return fa[x]=(x==fa[x]? x:get(fa[x]));
}
这里做了一个路径压缩,使这颗树变得扁平,大大减小了查找的时间
合并
void merge(int x,int y)
{
fa[get(x)]=get(y);
}
如果是启发式合并的话,是把较小的树插到较大的树
用处
1.可以做最小生成树
也就是kruskal算法
2.还可以用来判环
P3958 奶酪
思路
实际上处理一个上界,处理一个下界,然后判断上面和下面能不能连通就可以了非常简单
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <string>
#include <cmath>
using namespace std;
struct node{
long long high,low;
long long x,y,z;
};
long double dist(node p,node q)
{
return (long double)sqrt((p.x-q.x)*(p.x-q.x)+(p.y-q.y)*(p.y-q.y)+(p.z-q.z)*(p.z-q.z));
}
//并查集
int f[100010];
int get(int x)
{
return f[x]=(x==f[x]? x:get(f[x]));
}
void merge(int x,int y)
{
f[get(x)]=get(y);
}
void init()
{
for(int i=1;i<=100000;i++)
f[i]=i;
}
//并查集
int T;
int main()
{
cin>>T;
while(T--)
{
init();
long long n,h,r;
cin>>n>>h>>r;
node a[1005];
for(int i=1;i<=n;i++)
cin>>a[i].x>>a[i].y>>a[i].z;
int highest[1005]={0};
int tot1=0;
int lowest[1005]={0};
int tot2=0;
for(int i=1;i<=n;i++)
{
a[i].high=a[i].z+r;
a[i].low= a[i].z-r;
if(a[i].high>=h)
{
++tot1;
highest[tot1]=i;
}
if(a[i].low<=0)
{
++tot2;
lowest[tot2]=i;
}
for(int j=i;j<=n;j++)
{
if(dist(a[i],a[j])<=2*r)
merge(i,j);
}
}
bool flag=false;
for(int i=1;i<=tot1;i++)
{
for(int j=1;j<=tot2;j++)
{
if(get(highest[i])==get(lowest[j]))
{
flag=true;
cout<<"Yes"<<endl;
break;
}
}
if(flag) break;
}
if(flag) continue;
else
cout<<"No"<<endl;
}
return 0;
}
P1197 星球大战
思路
倒序考虑,直接倒序加边,统计连通块就可以了
如果是正序的话要使用非常难的动态图问题
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int maxn=4e5;
struct data
{
int nxt,to;
}e[maxn<<1];
int head[maxn],tot;
void add(int u,int v)
{
e[++tot]=(data){head[u],v};
head[u]=tot;
}
int f[maxn],n,m,v[maxn],k,a[maxn],cnt,ans[maxn];
int find(int x)
{
return x==f[x]?x:f[x]=find(f[x]);
}
void dfs(int x)
{
v[x]=1;
for (int i=head[x];i;i=e[i].nxt)
{
if (!v[e[i].to])
{
int f1=find(x),f2=find(e[i].to);
if (f1!=f2) f[f1]=f2;
dfs(e[i].to);
}
}
}
int main()
{
scanf("%d%d",&n,&m);
for (int i=0;i<n;++i)
f[i]=i;
for (int i=0;i<m;++i)
{
int u,v;
scanf("%d%d",&u,&v);
add(u,v); add(v,u);
}
scanf("%d",&k);
for (int i=0;i<k;++i)
scanf("%d",&a[i]),v[a[i]]=1;
for (int i=0;i<n;++i)
{
if (!v[i])
{
cnt++;
dfs(i);
}
}
ans[k]=cnt;
memset(v,0,sizeof(v));
for (int i=0;i<k;++i) v[a[i]]=1;
for (int i=k-1;i>=0;--i)
{
v[a[i]]=0;
cnt++;
for (int j=head[a[i]];j;j=e[j].nxt)
{
if (!v[e[j].to])
{
int f1=find(a[i]),f2=find(e[j].to);
if (f1!=f2)
{
f[f2]=f1;
cnt--;
}
}
}
ans[i]=cnt;
}
for (int i=0;i<=k;++i)
printf("%d\n",ans[i]);
return 0;
}
P1396 营救
思路
求最大值最小的问题,果断二分答案
也可以开始的时候认为无边,然后从小到大依次加边,直到s和t相连
这里可以使用一个带权并查集
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <string>
#include <cstring>
#include <queue>
using namespace std;
struct rec{int x,y,z;}edge[500010];
int n,m,s,t;
int fa[1000010];
bool operator <(rec a,rec b)
{
return a.z<b.z;
}
void init()
{
for(int i=1;i<=1000000;i++)
fa[i]=i;
}
int get(int x)
{
return fa[x]=(x==fa[x])? x:get(fa[x]);
}
int main()
{
init();
cin>>n>>m>>s>>t;
for(int i=1;i<=m;i++)
cin>>edge[i].x>>edge[i].y>>edge[i].z;
sort(edge+1,edge+m+1);
for(int i=1;i<=m;i++)
{
int x=get(edge[i].x);
int y=get(edge[i].y);
if(x==y) continue;
fa[x]=y;
if(get(s)==get(t))
{
cout<<edge[i].z<<endl;
return 0;
}
}
return 0;
}
P1783 海滩防御
思路
实际上 可以二分,处理两个信号塔的距离,判断他们整个是否连通即可
类似于奶酪,也类似于营救,
还可以用最短路,然后找最短路里面一条最大路径
#include<iostream>
#include<cstdio>
#include<cmath>
#include<queue>
#include<cstring>
using namespace std;
const int maxn = 1005;
const double eps=1e-4;
int n,m;
int x[maxn],y[maxn];
int vis[maxn];
double dis[maxn][maxn];
queue<int> q;
int check(double mid)
{
memset(vis,0,sizeof(vis));
vis[0]=1;
q.push(0);
while(!q.empty())
{
int v=q.front();q.pop();
for(int i=1;i<=m+1;i++)
{
if(!vis[i])
{
if(i==m+1||v==0)
{
if(dis[v][i]<=mid)
vis[i]=1,q.push(i);
}
else if(dis[v][i]<=2*mid)
vis[i]=1,q.push(i);
}
}
}
return vis[m+1];
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
scanf("%d%d",&x[i],&y[i]);
dis[0][i]=dis[i][0]=x[i];
dis[m+1][i]=dis[i][m+1]=n-x[i];
}
dis[0][m+1]=(1<<30);
for(int i=1;i<=m;i++)
for(int j=i+1;j<=m;j++)
dis[i][j]=dis[j][i]=sqrt((x[i]-x[j])*(x[i]-x[j])+(y[i]-y[j])*(y[i]-y[j]));
double l=0,r=10005,ans,mid;
while(r-l>=eps)
{
mid=(l+r)/2;
if(check(mid)) r=mid,ans=mid;
else l=mid;
}
printf("%.2lf",ans);
return 0;
}
单调栈与单调队列
单调栈即满足单调性的栈结构,只在一端进行进出
单调队列是满足单调性的队列结构,通常队首只能出,队尾可以进,也可以出(这是deque)
如何维护?
push的时候把不满足单调性的点全部pop掉
要让所有比我高的人给赶走
单调栈的本质是单调队列的退化
单调栈可以求出每个数前方或者后方比它大或者小的第一个数,以及该区间内最大的或者最小值
可以看出,求单调栈任意前缀最大或者最小值可以直接前缀和处理
求每个数前方或者后方比它大或者比它小的第一个数的时候往往没有区间约束
因此单调栈和单调队列解决的问题往往不同
单调队列模板
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <string>
using namespace std;
int n,k;
int a[1000005],q[1000005];
int main()
{
cin>>n>>k;
for(int i=0;i<n;i++)
cin>>a[i];
int hh=0,tt=-1;
for(int i=0;i<n;i++)
{
if(hh<=tt&&q[hh]<i-k+1)
hh++;
while(hh<=tt&&a[q[tt]]>=a[i])
tt--;
q[++tt]=i;
if(i>=k-1)
cout<<a[q[hh]]<<" ";
}
cout<<endl;
hh=0,tt=-1;
for(int i=0;i<n;i++)
{
if(hh<=tt&&q[hh]<i-k+1)
hh++;
while(hh<=tt&&a[q[tt]]<=a[i])
tt--;
q[++tt]=i;
if(i>=k-1)
cout<<a[q[hh]]<<" ";
}
cout<<endl;
return 0;
}
单调栈模板
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <string>
#include <stack>
using namespace std;
const int maxn=3000005;
int n;
int a[maxn],f[maxn];
stack <int> s;
int main()
{
std::ios::sync_with_stdio(false);
cin>>n;
for(int i=1;i<=n;i++)
cin>>a[i];
for(int i=n;i>=1;i--)
{
while(s.size()&&a[s.top()]<=a[i])
s.pop();
f[i]=s.empty()? 0:s.top();
s.push(i);
}
for(int i=1;i<=n;i++)
cout<<f[i]<<" ";
cout<<'\n';
return 0;
}
P1318 积水面积
题目
给定一个非负整数表示每个宽度为1的柱子的高度图
计算次排列的柱子,下雨之后可以接多少雨水
思路
从左向右思考这个问题,我们要想,能不能找到一种东西来维护她存了多少雨水
用单调栈啊!
利用单调栈来处理就可以了
实际上也很简单
P1106 删数问题
题目
键盘输入一个高精度的整数N,不超过250位,去掉其中任意k个数字后剩下的数字按左右次序将组成一个新的非负整数,变成给定N和k,寻找一种方法使得到的数字组成的新数最小
思路
目标是首位尽可能地小,然后次位尽可能小
也就是说,尽可能地保证高位更小
我们要维护一个单调递增的单调栈
很简单
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <string>
#include <stack>
using namespace std;
stack <int> s;
char a[500];
int b[500];
int c[500],cnt=0;
int k;
int main()
{
scanf("%s",a+1);
cin>>k;
int lena=strlen(a+1);
for(int i=1;i<=lena;i++)
b[i]=(int)(a[i]-'0');
for(int i=1;i<=lena;i++)
{
while(b[i]<c[cnt]&&k>0)
{
c[cnt]=0;
cnt--;
k--;
}
cnt++;
c[cnt]=b[i];
}
bool flag=false;
while(k--)
{
c[cnt]=0;
cnt--;
}
for(int i=1;i<=cnt;i++)
{
if(cnt==1) cout<<c[i];
if(c[i]!=0) flag=true;
if(flag==false&&c[i]==0) continue;
cout<<c[i];
}
cout<<endl;
return 0;
}
P1823 Patrik 音乐会的等待
题目
一个队伍,如果两个人相邻或者两个人中间的人都比他们矮,那他们就可以互相看见对方,问你有多少对人可以相互看到?
思路
单调栈
尝试构造一个单调递减的单调栈,b可以看到比他高的第一个人,还可以看到所有比它矮的人
如果后面再来一个人,如果比b矮的话,b前面的人它全都看不到
每个节点只计算他左边看到的人
这样不会重复
然而需要处理很多细节
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <string>
#include <stack>
using namespace std;
const int maxn=500005;
int n;
int a[maxn];
long long ans=0;
stack <pair<int,int> > s;
int main()
{
cin>>n;
for(int i=0;i<n;i++)
{
int x;
cin>>x;
pair <int,int> p(x,1);
for( ;(s.size()&&s.top().first<=x);s.pop())
{
ans+=s.top().second;
if(s.top().first==x)
p.second+=s.top().second;
}
if(s.size()) ans++;
s.push(p);
}
cout<<ans<<endl;
return 0;
}
P3957 跳房子
思路
至少为k,实际上就是分数的最小值大于等于k,很明显,需要二分答案
在加一个单调队列
二分一个g
用动态规划和二分
\(f[i]=max(f[i-d-g] \space to \space f[i-d+g])\)
时间复杂度是\(O(n^2logn)\)的
我们可以使用单调队列优化
如何维护它的最值呢?
我们需要维护一个区间长度为2g的序列
现在的问题就变成了如何把右端点向右移一格(这个和单调栈相同)
和如何把左端点向右,然后我们要判断当前区间的最大值是不是被pop出去
而新的最大值就在下一个了
动态规划的复杂度就从\(O(n^2)\)变成了\(O(n)\)
总的时间复杂度就变成了\(O(nlogn)\)
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <string>
using namespace std;
const int maxn=500005;
int n,d,k;
int x[maxn],s[maxn];
long long ans=0;
int f[maxn],q[maxn];
bool check(int xl,int xr)
{
memset(f,-1,sizeof(f));
memset(q,0,sizeof(q));
f[0]=0;
int i,j=0;
int head=1,last=0;
for(i=1;i<=n;i++)
{
while(x[i]-x[j]>=xl&&j<i)//单调队列判断边界
{
if(f[j]!=-1)
{
while(head<=last&&f[q[last]]<=f[j])//保证队首的都比队尾的大
last--;
q[++last]=j;
}
j++;
}
while(head<=last&&x[i]-x[q[head]]>xr)
head++;
if(head<=last)
f[i]=f[q[head]]+s[i];
}
for(int i=1;i<=n;i++)
if(f[i]>=k)
return true;
return false;
}
int main()
{
cin>>n>>d>>k;
long long sum=0;
for(int i=1;i<=n;i++)
{
cin>>x[i]>>s[i];
if(sum!=-717&&s[i]>0)
sum+=s[i];
if(sum>=k)
sum=-717;
}
if(sum!=-717)
{
cout<<-1<<'\n';
return 0;
}
int l=0,r=x[n];
while(l<=r)
{
int mid=(l+r)>>1;
int xl,xr;
if(mid>=d)
xl=1;
else
xl=d-mid;
xr=d+mid;
if(check(xl,xr))
ans=mid,r=mid-1;
else
l=mid+1;
}
cout<<ans<<'\n';
return 0;
}
倍增
简单来说,倍增是一种每次翻倍的计算方式,任何翻倍算法都可以算作倍增
常见的是翻两倍的二倍增
倍增常见的应用是通过当前\(2^i\)的状态,推出\(2^{i+1}\)的状态
提高组常见的倍增求LCA,RMQ等
倍增求RMQ
可以利用倍增算法来构造一个ST表来求区间最大值和最小值
预处理复杂度\(O(nlogn)\),查询区间最值的复杂度为\(O(1)\)
总的复杂度就是\(O(nlogn)\)
我们有一个长度为n的序列,我们可以设\(f[i][x]\)表示从第x个数开始,长度为\(2^i\)的一段数的最值,这个区间的右端点是\(x+2^i-1\),可以得到一个式子
这样就可以利用\(f[i-1][]\)来推导\(f[i][]\)了
对于查询\([l,r]\)的最值,我们只需要选择两个区间的并起来为\([l,r]\)就可以了,即若\(2^i \leq r-l+1 \leq 2^{i+1}\),只左端点对其l,右端点对其r,中间有交集就可以了.
模板题P3865(以取最大值为例)
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <string>
#include <cmath>
using namespace std;
int n,m;
int a[100005];
int lg[100005]={-1};
int f[100005][50];
int main()
{
std::ios::sync_with_stdio(false);
cin>>n>>m;
for(int i=1;i<=n;i++)
{
cin>>a[i];
lg[i]=lg[i/2]+1;
}
for(int i=1;i<=n;i++)
f[i][0]=a[i];
for(int i=1;i<=lg[n];i++)
{
for(int j=1;j+(1<<i)-1<=n;j++)
f[j][i]=max(f[j][i-1],f[j+(1<<(i-1))][i-1]);
}
while(m--)
{
int l,r;
cin>>l>>r;
int len=lg[r-l+1];
cout<<max(f[l][len],f[r-(1<<len)+1][len])<<endl;
}
return 0;
}
P1081 开车旅行
思路
没有思路
我们可以发现问题1是n个问题2,我们可以对问题1有一个专门的解决方案,但是我们一旦解决了问题2,我们就一定可以解决问题1
首先,数据不是非常大
如果我们只是模拟的话,也可以在\(O(n^2)\)的时间内完成
我们直接用模拟可以写出50分的数据
预处理A和B在每一个城市的决策:使用set(insert进入,然后用upper找到比它大的第一个点,第二个点,和比它小的第一个第二个点),\(a<b<x<c<d\),这样找出离他最近的第一个点和第二个点,然后使用双向链表巧妙计算,\(a<->b<->x<->c<->d\)
当A到达某个点的时候,后面的情况都是不变的
然后使用动态规划处理所有的情况
设\(f[i][j][k]\)表示以i为起点,k第一个开车,开车j天到达的目的地
设\(fa[k][i][j],fb[k][i][j]\)表示以i为起点,k第一个开车,开着j天,A和B分别行驶的距离
使用倍增优化,j表示\(2^j\)天,这是,j的空间就不是n了,而是logn
然后算出所有开车的可能性,最后进行开车的模拟就完成了
P7167 Fountain
思路
设\(f[i][j]\)表示从i开始流\(2^j\)个盘子需要多少水
用加一个单调递减的单调栈来判断水向哪里流
这样就可以构建出一棵树,进行树上倍增来处理快速查询
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <string>
#include <stack>
#define int long long
using namespace std;
const int maxn=100005;
const int inf=1e16;
int n,q,d[maxn],c[maxn];
stack<int>s;
int l[maxn],first[maxn],en=0;
struct edge {
int next,e;
}edge[maxn];
inline void add(int x,int y)
{
edge[++en].e=y;
edge[en].next=first[x];
first[x]=en;
}
int f[maxn][21],g[maxn][21],dep[maxn];
inline void dfs(int u,int fa)
{
dep[u]=dep[fa]+1;
f[u][0]=fa; g[u][0]=c[fa];
for(int i=1;(1<<i)<=dep[u];i++) f[u][i]=f[f[u][i-1]][i-1],g[u][i]=g[f[u][i-1]][i-1]+g[u][i-1];
for(int i=first[u];i;i=edge[i].next)
{
int v=edge[i].e;
dfs(v,u);
}
}
signed main()
{
cin>>n>>q;
for(int i=1;i<=n;i++) cin>>d[i]>>c[i];
d[n+1]=inf; c[n+1]=inf;
s.push(1);
for(int i=2;i<=n+1;i++) {
while (s.size()&&d[i]>d[s.top()])
{
l[s.top()]=i;
s.pop();
}
s.push(i);
}
for(int i=1;i<=n;i++) add(l[i],i);
dfs(n+1,0);
for(int i=1;i<=q;i++)
{
int r,v;
cin>>r>>v;
if (c[r]>=v)
{
cout<<r<<endl;
continue;
}
v-=c[r];
int x=r,ans=0;
for(int i=20;i>=0;i--) {
if (g[x][i]<=v&&(1<<i)<=dep[x])
{
v-=g[x][i]; x=f[x][i];
}
if (v==0) ans=x;
}
if (ans==0)
ans=f[x][0];
if (ans==n+1)
cout<<0<<endl;
else
cout<<ans<<endl;
}
return 0;
}
堆
大根堆:父亲一定比儿子大的完全二叉树
小根堆:父亲一定比儿子小的完全二叉树
完全二叉树:除了最底层的以外,其他的节点都有两个儿子,且最底层的节点全部靠左
根节点永远是最大(最小)的
但它并不符合二叉搜索树
堆的操作
插入:
在当前堆的最后插入,向上调节到根
调节就不断地和它的父亲互换,然后向上调节就可以了,且调整完了之后一定还是大根堆
删除
只能删除根节点
删除了最大的节点相当于pop,然后根没了
怎么办?
将最后一个节点变成根节点
和插入类似,我们可以把根不断向节点调整,调整完了之后一定是大根堆
优先队列本质上就是一个堆
#include <queue>
std::priority_queue <int> q
q.push(x),q.pop等等
优先队列如何使每次查询的是最小的数?
我们用以下声明方式声明一个小根堆
priority_queue <int,vector<int>,greater<int>> q;
实际上还可以直接插入负数
Dijkstra堆优化的时候使用
priority_queue <pair<int,int> >,vector<pair<int,int>,great<pair<int,int> > >
堆排序
利用堆的根节点最大和完全二叉树的性质
步骤
在原序列建立一个大根堆
void update(int x,int m)
{
int t=lson(x);
if(t>m) return ;
if(t<m&&a[t+1]>a[t]) t++;
if(a[t]>a[x]) swap(t,m);
}
void heap_sort()
{
for(int i=n;i>=1;i--) update(1,n);//建立大根堆
for(int i=n-1;i>=1;i--)
{
swap(a[i+1],a[i]);//依次取出
update(1,i);
}
}
建立大根堆\(O(n)\),依次取出\(O(logn)\)
找第k大的数,复杂度\(O(n+klogn)\),当k远小于n的时候用堆排的优势非常明显,并且堆排是稳定的
P2827 蚯蚓
思路
Trie树
trie树的中文译名为字典树
字典树是边权为字符串的树,一个节点代表一个字符串
所有节点表示已知串和已知串的所有前缀
建树
如果没有就加,如果有,就向下查找下一个有没有,然后在最后一个 节点打一个标记
应用
1.检索字符串(是否出现,出现次数计数)(相当于手动完成了map[string])的功能,但它比map[string]快得多。
2.维护和前缀有关的一些事情,找两个字符串的公共前缀
3.AC自动机
板子
struct Trie{
long long val[maxn],ch[maxn][26],size;
Trie()
{
size=1;
memset(ch[0],0,sizeof(ch[0]));
memset(val,0,sizeof(val));
}
long long index(char c)
{
return c-'a';
}
void insert(char s[])
{
long long u=0,len=strlen(s+1);
for(int i=1;i<=len;i++)
{
long long c=index(s[i]);
if(!ch[u][c])
{
memset(ch[size],0,sizeof(ch[size]));
ch[u][c]=size++;
}
u=ch[u][c];
}
}
long long search(char s[])
{
long long u=0,len=strlen(s+1);
for(int i=1;i<=len;i++)
{
long long c=index(s[i]);
if(!ch[u][c]) return 0;
u=ch[u][c];
}
if(!val[u])
{
val[u]=1;
return 1;
}
return 2;
}
}tree;
P2580 于是他错误地点名开始了
思路
记录一个字符串的权值是不是第一次出现
当我们发现名字不存在的时候,我们找的时候就没有他的前缀,直接就认为他不存在
实际上就是板子修改一下,没啥区别
P5629 区间与除法
思路
建立一个trie树
就可以了