2020级数据结构荣誉课第五次上机考试题解(图)

目录

 

题目一:7-1 图的深度优先搜索I (100 分)

题目描述: 

输入格式:

输出格式:

输入样例:

输出样例:

题目分析: 

代码:

 题目二:7-2 圆 (100 分)

题目描述:

题目分析:

代码:

题目三:7-3 供电 (100 分)

题目描述:

代码:

反思:

题目四: 7-4 发红包 (100 分)

题目描述:

题目分析:

代码:


题目一:7-1 图的深度优先搜索I (100 分)

题目描述: 

无向图 G 有 n 个顶点和 m 条边。求图G的深度优先搜索树(森林)以及每个顶点的发现时间和完成时间。每个连通分量从编号最小的结点开始搜索,邻接顶点选择顺序遵循边的输入顺序。

在搜索过程中,第一次遇到一个结点,称该结点被发现;一个结点的所有邻接结点都搜索完,该结点的搜索被完成。深度优先搜索维护一个时钟,时钟从0开始计数,结点被搜索发现或完成时,时钟计数增1,然后为当前结点盖上时间戳。一个结点被搜索发现和完成的时间戳分别称为该结点的发现时间和完成时间

输入格式:

第1行,2个整数n和m,用空格分隔,分别表示顶点数和边数, 1≤n≤50000, 1≤m≤100000.

第2到m+1行,每行两个整数u和v,用空格分隔,表示顶点u到顶点v有一条边,u和v是顶点编号,1≤u,v≤n.

输出格式:

第1到n行,每行两个整数di和fi,用空格分隔,表示第i个顶点的发现时间和完成时间1≤i≤n 。

第n+1行,1个整数 k ,表示图的深度优先搜索树(森林)的边数。

第n+2到n+k+1行,每行两个整数u和v,表示深度优先搜索树(森林)的一条边<u,v>,边的输出顺序按 v 结点编号从小到大。

输入样例:

在这里给出一组输入。例如:

6 5
1 3
1 2
2 3
4 5
5 6

输出样例:

在这里给出相应的输出。例如:

1 6
3 4
2 5
7 12
8 11
9 10
4
3 2
1 3
4 5
5 6

题目分析: 

这是一道DFS的变形题目。主要考察对DFS在时间顺序上的理解。根据DFS遍历顺序,对点与边相应的排个顺序即可。

但是边的按照末端点大小来排的,所以可以考虑用优先队列或存放到数组中用sort函数来快排。这里选择用优先队列来排序。

代码:

#include <iostream>
#include <vector>
#include <stack>
#include <algorithm>
#include <queue>
using namespace std;
const int maxn = 50001;
priority_queue<pair<int ,int > >q;
vector<int>a[maxn];
stack<int>st;
int mark[maxn];
int time0[maxn];
int time1[maxn];
int tik = 0;                                                    //时间戳
int cnt = 0;
void dfs(int i)                                            //dfs
{
    time0[i] = ++tik;
    mark[i] = 1;                                            //标记起始点
    for (int j = 0;j<int(a[i].size());j++)        
    {
        if (!mark[a[i][j]])
        {
            dfs(a[i][j]);
            cnt++;                                    //记录cnt
            mark[a[i][j]] = 1;                        //标记末端点
            q.push(make_pair(-a[i][j],i));            //放入堆中,以后进行排序输出
        }

    }
    time1[i] = ++tik;
}
int main()
{
    ios::sync_with_stdio(0);
    cin.tie(0);
    int n,m,t1,t2;
    cin>>n>>m;
    for (int i = 1;i<=m;i++)
    {
        cin>>t1>>t2;
        a[t1].push_back(t2);
        a[t2].push_back(t1);
    }
    for (int i = 1;i<=n;i++)
    {
        mark[i] = 0;
    }
    for (int i = 1;i<=n;i++)
    {
        if (!mark[i])
        {
            dfs(i);

        }
    }
    for (int i = 1;i<=n;i++)
    {
        cout<<time0[i]<<' '<<time1[i]<<'\n';
    }
    cout<<cnt;
    for (int i = 1;i<=cnt;i++)
    {
        cout<<'\n'<<q.top().second<<' '<<-q.top().first;
        q.pop();
    }
    return 0;
}

 题目二:7-2 圆 (100 分)

题目描述:

二维平面上有n 个圆。请统计:这些圆形成的不同的块的数目。

圆形成的块定义如下: (1)一个圆是一个块; (2)若两个块有公共部分(含相切),则这两个块形成一个新的块,否则还是两个不同的块。

输入格式:

第1行包括一个整数n,表示圆的数目,n<=8000。

第2到n+1行,每行3 个用空格隔开的数x,y,r。(x,y)是圆心坐标,r 是半径。所有的坐标及半径都是不大于30000 的非负整数。

