第七次练习赛解题报告及标程

本次算法练习包含多个难度级别的题目,涉及图论、数据结构及算法等核心内容。解析包括最短路径、二分查找、链表操作、最小生成树及拓扑排序等问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

  这次的题目难度梯度比较大,签到题很水,最难的题也很难;童鞋们也可以看到其中一些题直接取自各大OJ的ACM题,所以总体难度可以说是历次上机题和练习题之最。因为这次练习的过题数计入平时成绩,可以看作一次超长时长的上机,最终board的情况可以说在意料之中,但也略微超出了一点预估;希望成绩中没有很大的水分。

  A. 邀请函

  难度中等偏下。求有向图中一个点出发到所有点再返回的最短路之和。我们注意到从一个点到所有点就是纯粹的单源最短路;而从所有点返回一个点,倘若我们把图中所有的边反向,就又变成了单源最短路。所以这道题的做法就是对原图和反图各求一遍单源最短路。这里请童鞋们自行区分一下反图和补图的概念。

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int MAXN=1005;
const int INF=0x3f3f3f3f;
int g[MAXN][MAXN],dist[MAXN],n;
bool vis[MAXN];
void dijkstra(int src)
{
    memset(dist,0x3f,sizeof(dist));
    memset(vis,false,sizeof(vis));
    dist[src]=0;
    for(int i=1; i<=n; ++i)
    {
        pair<int,int> tmp=make_pair(INF,-1);
        for(int j=1; j<=n; ++j)
            if(!vis[j]&&dist[j]<tmp.first)
                tmp=make_pair(dist[j],j);
        if(!~tmp.second)
            break;
        vis[tmp.second]=true;
        for(int j=1; j<=n; ++j)
            dist[j]=min(dist[j],tmp.first+g[tmp.second][j]);
    }
}
int main()
{
    int m,u,v,l;
    while(~scanf("%d%d",&n,&m))
    {
        memset(g,0x3f,sizeof(g));
        for(int i=1; i<=n; ++i)
            g[i][i]=0;
        while(m--)
        {
            scanf("%d%d%d",&u,&v,&l);
            g[u][v]=min(g[u][v],l);
        }
        int ans=0;
        dijkstra(1);
        for(int i=1; i<=n; ++i)
            ans+=dist[i];
        for(int i=1; i<=n; ++i)
            for(int j=i+1; j<=n; ++j)
                swap(g[i][j],g[j][i]);
        dijkstra(1);
        for(int i=1; i<=n; ++i)
            ans+=dist[i];
        printf("%d\n",ans);
    }
}
#include<cstdio>
#include<cstring>
#include<queue>
using namespace std;
const int MAXN=1005;
const int MAXM=100005;
struct graph
{
    int head[MAXN];
    int to[MAXM];
    int next[MAXM];
    int len[MAXM];
    int tot;
    void init()
    {
        tot=0;
        memset(head,0xff,sizeof(head));
    }
    void add(int u,int v,int w)
    {
        to[tot]=v;
        len[tot]=w;
        next[tot]=head[u];
        head[u]=tot++;
    }
} g,rg;
int dist[MAXN];
bool inque[MAXN];
void spfa(graph &G,int src)
{
    memset(dist,0x3f,sizeof(dist));
    memset(inque,false,sizeof(inque));
    dist[src]=0;
    queue<int> q;
    q.push(src);
    inque[src]=true;
    while(!q.empty())
    {
        int x=q.front();
        q.pop();
        inque[x]=false;
        for(int i=G.head[x]; ~i; i=G.next[i])
        {
            int y=G.to[i];
            if(dist[x]+G.len[i]<dist[y])
            {
                dist[y]=dist[x]+G.len[i];
                if(!inque[y])
                {
                    q.push(y);
                    inque[y]=true;
                }
            }
        }
    }
}
int main()
{
    int n,m,u,v,l;
    while(~scanf("%d%d",&n,&m))
    {
        g.init();
        rg.init();
        while(m--)
        {
            scanf("%d%d%d",&u,&v,&l);
            g.add(u,v,l);
            rg.add(v,u,l);
        }
        int ans=0;
        spfa(g,1);
        for(int i=1; i<=n; ++i)
            ans+=dist[i];
        spfa(rg,1);
        for(int i=1; i<=n; ++i)
            ans+=dist[i];
        printf("%d\n",ans);
    }
}

  B. 寻找下界

  这道题我预谋已久了……题目原打算直接起名叫lower_bound,后来不希望大家使用STL,故而改成不伦不类的中文名;最后干脆直接禁掉了可能利用lower_bound()函数的所有头文件。原因很简单,二分虽然是每一个程序猿必须掌握的技巧,但又快又准地写好二分同样是一个学问,找一个序列中的某个数只不过是最基础的操作罢了。这道题,找的就是大于等于某个数的第一个数,同理,希望大家能自行练习寻找一个序列中大于某个数的第一个数(即upper_bound()函数)。话不多说,扔上三种姿势。

