2017.4.3 机房测试 (并查集,最短路,拓扑排序,最小生成树)

jyb远在北京仍不忘为我们出题。
这次是喜闻乐见的图论专题。

这里写图片描述
【解题报告】
用链表什么的暴力枚举题意,最坏复杂度是O(m^2)的。
注意到每次都是移动一摞砖,其实问题就是集合的合并,只是这个集合是一个有序的,我们还需要知道每个元素在集合中的位置。我们可以利用并查集高效的来解决。我们引入一个dep数组,对于祖先节点(将每摞砖最上方的一块设为祖先,设最下面的为祖先也是可以的),我们记录该集合中一共有多少块砖,对于其他节点,我们记录它上方有多少块砖,这样我们可以很容易地计算出下方有多少块。这样在合并x和y(设他们祖先为fx,fy)时,我们将dep[fx]赋给dep[fy](因为移到了上方,fx有多少块fy上方就有多少块),然后dep[fx]要加上原本的dep[fy]。难点就是在合并的过程中,dep数组怎么维护。其实很简单,只需要在路径压缩时顺带维护即可,如果一个节点x的祖先fx不是祖先(即已被合并过),那么dep[x]加上dep[fx]即可(等于头上又多了一摞砖,所以要加上)。
时间复杂度O(α(m))

代码如下:

#include<cstdlib>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<string>
#include<iostream> 
#include<queue>  
#include<cstdio>

using namespace std;  

int n,m;
int x,y;
char c;
int father,t1,t2;
int fa[3000010];        //
int dep[3000010];       //

int getfather(int x)
{
    if(x == fa[x])      //
        return x;
    else
    {
        int f = fa[x];              //用来判断自己记录的祖先是否真的是祖先
        fa[x] = getfather(fa[x]);   //路径压缩
        if(fa[f] != f)              //如果不是,则需要更新上面的砖块数
        dep[x] += dep[f];
        return fa[x];
    }
}

void Init()                         //
{
    for(int i = 1; i <= n; i++)
    {
        fa[i] = i;  
        dep[i] = 1;
    }
}

int main()  
{  
    freopen("bricks.in","r",stdin);
    freopen("bricks.out","w",stdout);
    scanf("%d%d",&n,&m);
    Init();
    while(m--)
    {
        scanf(" %s",&c);
        if(c == 'M')
        {
            scanf("%d%d",&x,&y);
            t1 = getfather(x);
            t2 = getfather(y);
            if(t1 != t2)
            {
                fa[t2] = t1;            //合并
                int ll = dep[t2];       //t2上面的砖块数等于t1那摞砖的总数
                dep[t2] = dep[t1];      //
                dep[t1] += ll;          //而t1的总数也需要加上t2的总数
            }
        }
        else
        {
            scanf("%d",&x);             //因为问的在下面的砖块,所以减去自己
            if(x == fa[x])              //因为祖先和非祖先,dep数组的含义不                                            //同需要分开讨论,祖先减去自己即可
            {
                printf("%d\n",dep[x] - 1);  
                continue;
            }
            father = getfather(x);      //非祖先的话,因为dep保存的是自己上面还有多少个,所以用总的减去上面的和自己,即可得到下面有多少个
            printf("%d\n",dep[father] - dep[x] - 1);
        }
    }
}

下面是自己的代码

//poj 1998
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define N 100000

int n,m;
int fa[2*N],sum[2*N],dis[2*N];

int getfather(int x)
{
    int t;
    if(fa[x]!=x) 
    {
        t=fa[x];
        fa[x]=getfather(fa[x]);
        dis[x]+=dis[t];
    }
    return fa[x];
}
void move(int x,int y)
{
    x=getfather(x),y=getfather(y);
    if(x!=y)
    {
        fa[y]=x;
        dis[y]=sum[x];
        sum[x]+=sum[y];
    } 
}
int main()
{
    freopen("bricks.in","r",stdin);
    freopen("bricks.out","w",stdout);
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
    fa[i]=i,sum[i]=1,dis[i]=0;
    while(m--)
    {
        char s[10];
        int x,y;
        scanf("%s",s);
        if(s[0]=='M')
        {
            scanf("%d%d",&x,&y);
            move(x,y);
        }
        else
        {
            scanf("%d",&x);
            printf("%d\n",sum[getfather(x)]-dis[x]-1);
        }
    }
    return 0;
}