输出格式:

1个整数,表示形成的块的数目。

输入样例:

在这里给出一组输入。例如:

2
0 0 1
1 0 2

输出样例:

在这里给出相应的输出。例如:

1

题目分析:

这题主要考察并查集,属于一道模板题,只不过在并的操作上有个条件是圆相交或相切。

代码:

#include <iostream>
#include <vector>
#include <stack>
using namespace std;
const int maxn = 8005;
typedef struct circle
{
    long long int x,y,r;
}cc;
cc c[maxn];
int father[maxn];
int n;
void make_set()
{
    for (int i = 1;i<=n;i++)
    {
        father[i] = 0;
    }
}
int FIND(int i)
{
    if (father[i]<=0) return i;
    else    return father[i] = FIND(father[i]);        //找根节点

}
void UNION(int x,int y)                                //按秩合并
{
    int fx = FIND(x);
    int fy = FIND(y);
    if (fx == fy)
        return;
    if (father[fx]<father[fy])  father[fy] = fx;
    else
    {
        if (father[fx] == father[fy])   father[fy]--;
        father[fx] = fy;
    }
}
bool isunion(int i,int j)                               //判断是否是一个块
{
    long long int d1 = (c[i].x - c[j].x)*(c[i].x - c[j].x) + (c[i].y - c[j].y)*(c[i].y - c[j].y);
    long long int d2 = (c[i].r+c[j].r)*(c[i].r+c[j].r);
    if (d2>=d1)
        return 1;
    else
        return 0;
}
int main()
{
    ios::sync_with_stdio(0);cin.tie(0);
    cin>>n;
 //   make_set();
    for (int i = 1;i<=n;i++)
    {
        cin>>c[i].x>>c[i].y>>c[i].r;
        for (int j = 1;j<i;j++)
        {
            if (isunion(i,j))
                UNION(i,j);
        }
    }
    int cnt = 0,f;
    int ans = 0;
    int a[maxn];
    for (int i = 1;i<=n;i++)
    {
        f = 0;
        for (int j = 0;j<cnt;j++)
        {
            if (a[j] == FIND(i))
            {
                f = 1;break;
            }
        }
        if (!f)
        {
            a[cnt++] = FIND(i);
        }
    }
    cout<<cnt;
    return 0;
}

题目三:7-3 供电 (100 分)

题目描述:

要给N个地区供电。每个地区或者建一个供电站,或者修一条线道连接到其它有电的地区。试确定给N个地区都供上电的最小费用。

输入格式:

第1行,两个个整数 N 和 M , 用空格分隔,分别表示地区数和修线路的方案数,1≤N≤10000,0≤M≤50000。

第2行,包含N个用空格分隔的整数P[i],表示在第i个地区建一个供电站的代价,1 ≤P[i]≤ 100,000,1≤i≤N 。

接下来M行,每行3个整数a、b和c,用空格分隔,表示在地区a和b之间修一条线路的代价为c,1 ≤ c ≤ 100,000,1≤a,b≤N 。

输出格式:

一行,包含一个整数, 表示所求最小代价。

输入样例:

在这里给出一组输入。例如:

4 6
5 4 4 3
1 2 2
1 3 2
1 4 2
2 3 3
2 4 3
3 4 4

输出样例:

在这里给出相应的输出。例如:

9

可以看出此题是要用最小生成树的算法来做。但是此题还有一个点就是每个地区除了联通线路之外还有自己建发电站的操作。因此可以考虑加上一个虚源点,让他到每个发电站的代价为建设发电站的成本。

此方法的最小代价正确性显然,因为prim算法可保证生成树为最小生成树。

证明每个联通分量都至少有一个发电站:

1、产生联通分量的条件是有一个地区的自建代价小于修路的代价,所以每个联通分量必有发电站

2、采用虚源点的方式确保会建设一个代价最小的发电站,若全地区联通的情况下保证有且仅有一个发电站。

可以采用堆优化的方法,从而使时间复杂度达到0(N*logE)

代码:

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100001,INF = 9999999;
typedef struct node
{
    int to,cost;
}link;
vector<link>a[MAXN];
int mark[MAXN],s[MAXN];
long long int lowcost[MAXN];
priority_queue<pair<long long int ,int > >q;
int main()
{
    ios::sync_with_stdio(0);cin.tie(0);
    int n,m,k,u,v;
    link t;
    cin>>n>>m;
    for (int i = 1;i<=n;i++)
    {
        mark[i] = 0;
        lowcost[i] = INF;
        t.to = i;cin>>t.cost;
        a[0].push_back(t);              //虚源点
    }
    for (int i = 1;i<=m;i++)
    {
        cin>>k>>t.to>>t.cost;
        a[k].push_back(t);       //双向边
        swap(k,t.to);
        a[k].push_back(t);
    }
    lowcost[0] = 0;
    q.push(make_pair(0,0));
    while (!q.empty())            //堆优化
    {
        u = q.top().second;
        q.pop();
        if (mark[u])    continue;
        mark[u] = 1;
        for (int j = 0;j<int(a[u].size());j++)
        {
            v = a[u][j].to;
            if (a[u][j].cost<lowcost[v]&&!mark[v])
            {
                lowcost[v] = a[u][j].cost;
                q.push(make_pair(-lowcost[v],v));
            }
        }
    }
    int ans = 0;
    for (int i = 1;i<=n;i++)
    {
        ans+=lowcost[i];
    }
    cout<<ans;
    return 0;
}