#include<cstdio>
using namespace std;
const int MAXN=100005;
int a[MAXN],n;
int lower_bound(int x)
{
    int l=0,r=n;
    while(l<r)
    {
        int m=l+r>>1;
        x>a[m]?l=m+1:r=m;
    }
    return l;
}
int main()
{
    int m,k;
    while(~scanf("%d%d",&n,&m))
    {
        for(int i=0; i<n; ++i)
            scanf("%d",&a[i]);
        while(m--)
        {
            scanf("%d",&k);
            int idx=lower_bound(k);
            printf("%d\n",idx==n?-1:a[idx]);
        }
    }
}
#include<cstdio>
using namespace std;
const int MAXN=100005;
int a[MAXN],n;
int lower_bound(int x)
{
    int l=0,r=n-1;
    while(l<=r)
    {
        int m=l+r>>1;
        x>a[m]?l=m+1:r=m-1;
    }
    return l;
}
int main()
{
    int m,k;
    while(~scanf("%d%d",&n,&m))
    {
        for(int i=0; i<n; ++i)
            scanf("%d",&a[i]);
        while(m--)
        {
            scanf("%d",&k);
            int idx=lower_bound(k);
            printf("%d\n",idx==n?-1:a[idx]);
        }
    }
}
#include<cstdio>
#include<algorithm>
using namespace std;
const int MAXN=100005;
int a[MAXN];
int main()
{
    int n,m,k;
    while(~scanf("%d%d",&n,&m))
    {
        for(int i=0; i<n; ++i)
            scanf("%d",&a[i]);
        while(m--)
        {
            scanf("%d",&k);
            int idx=lower_bound(a,a+n,k)-a;
            printf("%d\n",idx==n?-1:a[idx]);
        }
    }
}

  C. Cache

  因为期末上机考试的临近,我们出了这道题,算是对链表操作的回顾。这道题其意义在于考查了链表链节的合并和分离操作,倘若可以熟练处理这两个操作,其实就具备了写块状链表的一切知识和技巧。块状链表不在课程要求内,但有心搞ACM的童鞋不妨深入探究一下,自行查找资料并手动实现出块状链表的结构和功能。

  因为有些童鞋说这道题用STL写各种RE,于是我就写了一个可以AC的STL版本供参考。