这里写图片描述
【解题报告】
如果没有大雾天气,很显然就是求一个最短路就行了。但是由于大雾天气的存在,如果大雾天气出现在乐最短路的路径上,那么肯定不能按照最快的时间到达,如果这样的话很可能就失信与客户。所以我们就需要枚举最短路径上每条高速公路出现大雾天气的情况,再求最短路,可得一个最糟的情况,即得到在不失信于客户的前提下的最快时间。
注意到数据范围,每条高速公路的行驶时间最多相差10倍,spfa的时间复杂度的上界大概为O(kM),k = 10,而朴素Dijkstra求一次最短路为稳定O(n^2),总的最坏为O(n^3),所以我们采用spfa或者堆优化Dijkstra算法(jyb写的STL堆优被卡了两个点)求最短路。
(特别坑的是题里面是单向边)

代码如下:

#include <cstdio>
#include <cmath>
#include <cstring>
#include <string>
#include <cstdlib>
#include <algorithm>
#include <iostream>

using namespace std;

const int maxn = 4010;
const int maxm = 20010;

struct edge
{
    int u,v,next;
    double w;
}e[maxm];
int h[maxn],num;
double dis[maxn];
int pre[maxn];
bool vis[maxn];
int q[maxn],head,tail;
int n,m;

void build(int u,int v,double w)
{
    num++;
    e[num].u = u;
    e[num].v = v;
    e[num].w = w;
    e[num].next = h[u];
    h[u] = num;
}

void init()
{
    for(int i = 2; i <= n; i++)
        dis[i] = 1000000.0;
    head = 0;
    tail = 1;
    q[0] = 1;
    vis[1] = true;
}

double spfa(bool first)
{
    int v,u;
    init();
    while(head != tail)
    {
        u = q[head];
        for(int i = h[u]; i; i = e[i].next)
        {
            v = e[i].v;
            if(dis[v] > dis[u] + e[i].w)
            {
                dis[v] = dis[u] + e[i].w;
                if(first)
                    pre[v] = i;
                if(!vis[v])
                {
                    vis[v] = true;
                    q[tail] = v;
                    tail = (tail + 1) % n;
                }
            }
        }
        vis[u] = false;
        head = (head + 1) % n;
    }
    return dis[n];
}

int main()
{
    freopen("drive.in","r",stdin);
    freopen("drive.out","w",stdout);
    scanf("%d%d",&n,&m);
    int u,v;
    double sp,len;
    for(int i = 1; i <= m; i++)
    {
        scanf("%d%d%lf%lf",&u,&v,&sp,&len);
        build(u,v,len/sp);
    }
    int now = n;
    double ans = spfa(true);
    while(now != 1)
    {
        e[pre[now]].w *= 4.0;
        ans = max(ans,spfa(false));
        e[pre[now]].w /= 4.0;
        now = e[pre[now]].u;
    }
    printf("%.4lf\n",ans);
    return 0;
} 

这里写图片描述
暴力做法:Floyd求任意两点连通性。(给了30%的分)
我们知道,对于在同一SCC中的两点,它们是互相可达的,这也意味着一个SCC中的所有点和其他点的连通性都是一样的,显然我们可以将原图缩点简化问题。经过tarjan缩点后,得到了一个DAG,我们很容易想到如果一个图“分叉”,那么分叉上的点肯定是互不可达的,所以原图必须是“一条链”。为什么对链打引号呢?是因为这条链不用很严格,比如1->2 2->3 3->4 1->4 2->4这样的图也是满足的,且可能还有重边,所以只用入度出度去判断的方法容易出错,dfs去走一条链的办法也比较麻烦(我没想出有什么简单的办法)。我们利用拓扑排序,若一直都是一个入度为0的点(队中至多有一个点),那么就是满足题意的。
Ps:可以先写一个floyd来对拍(因为时间代价很低)
时间复杂度O(n+m)

代码如下:

#include <cstdio>
#include <cmath>
#include <cstring>
#include <cstdlib>
#include <iostream>
#include <algorithm>
#include <string>

using namespace std;

const int maxn = 100010;
const int maxm = 400010;

struct edge
{
    int v,next;
}e[maxm];
int h[maxn],num;
int indexx;
int top,stack[maxn];
int belong[maxn],cnt;
int hh[maxn],numm;
int q[maxn],head,tail;
int u,v,n,m;
bool vis[maxn];
int p[maxn],dfn[maxn],low[maxn];
int T;