反思:

但是考试时没用虚源点而直接使用堆优化,两个答案错误。但是不使用堆优化则答案正确。

我想原因是出在 lowcost[n] 的选最小上。因为此题所给的图不一定是强连通图,所以堆优化可能只探测完一个联通分支就结束掉!

然而不采用堆优化时,选取最小是一个n次循环,肯定会选到最小值,从而探测到全部的联通分量。

采用虚源点时,由于虚源点到所有节点都有一条边,所以图变成了强连通图。

不加虚源点的改进方法:将所有的lowcast提前放入堆中即可!

另外堆优化prim是确实有效的,时间节省了十几倍。

题目四: 7-4 发红包 (100 分)

题目描述:

新年到了,公司要给员工发红包。员工们会比较获得的红包,有些员工会有钱数的要求,例如,c1的红包钱数要比c2的多。每个员工的红包钱数至少要发888元,这是一个幸运数字。

公司想满足所有员工的要求,同时也要花钱最少,请你帮助计算。

输入格式:

第1行,两个整数n和m(n<=10000,m<=20000),用空格分隔,分别代表员工数和要求数。

接下来m行,每行两个整数c1和c2,用空格分隔,表示员工c1的红包钱数要比c2多,员工的编号1~n 。

输出格式:

一个整数,表示公司发的最少钱数。如果公司不能满足所有员工的需求,输出-1.

输入样例:

在这里给出一组输入。例如:

 2 1 
 1 2

输出样例:

在这里给出相应的输出。例如:

1777

题目分析:

若采用算法如bfs,可以遍历一个点,将其所有后序点权值赋值为该点权值+1与后续点权值的最大值,一次类推,即可完成题目要求。但是此方法遍历点时无任何限制,导致时间复杂度为O(e^{2}),十分不理想。可舍弃。

首先为了计算方便,可以考虑逆向建图的方法,将大于关系改为小于关系,从而从最小值888开始计算。在遍历的时候根据小于关系,若一点无前序节点,即为最小值888,并接触与其后序节点的小于限制;若一个节点有且仅有一个前序节点,该节点值即为前序节点值加一。以此原则逐一接触小于关系并给节点赋值,即可求出总体的最小值。

可以看出,这便是拓扑排序的思想。且此方法时间复杂度为O(n+e)

若有环形关系时,处理到到仅剩该环时,算法会提前结束。

代码:

#include <iostream>
#include <vector>
#include <stack>
using namespace std;
const int MAXN = 10001;
stack<int>st;
vector<int>a[MAXN];
int money[MAXN];
int Count[MAXN];
int main()
{
    ios::sync_with_stdio(0);
    cin.tie(0);
    int n,m,t1,t2;
    int j,k,u;
    cin>>n>>m;
    for (int i = 1;i<=m;i++)
    {
        cin>>t1>>t2;
        a[t2].push_back(t1);                        //反向建图
    }
    for (int i = 1;i<=n;i++)
    {
        Count[i] = 0;
    }
    for (int i = 1;i<=n;i++)
    {
        for (int j = 0;j<int(a[i].size());j++)
        {
            Count[a[i][j]]++;                        //统计入度
        }
    }
    for (int i = 1;i<=n;i++)
    {
        if (!Count[i])
        {
            st.push(i);                                //入度为0的点入栈
            money[i] = 888;
        }
    }
    for (int i = 1;i<=n;i++)                            //n次输出节点
    {
        if (st.empty())                                    //栈空,则有环路,算法结束
        {
            cout<<"-1";
            return 0;
        }
         j = st.top();st.pop();                        //弹出一个入度为0的点
        for (k = 0;k<int(a[j].size());k++)             //对该点所有后序节点接触关系
        {
             u = a[j][k];
            Count[u]--;                              
            if (!Count[u])                      //碰到一个有直接关系的点,该点入栈
            {
                st.push(u);
                money[u] = 1 + money[j];        //赋值
            }
        }
    }
    int ans = 0;
    for (int i = 1;i<=n;i++)
    {
        ans += money[i];
    }
    cout<<ans;
    return 0;
}

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值