#include<iostream>
#include<string>
#include<list>
using namespace std;
list<string> data;
list<string>::iterator cur,tmp;
int main()
{
    int n,m,k;
    while(cin>>n>>m)
    {
        string str,op;
        while(n--)
        {
            cin>>str;
            data.push_back(str);
        }
        cur=data.begin();
        while(m--)
        {
            cin>>op;
            switch(op[4])
            {
            case 'R':
                ++cur;
                if(cur==data.end())
                    --cur;
                break;
            case 'L':
                if(cur!=data.begin())
                    --cur;
                break;
            case 'I':
                tmp=cur;
                ++tmp;
                if(tmp!=data.end())
                {
                    (*cur)+=(*tmp);
                    data.erase(tmp);
                }
                break;
            case 'D':
                cin>>k;
                str=(*cur).substr(k,(*cur).size()-k);
                (*cur)=(*cur).substr(0,k);
                tmp=cur;
                ++tmp;
                data.insert(tmp,str);
                break;
            }
        }
        for(tmp=data.begin(); tmp!=data.end(); ++tmp)
            cout<<(*tmp)<<' ';
        cout<<'\n'<<(*cur)<<'\n';
        data.clear();
    }
}

  D. 行者无疆

  从过题数可以看出,这是毫无疑问的最难题;大概一个多月前的一次训练赛,我和Thor合力搞了将近一个小时才做出来,不过当时那道题时限更严苛,后来我们总结了最短路的一些时间复杂度上的经验并写在了这篇blog里。我的做法是假设起点和终点也有充电桩,然后以每个充电桩为起点跑一遍单源最短路,接着把所有的充电桩放在一起重新建图,倘若之间的最短路径小于要求则有一条边,然后再从起点跑一遍单源最短路即可。当然,这道题也可以直接修改dijkstra,理论上速度会更快,但显然从思考难度上又高了一些,这里不再讨论。

  因为有两次建图且分别求了单源最短路,第一次是稀疏图,第二次是稠密图,所以我多写了一种用spfa处理第一次建图的写法,因为稀疏图下spfa相对于dijkstra的优势还是比较明显的,而稠密图下又会慢于dijkstra。

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int MAXN=305;
const int INF=0x3f3f3f3f;
int city[MAXN][MAXN],pile[MAXN][MAXN],s[MAXN],dist[MAXN];
bool vis[MAXN];
void dijkstra(int g[][MAXN],int n,int src)
{
    memset(dist,0x3f,sizeof(dist));
    memset(vis,false,sizeof(vis));
    dist[src]=0;
    for(int i=1; i<=n; ++i)
    {
        pair<int,int> tmp=make_pair(INF,-1);
        for(int j=1; j<=n; ++j)
            if(!vis[j]&&dist[j]<tmp.first)
                tmp=make_pair(dist[j],j);
        if(!~tmp.second)
            break;
        vis[tmp.second]=true;
        for(int j=1; j<=n; ++j)
            dist[j]=min(dist[j],tmp.first+g[tmp.second][j]);
    }
}
int main()
{
    int n,m,v,k,x,y,d;
    while(~scanf("%d%d%d",&n,&m,&v))
    {
        memset(city,0x3f,sizeof(city));
        for(int i=1; i<=n; ++i)
            city[i][i]=0;
        memset(pile,0x3f,sizeof(pile));
        while(m--)
        {
            scanf("%d%d%d",&x,&y,&d);
            if(d<city[x][y])
                city[x][y]=city[y][x]=d;
        }
        scanf("%d",&k);
        for(int i=1; i<=k; ++i)
            scanf("%d",&s[i]);
        s[++k]=1;
        s[++k]=n;
        for(int i=1; i<=k; ++i)
            pile[i][i]=0;
        for(int i=1; i<=k; ++i)
        {
            dijkstra(city,n,s[i]);
            for(int j=i+1; j<=k; ++j)
            {
                int d=dist[s[j]];
                if(d>v*5)
                    d=INF;
                pile[i][j]=pile[j][i]=d;
            }
        }
        dijkstra(pile,k,k-1);
        printf("%d\n",dist[k]==INF?-1:dist[k]);
    }
}
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
const int MAXN=305;
const int MAXM=20005;
const int MAXK=55;
const int INF=0x3f3f3f3f;
struct graph
{
    int head[MAXN];
    int to[MAXM];
    int next[MAXM];
    int len[MAXM];
    int tot;
    void init()
    {
        tot=0;
        memset(head,0xff,sizeof(head));
    }
    void add(int u,int v,int w)
    {
        to[tot]=v;
        len[tot]=w;
        next[tot]=head[u];
        head[u]=tot++;
    }
} city;
int dist[MAXN];
bool inque[MAXN];
void spfa(int src)
{
    memset(dist,0x3f,sizeof(dist));
    memset(inque,false,sizeof(inque));
    dist[src]=0;
    queue<int> q;
    q.push(src);
    inque[src]=true;
    while(!q.empty())
    {
        int x=q.front();
        q.pop();
        inque[x]=false;
        for(int i=city.head[x]; ~i; i=city.next[i])
        {
            int y=city.to[i];
            if(dist[x]+city.len[i]<dist[y])
            {
                dist[y]=dist[x]+city.len[i];
                if(!inque[y])
                {
                    q.push(y);
                    inque[y]=true;
                }
            }
        }
    }
}
int pile[MAXK][MAXK],s[MAXK],k;
bool vis[MAXK];
void dijkstra(int src)
{
    memset(dist,0x3f,sizeof(dist));
    memset(vis,false,sizeof(vis));
    dist[src]=0;
    for(int i=1; i<=k; ++i)
    {
        pair<int,int> tmp=make_pair(INF,-1);
        for(int j=1; j<=k; ++j)
            if(!vis[j]&&dist[j]<tmp.first)
                tmp=make_pair(dist[j],j);
        if(!~tmp.second)
            break;
        vis[tmp.second]=true;
        for(int j=1; j<=k; ++j)
            dist[j]=min(dist[j],tmp.first+pile[tmp.second][j]);
    }
}
int main()
{
    int n,m,v,x,y,d;
    while(~scanf("%d%d%d",&n,&m,&v))
    {
        city.init();
        while(m--)
        {
            scanf("%d%d%d",&x,&y,&d);
            city.add(x,y,d);
            city.add(y,x,d);
        }
        scanf("%d",&k);
        for(int i=1; i<=k; ++i)
            scanf("%d",&s[i]);
        s[++k]=1;
        s[++k]=n;
        memset(pile,0x3f,sizeof(pile));
        for(int i=1; i<=k; ++i)
            pile[i][i]=0;
        for(int i=1; i<=k; ++i)
        {
            spfa(s[i]);
            for(int j=i+1; j<=k; ++j)
            {
                int d=dist[s[j]];
                if(d>v*5)
                    d=INF;
                pile[i][j]=pile[j][i]=d;
            }
        }
        dijkstra(k-1);
        printf("%d\n",dist[k]==INF?-1:dist[k]);
    }
}

  E. 多层礼包

  水题,对栈的复习,不用栈也能做,不再多说。

