CSP-S 十一集训Day4

Day3 博客 鸽了

今天讲图论奥 

强连通 弱连通 强者 弱鸡

dij spfa floyed 

求一条1到n的边权递增的最短路

考虑更改dis 数组的含义 dis 表示从1到i边权递增的最短路 

首先考虑对所有的边 从小到大排序 然后考虑 一条一条加入图中 

对于加入的边 我们判断一下 是否可用即可 

考虑 算法的正确性 如果某条边是 但某段最短路上的 最后一条边 因为他是最大的 所以在他加进来之前 路径在他之前的边

都被加进来过了 所以路径是合法路径 但是这一定是存在 对于边权相同的处理 注意细节就好;


题目

有n个点 m条边 还有k条铁路 k条铁路都和1号点相连 求最多删掉多少条铁路 st其他点到1号点的最短路 不变

直接用道路跑最短路 然后判断铁路长度 是行不通的 因为 你的铁路可能被删掉之后 存在另外一条 最短路 是可以通过另外一条铁路 然后 到达的

那么 我么需要 把所有的 铁路 道路 加进去 然后跑最短路 然后记录 最短路的条数

那么判断的时候 对于等于的情况 需要特别处理一个地方 就是 考虑是否存在 多条(>1)最短路 如果存在 那么也是可以去掉这个铁路的 因为去掉了 也是会最少保留一条最短路

