本次寒假共学习了三个部分:
1 最短路
2 最小生成树
3 线段树
一,最短路
一共是三种主要方法:
1 弗洛伊德算法
是运用了dp的思想。初始化结束后,开始进行三重循环,每层循环从第一个节点开始遍历,直至遍历到第n个节点,设最外层循环当前节点为i,中间层循环的当前节点为j,内层循环的当前节点为k,且i≠j≠k。则以节点i为中介点,以节点j为起点,节点k为目标点,判断由起点j经由中介点i到达目标点k的代价值是否小于由起点j直接到目标点k的代价值,若小于,则将从起点j到目标点k的代价值d[j][k]更新为d[j][i]+d[i][k]。三重循环结束后,路径规划结束。
时间复杂度高,O()。
for (ll k=1;k<=n;k++)
{
for (ll i=1;i<=n;i++)
{
for (ll j=1;j<=n;j++)
{
if (f[i][k]==0||f[k][j]==0) continue;
f[i][j]=min(f[i][j],f[i][k]+f[k][j]);
if (f[i][j]==0) f[i][j]=f[i][k]+f[k][j];
}
}
}
2 dijkstra算法
是运用了贪心的思想 ,可以使用一个优先队列来维护所有未被标记的节点,并以距离起点的距离作为优先级,每次选择优先级最高的节点作为当前节点。在遍历当前节点的邻居节点时,可以计算从起点到邻居节点的距离,并更新距离和路径信息。如果发现新的路径比之前记录的路径更短,则更新路径信息。
时间复杂度O()。
void DJ(int s){
for(int i = 1;i <= n; ++i) dis[i] = INF,vis[i] = false;
priority_queue<PII,vector<PII>,greater<PII> > que;
que.push({0,s});
dis[s] = 0;
while(!que.empty()){
int t = que.top().second;
que.pop();
if(vis[t]) continue;
vis[t] = true;
for(int i = 0,l = E[t].size();i < l; ++i) {
int j = E[t][i].first,w = E[t][i].second;
if(dis[j] > dis[t] + w){
dis[j] = dis[t] + w,que.push({dis[j],j});
}
}
}
}
3 SPFA算法
是基于松弛操作的最短路算法,同时加入了队列优化。用队列来维护哪些点可能会需要松弛操作,这样就对必要的边都进行一次松弛操作,一直循环的进行这个操作,直到我们不能进行松弛操作为止。
时间复杂度O()。
void SPFA(int s){
for(int i = 1;i <= n; ++i)
vis[i] = false,dis[i] = INF;
queue<int> que;
que.push(s);
dis[s] = 0,vis[s] = true;
while(!que.empty()){
int t = que.front();
que.pop();
vis[t] = false;
for(int i = 0,l = E[t].size();i < l; ++i) {
int j = E[t][i].v;
int k = E[t][i].w;
if(dis[j] > dis[t] + k){
dis[j] = dis[t] + k;
if(!vis[j]){
vis[j] = true;
que.push(j);
}
}
}
}
}
二,最小生成树
概念:一个图中可能存在多条相连的边,我们一定可以从一个图中挑出一些边生成一棵树。这仅仅是生成一棵树,还未满足最小,当图中每条边都存在权值时,这时候我们从图中生成一棵树(n - 1 条边),生成这棵树的总代价就是每条边的权重相加之和。
分为两种算法:
1 kruskal
Kruskal首先将所有的边按从小到大顺序排序,并将每一个点分属于n个独立的集合。然后按顺序枚举每一条边。如果这条边连接着两个不同的集合,那么就把这条边加入最小生成树,这两个不同的集合就合并成了一个集合;如果这条边连接的两个点属于同一集合,就跳过。直到选取了n-1条边为止。可以用并查集实现集合的合并。
时间复杂度:O()。
sort (a+1,a+1+m,cmp);
for (ll i=1;i<=m&&cnt<n-1;i++)
{
ll x=find(a[i].x);
ll y=find(a[i].y);
if (x!=y)
{
cnt++;
f[x]=y;
ans+=a[i].z;
}
}
if (cnt<n-1) cout<<"Keng Die!";
else cout<<ans;
2 prim
每次从连通网中找出一个权值最小的边,这样的操作重复 N-1
次,由N-1
条权值最小的边组成的生成树就是最小生成树
时间复杂度:O()。
三,线段树
主要有三个部分:
1 单点修改
先递归建树,
void tree(int p,int x,int y)//建立表示区间[x,y]的线段树,p为当前节点编号
{
f[p].x=x;//左端点赋值
f[p].y=y;//右端点赋值
//为新节点表示的左右区间赋值
if (x<y)//还可以继续建立
{
tree(p*2,x,(x+y)/2);//递归建立左子树,左端点为x,右端点为(x+y)/2,将线段树分为两段
tree(p*2+1,(x+y)/2+1,y);//递归建立右子树,左端点为(x+y)/2+1,右端点为y,将线段树分为两段
f[p].v=f[p*2].v+f[p*2+1].v;//线段树的值为左右两段线段树之和
}
else
{
f[p].v=a[x];//剩下的单独赋值为数列中对应的数
}
}
再单点修改,
void add(int p,int k,int d)//把d加到包含k的区间上,改变区间的v值,p为当前讨论的节点编号
{//1号节点(根节点)一定包含k,所以r从1号节点开始
f[p].v+=d;//对于每一个正在讨论的节点,将值加上d,类似于求前缀和
if (f[p].x==f[p].y) return;//左右端点重合,停止
if (f[2*p].x<=k&&f[p*2].y>=k)//当需要赋值的点在目前的左区间中
{
add(p*2,k,d);//当前节点有左孩子,且左孩子表示的区间中包含了k
}
if (f[2*p+1].x<=k&&f[p*2+1].y>=k)//当需要赋值的点在目前的左区间中
{
add(p*2+1,k,d);//当前节点有右孩子,且右孩子表示的区间中包含了k
}
}
最后区间求和,
ll sum(int p,int x,int y)//对于待计算的区间[x,y],求该区间所有数字之和,p为当前讨论的节点编号
{
if (y<f[p].x||x>f[p].y) return 0;//如果p表示的区间完全被[x,y]覆盖,就直接返回该区间的v值
if (x<=f[p].x&&f[p].y<=y) return f[p].v;//区间已经完全覆盖要求求和的区域,直接返回值
else //讨论[x,y]与p表示的区间部分相交的情况
{
ll flag=0;
flag+=sum(2*p,x,y);//讨论[x,y]与p的左孩子相交部分的和
flag+=sum(2*p+1,x,y);//讨论[x,y]与p的右孩子相交部分的和
return flag;
}
}
2 区间修改
先递归建树,
void tree(int p,int x,int y)
{
f[p].a=x;//左端点赋值
f[p].b=y;//右端点赋值
//为新节点表示的左右区间赋值
f[p].v=f[p].lazy=0;//赋初值为0
if (x==y)
{
f[p].v=a[x];//重合部分的值为原本数列上的数值
return;
}
int mid=(x+y)/2;//二分
tree(p*2,x,mid);//递归建立左子树,左端点为x,右端点为(x+y)/2
tree(p*2+1,mid+1,y);//递归建立右子树,左端点为(x+y)/2+1,右端点为y
//将线段树分为两段
f[p].v=max(f[p*2].v,f[p*2+1].v);
}
再区间修改,
void Add(int x,int y,int k,int z)//将区间x到y的所有数字增加z,k为当前讨论到的节点
{
if(f[k].lazy!=0)PutDown(k);//遇到延迟标记,先进行下放
if(x<=f[k].a&&f[k].b<=y)//区间完全包含修改部分
{
f[k].lazy+=z;//标记节点要增加的值,也意味着它的子孙节点都要增加相同的值
f[k].v+=z;//节点本身要增加的值,也意味着它的子孙节点都要增加相同的值
return;
}
int mid=(f[k].a+f[k].b)/2;//二分
if(x<=mid&&y>=f[k*2].a)Add(x,y,k*2,z);//包含右区间,分到右边继续重复操作
if(y>mid&&x<=f[k*2+1].b)Add(x,y,k*2+1,z);//包含左区间 ,分到左边继续重复操作
f[k].v=max(f[k*2].v,f[k*2+1].v);//区间最大值等于左区间最大值和右区间最大值取最大
}
还有区间求和 ,
int Ask(int x,int y,int k)//询问区间[x,y]的最大值,k为当前讨论到的节点
{
if(f[k].lazy!=0)PutDown(k);//将累积在点k上的Lazy值下放
if(x<=f[k].a&&f[k].b<=y)return f[k].v;//区间完全包含查询部分,直接返回区间最大值
int Lmax=0,Rmax=0;//左区间最大值与右区间最大值
int mid=(f[k].a+f[k].b)/2;//二分
if(x<=mid&&y>=f[k*2].a)Lmax=Ask(x,y,k*2);//包含右区间,分到右边继续重复操作
if(y>mid&&x<=f[k*2+1].b)Rmax=Ask(x,y,k*2+1);//包含左区间 ,分到左边继续重复操作
return max(Lmax,Rmax);//区间最大值等于左区间最大值和右区间最大值取最大,返回之
}
最后还有延迟标记的下放,方便统一求和。
void PutDown(int k) //下放操作,将累积在点k上的Lazy值下放到它的儿子节点
{
f[k*2].v+=f[k].lazy;//左节点的值加上上一层滞留的lazy值
f[k*2].lazy+=f[k].lazy;//更新右节点的lazy值
f[k*2+1].v+=f[k].lazy;//左节点的值加上上一层滞留的lazy值
f[k*2+1].lazy+=f[k].lazy;//更新右节点的lazy值
f[k].lazy=0;
}
3 动态开点
先区间修改,同时延迟标记,
void modify(ll i,ll l,ll r,ll a,ll b,ll k)
{
if (lazy[i]) pushdown(i);
if (a<=l && r<=b)
{
lazy[i]+=k;maxx[i]+=k;
return ;
}
ll mid=(l+r)/2;
if (a<=mid)
{
if (!ls[i])ls[i]=++t;
modify(ls[i],l,mid,a,b,k);
}
if (b>mid)
{
if (!rs[i])rs[i]=++t;
modify(rs[i],mid+1,r,a,b,k);
}
if (ls[i])maxx[i]=max(maxx[i],maxx[ls[i]]);
if (rs[i])maxx[i]=max(maxx[i],maxx[rs[i]]);
}
然后再区间求和,
ll getans(ll i,ll l,ll r,ll a,ll b)
{
if (lazy[i])pushdown(i);
if (a<=l&&r<=b)return maxx[i];
ll mid=(l+r)/2,lmaxx=0,rmaxx=0;
if (a<=mid) lmaxx=getans(ls[i],l,mid,a,b);
if (b>mid) rmaxx=getans(rs[i],mid+1,r,a,b);
return max(lmaxx,rmaxx);
}
最后是延迟标记的下放。
void pushdown(ll i)
{
if (!ls[i])ls[i]=++t;
if (!rs[i])rs[i]=++t;
lazy[ls[i]]+=lazy[i];
maxx[ls[i]]+=lazy[i];
lazy[rs[i]]+=lazy[i];
maxx[rs[i]]+=lazy[i];
lazy[i]=0;
}
心态问题:
对待平常的练习考试要认真,不要过于依赖他人的帮助,也要自觉学习,努力才会有回报!