#include<cstdio>
#include<stack>
using namespace std;
char str[105];
int main()
{
    while(~scanf("%s",&str))
    {
        stack<char> s;
        for(int i=0; str[i]!='G'; ++i)
            switch(str[i])
            {
            case '[':
                s.push(str[i]);
                break;
            case ']':
                if(!s.empty())
                    s.pop();
                break;
            }
        printf("%d\n",s.size());
    }
}
#include<cstdio>
using namespace std;
char str[105];
int main()
{
    while(~scanf("%s",&str))
    {
        int ans=0;
        for(int i=0; str[i]!='G'; ++i)
            switch(str[i])
            {
            case '[':
                ++ans;
                break;
            case ']':
                if(ans>0)
                    --ans;
                break;
            }
        printf("%d\n",ans);
    }
}

  F. 无线网络

  这道题的难度在于思路。题目给出了所有点的坐标,然后很多人就不知道这是一道什么类型的题了;实质上是给出了一个完全图。然后我们注意到,实现任意寝室的互联,并不需要直接互联,换言之,任意寝室的互联不过寻找一个最小生成树。然后是如何处理电力猫的问题,我们注意到电力猫相当于超级版的路由器,实现路由器的功能又不考虑距离,所以假设有k个电力猫,实质上我们是在找最小生成树的第k大边的长度。更详细的讨论可以看POJ原题的Discuss;更详细的证明需要讨论最小生成树和连通分量的关系,可以看这篇文章的第11~12页。此外,这道题是一个完全图,换言之是个非常稠密的图,用prim是比kruskal好得多的选择,虽然我控制了数据量让它们都能过。