void dfs(int u)                     //tarjan
{
    int v;
    indexx++;
    dfn[u] = indexx;
    low[u] = indexx;
    vis[u] = true;
    stack[++top] = u;
    for(int i = h[u]; i != 0; i = e[i].next)
    {
        v = e[i].v;
        if(!dfn[v])
        {
            dfs(v);
            if(low[v] < low[u])
                low[u] = low[v];
        }
        else
            if(vis[v] && dfn[v] < low[u])
                low[u] = dfn[v];
    }
    if(dfn[u] == low[u])
    {
        cnt++;
        while(1)
        {
            int x = stack[top];
            vis[x] = false;
            belong[x] = cnt;
            top--;
            if(x == u)
                break;
        }
    }
}

void build(int u,int v)                 //原图建边
{
    num++;
    e[num].v = v;
    e[num].next = h[u];
    h[u] = num;
}

void build2(int u,int v)                //缩点后的图建边
{
    num++;
    e[num].v = v;
    e[num].next = hh[u];
    hh[u] = num;
}

int main()
{
    freopen("graph.in","r",stdin);
    freopen("graph.out","w",stdout);
    scanf("%d",&T);
    while(T--)
    {
        scanf("%d%d",&n,&m);
        num = cnt = indexx = 0;
        memset(h,0,sizeof(h));
        memset(hh,0,sizeof(hh));
        while(m--)
        {
            scanf("%d%d",&u,&v);
            build(u,v);
        }
        memset(dfn,0,sizeof(dfn));
        memset(low,0,sizeof(low));
        memset(p,0,sizeof(p));              //记住初始化
        for(int i = 1; i <= n; i++)
            if(!dfn[i])
                dfs(i);
        for(int i = 1; i <= n; i++)
        {
            for(int j = h[i]; j; j = e[j].next)
                if(belong[i] != belong[e[j].v])     //不是一个SCC的就建边
                {
                    p[belong[e[j].v]]++;
                    build2(belong[i],belong[e[j].v]);
                }
        }
        bool ans = true;
        head = tail = 0;
        for(int i = 1; i <= cnt; i++)           //统计入度为0的
            if(!p[i])
                q[tail++] = i;
        while(head < tail)
        {
            if(tail - head > 1)                 //队中有超过两个点,就说明不满足条件
            {
                ans = false;
                break;
            }
            u = q[head];
            for(int i = hh[u]; i; i = e[i].next)
            {
                v = e[i].v;
                p[v]--;
                if(p[v] == 0)
                    q[tail++] = v;
            }
            head++;
        }
        if(ans)
            printf("Yes\n");
        else
            printf("No\n");
    }
    return 0;
}

下面是我自己的代码

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;

const int maxn = 1000010;
const int maxm = 400010;

struct edge
{
    int v,next;
}e[maxm];
int h[maxn],num;
int indexx;
int top,stack[maxn];
int belong[maxn],cnt;
int hh[maxn],numm;
int q[maxn],head,tail;
int u,v,n,m;
bool vis[maxn];
int p[maxn],dfn[maxn],low[maxn];
int T;
void dfs(int u)                     //tarjan
{
    int v;
    indexx++;
    dfn[u]=indexx;
    low[u]=indexx;
    vis[u]=true;
    stack[++top] = u;
    for(int i=h[u];i!=0;i=e[i].next)
    {
        v=e[i].v;
        if(!dfn[v])
        {
            dfs(v);
            if(low[v] < low[u])
            low[u] = low[v];
        }
        else
        if(vis[v] && dfn[v] < low[u])
        low[u] = dfn[v];
    }
    if(dfn[u]==low[u])
    {
        cnt++;
        while(1)
        {
            int x = stack[top];
            vis[x] = false;
            belong[x] = cnt;
            top--;
            if(x == u)
            break;
        }
    }
}
void build(int u,int v)                 //原图建边
{
    num++;
    e[num].v=v;
    e[num].next=h[u];
    h[u]=num;
}
void build2(int u,int v)                //缩点后的图建边
{
    num++;
    e[num].v=v;
    e[num].next=hh[u];
    hh[u]=num;
}
int main()
{
    freopen("graph.in","r",stdin);
    freopen("graph.out","w",stdout);
    scanf("%d",&T);
    while(T--)
    {
        scanf("%d%d",&n,&m);
        num=cnt=indexx=0;
        memset(h,0,sizeof(h));
        memset(hh,0,sizeof(hh));
        while(m--)
        {
            scanf("%d%d",&u,&v);
            build(u,v);
        }
        memset(dfn,0,sizeof(dfn));
        memset(low,0,sizeof(low));
        memset(p,0,sizeof(p));              //记住初始化
        for(int i=1;i<=n;i++)
        if(!dfn[i])
        dfs(i);
        for(int i=1;i<=n;i++)
        for(int j=h[i];j;j=e[j].next)
        if(belong[i]!=belong[e[j].v])       //不是一个SCC的就建边
        {
            p[belong[e[j].v]]++;
            build2(belong[i],belong[e[j].v]);
        }
        bool ans=true;
        head=tail=0;
        for(int i=1; i<=cnt;i++)            //统计入度为0的
        if(!p[i])
        q[tail++] = i;
        while(head<tail)
        {
            if(tail-head>1)                 //队中有超过两个点,就说明不满足条件
            {
                ans=false;
                break;
            }
            u=q[head];
            for(int i=hh[u];i;i=e[i].next)
            {
                v=e[i].v;
                p[v]--;
                if(p[v]==0)
                q[tail++] = v;
            }
            head++;
        }
        if(ans)
        printf("Yes\n");
        else
        printf("No\n");
    }
    return 0;
}