#include<bits/stdc++.h>
using namespace std;
typedef pair<int,int> PII;
template<typename T>inline void read(T &x) {
    x=0;T f=1,ch=getchar();
    while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
    while(isdigit(ch)) {x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
    x*=f;
}
const int N=1e6+10;
int n,m,k,lin[N],cnt,v[N],ans,vis[N],dis[N],d[N],path[N],tot[N];
struct gg {
    int y,w,next;
}e[N];
void add(int x,int y,int w) {
    e[++cnt].y=y; e[cnt].w=w;
    e[cnt].next=lin[x];lin[x]=cnt;
}
void dijkstra(int s) {
    memset(vis,0,sizeof(vis));
    memset(dis,0x3f,sizeof(dis));
    dis[s]=0;
    priority_queue<PII,vector<PII>,greater<PII> >q;
    q.push(make_pair(dis[s],s));
    while(!q.empty()) {
        PII t=q.top();q.pop();
        int d=t.first,u=t.second;
        if(vis[u]) continue;
        vis[u]=1;
        for(int i=lin[u];i;i=e[i].next) {
            int v=e[i].y;
            if(d+e[i].w==dis[v]) tot[v]++;
            if(d+e[i].w<dis[v]) {
                tot[v]=1;
                dis[v]=d+e[i].w;
                q.push(make_pair(dis[v],v));
            }
        }
    } 
}
int main() {
    read(n); read(m); read(k);
    for(int i=1;i<=m;i++) {
        int u,v,w;
        read(u); read(v); read(w);
        add(u,v,w);
        add(v,u,w);
    }
    memset(v,-1,sizeof(v));
    for(int i=1;i<=k;i++) {
        int x,w;
        read(x); read(w);
        add(1,x,w);
        v[i]=w;
        path[i]=x;
        add(x,1,w);
    }
    dijkstra(1);
    for(int i=1;i<=k;i++) {
        int t=path[i],w=v[i];
        if(dis[t]<w) ans++;
        if(dis[t]==w) {
            if(tot[t]>1) {
                ans++;
                tot[t]--;
            }
        } 
    }
    cout<<ans<<endl;
    return 0;
}
View Code

 CF938D Buy a Ticket

有n个点 m条边 每个点具有一个点权ai 定义 $d_{i j}$ 表示 任意两点之间的最短路 

对于每一个i 求一个 满足 $min(2*d_{i,j}+a_j)$ 的 j

首先我们知道这个*2是没有意义的 我们考虑 对所有的边权*2 那么$a_j$ 我们考虑 怎么解决呢

我们可以改一下这个式子 考虑 $min(d'_{j,i}+d_{0,j})$  就是多建一个超级源点 考虑 此时转换成一个单源最短路的问题了

 dij的时候 直接将所有的 a_j 作为 j 的dis值 加入队列里 然后更新即可

#include<bits/stdc++.h>
using namespace std;
template<typename T>inline void read(T &x) {
    x=0;T f=1,ch=getchar();
    while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
    while(isdigit(ch)) {x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
    x*=f;
}
typedef long long ll;
typedef pair<ll,int> PII;
const int N=210000; 
int n,m,k,x,y,tot,ans,vis[N],lin[N];
ll v,dis[N];
struct gg {
    int y,next;
    ll v;
}a[N<<1];
inline void add(int x,int y,ll v) {
    a[++tot].y=y;
    a[tot].next=lin[x];
    lin[x]=tot;
    a[tot].v=v;
}
priority_queue<PII,vector<PII>,greater<PII> >q;
inline void dij() {
    while(q.size()) {
        ll x=q.top().second;
        q.pop();
        if(vis[x]) continue;
        vis[x]=1;
        for(ll i=lin[x];i;i=a[i].next) {
            ll y=a[i].y;
            if(dis[y]>dis[x]+a[i].v) {
                dis[y]=dis[x]+a[i].v;
                q.push(make_pair(dis[y],y));
            }
        }
    }
}
int main() {
    read(n); read(m);
    for(int i=1;i<=m;i++) {
        read(x); read(y); read(v);
        add(x,y,2*v); add(y,x,2*v);
    }
    for(int i=1;i<=n;i++) {
        read(dis[i]);
        q.push(make_pair(dis[i],i));
    }
    dij();
    for(int i=1;i<=n;i++)
        printf("%lld ",dis[i]);
    return 0;
} 
View Code

 经典问题 多起点最短路

这种题目的一般套路是我们虚设一个源点 然后源点连向关键点 边权为0 然后从源点出发 求到其他点的最短路  就是这k个点

出发到某个点的 最短路。

题目 :对于一个N个点 M条边无向带劝图 其实有K个关键点  求这K个关键点两两之间的最短路  2<=K<=N,M<=1e5

考虑进行转化 如果给定一个二分图 假设集合S 和T 求从S到T的最短路

那么 这就对应上面的经典问题 建一个超级源点即可 

那么考虑 这个问题 我们进行多少次的 划分 才能把 当前问题转化成 我们擅长的模型呢

就是任意两个点对都至少有一次被划分到 两个不同的集合内?

我们存在一个简单的想法 按照奇偶编号 这样我们可以求出所有奇数点到偶数点的最短路

那么考虑一个事情 如果两端都是奇数点 或者都是 偶数点呢 

我们当然可以考虑 他们/2 之后的奇偶

所以对于两个关键点 u v 他们对应的二进制位一定有一位不同 那么u v 至少会在一次划分中 被划分到不同的集合内

所以他们的最短路一定是会被考虑到 所以 我们对于每一个二进制位 将等于0 和 等于1 的都 求一次最短路

所以 任意两个关键点的最短路一定会被考虑到 所以整体复杂度是 (Mlog2N);

题目:给定一个N*M的网格图 他们可能有一些是障碍 一些是网格 或者A的基地 B的基地 C的基地

现在A B C 会随机选择一个自己的基地作为出发点 然后到达一个空地回合 这个空地一定是到到达他们三个最短距离之和最短

的点 求他们回合的路径长度的期望大小 N,M<=150 A的基地 B的基地<=50

暴力枚举AB的起点 求期望

但是我们注意到 A B基地的数量是较少的 所以我们当然是考虑 暴力枚举 AB的基地的 作为起点 这样我们知道了 每个空地到A

B的最短路 这样我们 对于每一个可能的C的基地 我们都要选出一个到达A B 的距离之和 加上到自己的最短的点的最短路

考虑这是一个多源点多汇点的最短路 

那么 我们可以建一个 超级源 向每个空地练一条边权为 到A +到B距离的点 此时对于每一个C的基地 那么 $dis_C$

就是从A B C回合的最短距离 其实网格图 我们更可以改成BFS 所以复杂度是(A基地的数量*B基地的数量*N*M)

题意:给出一个N个点M条边的图 对于任意点对(u,v) 要求出来 其所有的最短路 覆盖的路径数量 N<=500

我们显然可以先跑一个多源最短路 求出任意两点之间的最短路 然后存在一个 n^4 的暴力 

每次枚举点对 然后枚举 边 根据 dis 判断这条边在不在最短路上即可 

考虑 优化 floyed不能省 然后考虑 省掉枚举边的复杂度 

那么根据 floyed 的特点 我们显然知道 对于s 到 t 的最短路 存在一个中间点 p 如果 s t 的距离是 p转移而来的 

那么 所以以p 结尾的 在s->p 的最短路上的边 我们都可以累加上 显然这样是不重复的 

那么 如果p 不是中间点 那么 显然 以p 结尾的所有边都不会在s t 的最短路上


生成树问题

对于Kruskal  存在一个比较经典问题 对于每个边权是一个关于时间T的二次函数 让你求某个时间区间的最短路 或者 整个时间轴的最小生成树

划分k个集合 使得 跨集合的点的最短路尽可能大

实际上我们是在求 跨集合的边的最小值 我们使用Kruskal 将原图合并为剩下K个集合 再次添加进来的边就是答案;

给出一张N个点M条边的无向图。 

接下来有Q组询问,每组询问两点(s,t)间所有路径当中最长边的最小值是多少。

类似于 货车运输 的题目

是一个瓶颈生成树 然后我们最小生成树肯定是要跑的 然后考虑 把这颗树建出来 然后存一下每个节点的fa值 然后考虑 倍增维护边权上的 最小值 最后求lca 的过程中 更新这个值即可

翻到了我原来写的代码 我也不知道维护的是最大值还是最小值 反正都是一样的 其实还有一种做法 那就是重构树 这里简单介绍一下

对于Kruskal重构树 

在使用并查集合并两个集合(两棵树)的根时,我们新建一个点权为连接这两个集合的边权大小的节点作为树根。

所有原图中的节点都是叶子 总共的点数O(n) 是一个二叉树。

如果把叶子节点的点权看作是0的话,整棵树满足大根堆的性质。(为什么?) 考虑我们在构造的过程中是按照从小到大的顺序排序的 其实这个是可以改变成小根堆的

最小生成树上两点间的路径边权最大值 为它们重构树上LCA的点权大小。

使用Kruskal重构树,两点间路径最大值即为重构树上的两点LCA,可以做到优秀的排序外线性复杂度。

#include<bits/stdc++.h>
using namespace std;
const int maxn=6e5+10;
const int inf=0x3f3f3f3f;
template<typename T>inline void read(T &x)
{
    x=0;
    T f=1,ch=getchar();
    while (!isdigit(ch) && ch^'-') ch=getchar();
    if (ch=='-') f=-1, ch=getchar();
    while (isdigit(ch)) x=(x<<1)+(x<<3)+(ch^48), ch=getchar();
    x*=f;
}
struct Edge
{
    int x,y,z,next;
}G[maxn<<1],A[maxn<<1];//G[]是最大/小生成树的图
int n,m,head[maxn],len;
inline void add(int x,int y,int z)
{
    G[++len].y=y,G[len].z=z,G[len].next=head[x],head[x]=len;
}
int fa[maxn];
inline int get(int x)
{
    if (x==fa[x]) return x;
    return fa[x]=get(fa[x]);
}
inline bool cmp(Edge a,Edge b)
{
//    return a.z>b.z;
    return a.z<b.z;
}
inline void Kruskal()
{
    sort(A+1,A+m+1,cmp);
    for (int i=1;i<=n;++i)
        fa[i]=i;
    for (int i=1;i<=m;++i)
    {
        int x=get(A[i].x),y=get(A[i].y);
        if (x!=y)
        {
            fa[y]=x;
            add(A[i].x,A[i].y,A[i].z);
            add(A[i].y,A[i].x,A[i].z);
        }
    }
}
int d[maxn],f[maxn][21],w[maxn][21];//fa[]表示并查集中的父节点,f[][]表示树上的父节点,w[][]表示最大载重
inline void dfs(int x)
{
    for (int i=1;i<=20;++i)//LCA初始化
    {
        f[x][i]=f[f[x][i-1]][i-1];
//        w[x][i]=min(w[x][i-1],w[f[x][i-1]][i-1]);
        w[x][i]=max(w[x][i-1],w[f[x][i-1]][i-1]);
    }
    for (int i=head[x];i;i=G[i].next)
    {
        int y=G[i].y;
        if (d[y]) continue;
        d[y]=d[x]+1;//计算深度
        f[y][0]=x;//储存父节点
        w[y][0]=G[i].z;//储存到父节点的权值
        dfs(y);
    }
}
inline int lca(int x, int y)
{
    if (get(x)!=get(y)) return -1;//不连通,输出-1
//    int ans=inf;
    int ans=0;
    if (d[x]>d[y]) swap(x,y);//保证y节点更深
    for (int i=20;i>=0;--i)//将y节点上提到于x节点相同深度
        if (d[f[y][i]]>=d[x])
        {
//            ans=min(ans,w[y][i]);//更新最大载重(最小边权)
            ans=max(ans,w[y][i]);
            y=f[y][i];//修改y位置
        }
    if (x==y) return ans;//如果位置已经相等,直接返回答案
    for (int i=20;i>=0;--i)//寻找公共祖先
        if (f[x][i]!=f[y][i])
        {
//            ans=min(ans,min(w[x][i], w[y][i]));//更新最大载重(最小边权)
            ans=max(ans,max(w[x][i],w[y][i]));
            x=f[x][i],y=f[y][i];//修改x,y位置
        }
//  ans=min(ans,min(w[x][0],w[y][0]));//更新此时x,y到公共祖先最大载重,f[x][0], f[y][0]即为公共祖先
    ans=max(ans,max(w[x][0],w[y][0]));
    return ans;
}
int main()
{
    int flag=0;
    while (scanf("%d %d",&n,&m)!=EOF)
    {
        if (flag) printf("\n");
        else flag=1;
        memset(f,0,sizeof(f));
        memset(w,0,sizeof(w));
        memset(d,0,sizeof(d));
        memset(head,0,sizeof(head));
        len=0;
        for (int i=1;i<=m;++i)
            read(A[i].x),read(A[i].y),read(A[i].z);
        Kruskal();
        for (int i=1;i<=n;++i)//dfs收集信息
            if (!d[i])
            {
                d[i]=1;
                dfs(i);
                f[i][0]=i;
//                w[i][0]=inf;
                w[i][0]=-inf;
            }
        int q;
        read(q);
        while (q--)
        {
            int x,y;
            read(x);read(y);
            printf("%d\n",lca(x,y));
        }
    }
    return 0;
}
View Code

 

CF76A Gift

给出一张N个点M条边的无向连通图。

每条边有两个权a(i)和b(i)

现在,你需要确定两个参数A,B。使得只保留a(i)<=A且 b(i)<=B的边后图依然连通。

你的目标是最小化A*WA+B*WB. 其中WA和WB为两个输入当中给定的常数

首先比较完美的事情是 我们有一个N^6的暴力 hh

考虑枚举A 然后满足A限制的边就确定了 然后求一个b的最大值最小 st 图联通即可 那么就是一个瓶颈生成树

再考虑将边按照A排序 每次更新最小生成树即可 其实也可以维护一个动态生成树


 

posted @ 2019-10-04 18:55  Tyouchie  阅读(199)  评论(1编辑  收藏  举报