#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
using namespace std;
const int MAXN=505;
const int INF=0x3f3f3f3f;
bool vis[MAXN];
double g[MAXN][MAXN],lowc[MAXN],len[MAXN];
int n,k,x[MAXN],y[MAXN];
inline int sqr(int x)
{
    return x*x;
}
double prim()
{
    memset(vis,false,sizeof(vis));
    for(int i=1; i<=n; ++i)
        lowc[i]=g[1][i];
    vis[1]=true;
    int cnt=0;
    for(int i=1; i<n; ++i)
    {
        int mark=-1;
        double minc=INF;
        for(int j=1; j<=n; ++j)
            if(!vis[j]&&minc>lowc[j])
            {
                minc=lowc[j];
                mark=j;
            }
        if(!~mark)
            return -1;
        len[cnt++]=minc;
        vis[mark]=true;
        for(int j=1; j<=n; ++j)
            if(!vis[j]&&lowc[j]>g[mark][j])
                lowc[j]=g[mark][j];
    }
    sort(len,len+cnt);
    for(int i=0;i<cnt;++i)
        printf("%.2f ",len[i]);
    putchar('\n');
    return len[cnt-k];
}
int main()
{
    while(~scanf("%d%d",&n,&k))
    {
        for(int i=1; i<=n; ++i)
            scanf("%d%d",&x[i],&y[i]);
        for(int i=1; i<=n; ++i)
            for(int j=i; j<=n; ++j)
                g[i][j]=g[j][i]=sqrt(sqr(x[i]-x[j])+sqr(y[i]-y[j]));
        printf("%.2f\n",prim());
    }
}
#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
using namespace std;
const int MAXN=505;
const int MAXM=MAXN*MAXN;
inline int sqr(int x)
{
    return x*x;
}
struct edge
{
    int u,v;
    double w;
    edge(int _u=0,int _v=0,double _w=0):u(_u),v(_v),w(_w) {}
    bool operator<(const edge &oth) const
    {
        return w<oth.w;
    }
} e[MAXM];
int n,m,k,x[MAXN],y[MAXN],u[MAXN];
void init()
{
    for(int i=1; i<=n; ++i)
        u[i]=i;
}
int find(int x)
{
    if(u[x]!=x)
        u[x]=find(u[x]);
    return u[x];
}
void merge(int x,int y)
{
    u[find(x)]=find(y);
}
double kruskal()
{
    if(n-k==0)
        return 0;
    sort(e,e+m);
    int cnt=0;
    init();
    for(int i=0; i<m; ++i)
        if(find(e[i].u)!=find(e[i].v))
        {
            merge(e[i].u,e[i].v);
            if(++cnt==n-k)
                return e[i].w;
        }
}
int main()
{
    while(~scanf("%d%d",&n,&k))
    {
        for(int i=1; i<=n; ++i)
            scanf("%d%d",&x[i],&y[i]);
        m=0;
        for(int i=1; i<=n; ++i)
            for(int j=i+1; j<=n; ++j)
            {
                double l=sqrt(sqr(x[i]-x[j])+sqr(y[i]-y[j]));
                e[m++]=edge(i,j,l);
                e[m++]=edge(j,i,l);
            }
        printf("%.2f\n",kruskal());
    }
}

  G. barty的智商

  首先说句题外话,barty是北航有史以来第一支也是目前唯一一支ACM Final队的成员,是GG的队友,专攻图论。这道题也是曾经的一道校赛原题,然而它并不难。它突出的思想是如何把一个求解性问题转化成验证性问题。对于这道题,我们寻找满足要求的最低智商是困难的,但对于某一个智商值,我们判断它是否满足要求是容易的,只要跑一遍拓扑排序就可以了。于是可以产生二分答案的思想,就是在智商值的范围内进行二分,对于每一个值进行验证;二分过程和B题极其相似,也是在找一个下界(从而也可以看出对于二分,像书上单纯的查找某个值是否存在是最基础的,而B题的二分方式同样是有意义的)。

  验证的时候可以选择建立新图,这里使用了vector模拟邻接表。