这里写图片描述
【解题报告】
最朴素的做法:枚举航空公司,将本公司的航线加入,然后做一遍MST,时间复杂度是O(klogk+mk)的。
但我们注意到“在最小生成树中任意一条边都是连接两个集合边权最小的一条边”,这个是MST一个非常重要的结论,是Prim和kruscal算法中贪心的核心。利用这条性质我们可以知道,对于每个航空公司,我们只需要原图MST中得到的航线即可得到最优购买方案。所以更优的做法是:先跑一次MST,把得到的边记录下来,然后枚举航空公司,还是先讲本公司的边全部加进去,然后只用扫一边第一次MST中记录下来的边,用kruscal的方法去构造树即可。时间复杂度为O(klogk+nm)。
对于本题,50%的数据是可以O(mk)过的,最后30%的数据我特别构造了一下,必须枚举到排序后的最后几条边才能构造出一颗生成树,所以O(mk)应该是过不了的,此外的20%数据是随机数据,加了构造出树就跳出判断的代码可能会过(但是jyb写的mk的没过。。);另外,50%的数据需要long long

代码如下:

#include <cstdio>
#include <cmath>
#include <cstring>
#include <string>
#include <cstdlib>
#include <algorithm>
#include <iostream>

using namespace std;
const int maxm = 200002;
const int maxn = 2002;
const long long INF = 1ll << 60;

struct line
{
    int u,v;
    long long w;
    bool operator < (const line &b) const
    {
        return w < b.w;
    }
}l[maxm],mst[maxn];

struct edge
{
    int u,v,next;
}e[maxm];
int h[maxn],num;
int fa[maxn];
long long a[maxn];
int n,m,k,cnt;
long long ans;

void build(int no,int u,int v)
{
    num++;
    e[num].u = u;
    e[num].v = v;
    e[num].next = h[no];
    h[no] = num;
}

int getfather(int x)
{
    if(x == fa[x])
        return x;
    else
        return fa[x] = getfather(fa[x]);
}

int main()
{
    freopen("airplane.in","r",stdin);
    freopen("airplane.out","w",stdout);
    scanf("%d%d%d",&n,&m,&k);
    for(int i = 1; i <= m; i++)
        scanf("%I64d",&a[i]);
    int u,v,no,w;
    for(int i = 1; i <= k; i++)
    {
        scanf("%d%d%d%d",&u,&v,&w,&no);
        l[i].u = u;
        l[i].v = v;
        l[i].w = w;
        build(no,u,v);
    }
    sort(l+1,l+1+k);
    for(int i = 1; i <= n; i++)
        fa[i] = i;
    for(int i = 1; i <= k; i++)
    {
        int x = getfather(l[i].u);
        int y = getfather(l[i].v);
        if(x != y)
        {
            fa[y] = x;
            mst[++cnt] = l[i];
        }
    }
    ans = INF;
    for(int i = 1; i <= m; i++)
    {
        for(int j = 1; j <= n; j++)
            fa[j] = j;
        for(int j = h[i]; j; j = e[j].next)
        {
            int x = getfather(e[j].u);
            int y = getfather(e[j].v);
            if(x != y)
                fa[y] = x;
        }
        long long res = 0;
        for(int j = 1; j <= cnt; j++)
        {
            int x = getfather(mst[j].u);
            int y = getfather(mst[j].v);
            if(x != y)
            {
                fa[y] = x;
                res += mst[j].w;
            }
        }
        if(res + a[i] < ans)
            ans = res + a[i];
    }
    printf("%I64d\n",ans);
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值