#include<cstdio>
#include<cstring>
#include<vector>
#include<queue>
using namespace std;
const int MAXN=10005;
const int INF=0x3f3f3f3f;
vector<pair<int,int> > G[MAXN];
vector<int> g[MAXN];
int du[MAXN],n;
bool toposort()
{
    memset(du,0,sizeof(du));
    for(int i=1; i<=n; ++i)
        for(int j=0; j<g[i].size(); ++j)
            ++du[g[i][j]];
    int tot=0;
    queue<int> q;
    for(int i=1; i<=n; ++i)
        if(!du[i])
            q.push(i);
    while(!q.empty())
    {
        int u=q.front();
        q.pop();
        ++tot;
        for(int i=0; i<g[u].size(); ++i)
        {
            int v=g[u][i];
            if(!(--du[v]))
                q.push(v);
        }
    }
    return tot==n;
}
int main()
{
    int t,m,a,b,c;
    scanf("%d",&t);
    while(t--)
    {
        scanf("%d%d",&n,&m);
        for(int i=1; i<=n; ++i)
            G[i].clear();
        while(m--)
        {
            scanf("%d%d%d",&a,&b,&c);
            G[b].push_back(make_pair(a,c));
        }
        int l=0,r=INF;
        while(l<r)
        {
            int m=l+r>>1;
            for(int i=1; i<=n; ++i)
            {
                g[i].clear();
                for(int j=0; j<G[i].size(); ++j)
                    if(G[i][j].second>m)
                        g[i].push_back(G[i][j].first);
            }
            toposort()?r=m:l=m+1;
        }
        printf("%d\n",l);
    }
}

  也可以不建新图,对拓扑排序稍作改动,用前向星来写的邻接表。

#include<cstdio>
#include<cstring>
#include<queue>
using namespace std;
const int MAXN=10005;
const int MAXM=10005;
const int INF=0x3f3f3f3f;
struct graph
{
    int head[MAXN];
    int to[MAXM];
    int next[MAXM];
    int len[MAXM];
    int tot;
    void init()
    {
        tot=0;
        memset(head,0xff,sizeof(head));
    }
    void add(int u,int v,int w=-1)
    {
        to[tot]=v;
        len[tot]=w;
        next[tot]=head[u];
        head[u]=tot++;
    }
} g;
int du[MAXN],n;
bool toposort(int k)
{
    memset(du,0,sizeof(du));
    for(int i=1; i<=n; ++i)
        for(int j=g.head[i]; ~j; j=g.next[j])
            if(g.len[j]>k)
                ++du[g.to[j]];
    int tot=0;
    queue<int> q;
    for(int i=1; i<=n; ++i)
        if(!du[i])
            q.push(i);
    while(!q.empty())
    {
        int u=q.front();
        q.pop();
        ++tot;
        for(int i=g.head[u]; ~i; i=g.next[i])
            if(g.len[i]>k)
            {
                int v=g.to[i];
                if(!(--du[v]))
                    q.push(v);
            }
    }
    return tot==n;
}
int main()
{
    int t,m,a,b,c;
    scanf("%d",&t);
    while(t--)
    {
        scanf("%d%d",&n,&m);
        g.init();
        while(m--)
        {
            scanf("%d%d%d",&a,&b,&c);
            g.add(a,b,c);
        }
        int l=0,r=INF;
        while(l<r)
        {
            int m=l+r>>1;
            toposort(m)?r=m:l=m+1;
        }
        printf("%d\n",l);
    }
}

  总的来说,我想给所有坚持独立完成题目的童鞋点个赞,这次的题目可做性很强,但也确实很有难度,坚持做下来的人很值得鼓励。期末考试临近,希望大家好好复习重点的数据结构和算法,期末上机和笔试加油~(虽然上机是我们出题,笔试我们貌似也要参与判卷or合分……)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值