【研一课程】算法设计与分析 FInal OJ解题报告

算法设计与分析 课程 FInal OJ解题报告

第一题Traveler——即AtCoder Beginner Contest 197 E-Traveler Editorial

  • 题意:机器人站在一个一维的数轴的原点位置;有N个球分布在数轴上,这些球所处的位置 X i X_i Xi不一定是正数,同时每个球都有自己的颜色 C i C_i Ci C i C_i Ci的范围是从1到 N N N;现让机器人触发,按照各球的 C i C_i Ci递增的顺序把所有小球全部捡起来,最后回到原点!输出,最少需要走多少距离?所有的输入都是整数,各个球的位置各不相同,而同色的球可以有多个~

  • 题目链接:AtCoder Beginner Contest 197 E-Traveler Editorial

  • 题目类型: DP,动态规划。

  • 解题思路

    • 错误解法①——把所有球从小颜色开始捡,相同的颜色从最左边开始捡。 反例:(数字表示球的颜色,0表示原点) 0 − 1 − 2 − 1 − 2 0-1-2-1-2 01212,按照①的思路,是走完两个1以后,往左边走捡第一个2,然后往右边走捡第二个2,然后回起点;但实际上,最快的是走完两个1以后,先把最右边的2捡了,然后再回原点0的路上把第一个2给捡了。
    • 错误解法②——在①的基础上,进行分类讨论,分成多种情况来判断当我们把目前颜色捡完了要捡下一个颜色时是往左走还是往右走。实操了以后发现很麻烦,遂罢休。
    • 正确解法:(看题解后理解的( ̄▽ ̄)"…)按照上面的思路,问题最难办的是,当一个颜色的球收集完以后,面对下一组新颜色的球,到底是从最左方向的球开始捡还是从最右方向的球开始捡?同时,这个问题有趣的是,每当我们要进行决策这个问题的时候,之前收集完毕的球(颜色标号小的那些球)是不会影响我们当前的决策的!
      • 所以我们可以用DP来解决此问题—— d p [ i ] [ [ j ] dp[i][[j] dp[i][[j] 表示从最左边开始( j = 0 j=0 j=0 ) 收完所有 i i i 颜色的球以后,需要最少(注意,是”最少“)走多少步数、或从最右边开始( j = 1 j=1 j=1 ) 收完所有 i i i 颜色的球以后最少需要走多少步数.
        • 定义 c o l o r [ i ] [ 0 ] color[i][0] color[i][0]表示颜色 i i i的球第一个出现的位置,和 c o l o r [ i ] [ 1 ] color[i][1] color[i][1]表示颜色 i i i的球最后出现的位置。
        • 状态转移方程: 可以画画草稿图,可以轻易的得到,每一个颜色的收集结果一共4种情况:上一次从左边收这次从左边收;上一次从右边收这次从左边收;上次从左边收这次从右边收;上次从右边收这次也从右边开始收集球。因此可以得到—— d p [ i ] [ 0 ] = m i n { d p [ i − 1 ] [ 0 ] + a b s ( c o l o r [ i − 1 ] [ 1 ] − c o l o r [ i ] [ 0 ] ) , d p [ i − 1 ] [ 1 ] + a b s ( c o l o r [ i − 1 ] [ 0 ] − c o l o r [ i ] [ 0 ] ) } + c o l o r [ i ] [ 1 ] − c o l o r [ i ] [ 0 ] dp[i][0]=min\{ dp[i-1][0] + abs(color[i-1][1]-color[i][0]), dp[i-1][1] + abs(color[i-1][0]-color[i][0]) \} + color[i][1]-color[i][0] dp[i][0]=min{dp[i1][0]+abs(color[i1][1]color[i][0]),dp[i1][1]+abs(color[i1][0]color[i][0])}+color[i][1]color[i][0];以及 d p [ i ] [ 1 ] = m i n ( d p [ i − 1 ] [ 0 ] + a b s ( c o l o r [ i − 1 ] [ 1 ] − c o l o r [ i ] [ 1 ] ) , d p [ i − 1 ] [ 1 ] + a b s ( c o l o r [ i − 1 ] [ 0 ] − c o l o r [ i ] [ 1 ] ) ) + c o l o r [ i ] [ 1 ] − c o l o r [ i ] [ 0 ] dp[i][1]=min( dp[i-1][0] + abs(color[i-1][1]-color[i][1]), dp[i-1][1] + abs(color[i-1][0]-color[i][1]) ) + color[i][1]-color[i][0] dp[i][1]=min(dp[i1][0]+abs(color[i1][1]color[i][1]),dp[i1][1]+abs(color[i1][0]color[i][1]))+color[i][1]color[i][0].
        • 别忘了最后一个颜色的球收集完毕要回到原点!
  • 知识点c语言里的 a b s ( ) abs() abs() 函数是在头文件stdlib.h里的,并不在cmath,如果只加入了后者库,调用 a b s ( ) abs() abs() 函数返回值是0.因为默认使用 c m a t h cmath cmath的函数返回值都是double类型的,进行整数求绝对值过程回变成0或其他意想不到的结果!

  • 题外话

    • 有时候就在纠结,贪心和DP常常分不太清楚,毕竟很多时候思想都是”在当前阶段做最好的选择“,就会有点迷惑。我想了想,我自己的理解是:在这个逻辑上,贪心更侧重于”直接执行更好的选择“,这个”更好“的定义是我们可以从”决策逻辑”、“行为“上就能预见谁更好的,比如“选最大的石头”、“选最大的梨子”;而DP是”在诸多选择中选择最好的那个“,有一种遍历所有结果以后选择更好解的感觉,我们不能从”决策逻辑“、”行为“上提前预见,而是从“结果”上选择结果,所以dp的状态转移方程的min会有很多个值,甚至有一个for语句遍历所有可能然后选择min或max的“更好”的那个!
    • 上面这个问题是“dp和贪心在选择当前解”的思考逻辑上有什么不同?但我有时候还会纠结另一个问题,贪心是“在当前阶段做出最好的选择”,dp在状态转移的时候其实也是基于“在当前阶段做出最好的选择”,那么凭什么贪心算法得到的solution在某些场合是局部最优,而dp却能保证是全局最优呢?我想了想,想出了自己的理解:首先,贪心算法在每个阶段,确实是做出了“当前阶段最好的选择”,但缺陷是,你并不能保证“每一次的当前阶段最优的选择一定能保证所有阶段合拢下也是最优的”,所以贪心算法的本质是 “在之前阶段的局部最优的结果上,进行局部最优的选择,得到下一个局部最优的solution” 的过程。所以在算法理论课上,贪心算法的正确性需要用“数学归纳法”或“反证法”来证明它的正确性,比较麻烦。而DP算法的特点是,它的每一个状态函数 d p [ i − 1 ] dp[i-1] dp[i1] ,在定义都是“前 i − 1 i-1 i1 子问题下的最优解”!比如背包问题的 d p [ i ] [ j ] dp[i][j] dp[i][j] 表示的是 i i i大小包装前 j j j个物品的最大容量(最优解)!人家DP的结果函数的这个定义就和贪心不同,它强调了,我们求出来的 i − 1 i-1 i1 子问题已经是最优了,虽然“我”DP也是“在当前子问题 i i i下做出最优的选择”,但“我”DP能保证这么搞出来的子问题 i i i 也是 “最优解” ,但是贪心算法就没这份底气。因此,DP算法的本质是 “在之前阶段的全局最优的结果上,进行最优的选择(可能看起来也是局部最优),得到的是下一个全局最优的solution” 的过程。当然,DP问题两要素:最优子问题结构、子问题重叠。刚刚这个讨论只针对了第一点。一定要强化理解DP算法的这两个特征
  • 代码Code:

#include<cstdio>
#include<cstdlib>
#include<vector>
#include<queue>
using namespace std;
#define inf 200005
#define INF 0x3f3f3f3f

int n;
int color[inf][2];
// the accumulation may exceed of int32
long long int dp[inf][2]; //dp[i][0] is the minimum steps after we get every i-color ball from the left, dp[i][1] is the minimum steps after we get every i-color ball from the right
int book[inf];
int main()
{
    int i,j;
    int x,c;
    scanf("%d",&n);
    for(i=1;i<=n;i++)       //init
    {
        color[i][0]=INF;    //the end position of color[i]
        color[i][1]=-INF;   //the begin position of color[i]
    }
    for(i=1;i<=n;i++)        //read data in disorder
    {
        scanf("%d%d",&x,&c);
        book[c]=1;
        if(x<color[c][0])color[c][0]=x; //update the begin position
        if(x>color[c][1])color[c][1]=x; //update the end position
    }

    //begin dp...
    dp[0][0]=dp[0][1]=0;
    for(i=1;i<=n;i++)
    {
        if(!book[i])    // too lazy to introduce a vector to set down which color has shown up. So I take this way~
        {
            dp[i][0]=dp[i-1][0];
            dp[i][1]=dp[i-1][1];
            color[i][0]=color[i-1][0];
            color[i][1]=color[i-1][1];
            continue;
        }
        dp[i][0]=min(dp[i-1][0]+ abs(color[i-1][1]-color[i][0]), dp[i-1][1] + abs(color[i-1][0]-color[i][0]) );
        dp[i][0]+=color[i][1]-color[i][0];
        dp[i][1]=min(dp[i-1][0]+ abs(color[i-1][1]-color[i][1]), dp[i-1][1] + abs(color[i-1][0]-color[i][1]) );
        dp[i][1]+=color[i][1]-color[i][0];
    }
    printf("%lld\n",min(dp[n][0]+abs(color[n][1]),dp[n][1]+abs(color[n][0])));  //return to the origin
    return 0;
}

第二题 Pipes——即Codeforces Round #212 (Div. 2) E Petya and Pipes

  • 题意:给一张有向图构成一个网络流,一共有 N N N个节点,起点是0,汇点是 N − 1 N-1 N1,以 N × N N \times N N×N的形式输入有向图的矩阵。然后给定一个 K K K,问,可以将任意条pipe的容量增大1或更多,但所有增加的值不能超过 K K K,请问这种情况下的最大流能够是多少?所有管道的容量以及K什么的都是整数!

  • 题目链接:Codeforces Round #212 (Div. 2) E Petya and Pipes

  • 题目类型: 最少费用最大流( Minimum Cost Maximum Flow, MCMF

  • 题解:

    • 什么是 最简单的最少费用最大流问题呢? 就是每根pipe上流动的flow都会有一个各自规定的费用,而且是按照”单位flow“算价钱的”单位费用“,流得越多花费越多!MCMF问题就是,在实现能够求得最大流的情况下,总费用是最少的!
    • 如何求解最大流问题呢?由给定的网络流的图,转换成”残余网络图“,然后在这个残余网络图上利用边上的残余流量值,跑FF算法或EK算法就行了。注意,总结一个重点最大流问题是在残余网络上找增广路对应的path然后更新残余网络图!——主要要素是:①在残余网络上;②用边上的residual值;③找增广路的path; ④用path更新残余网络。
    • 那是如何求解MCMF问题呢?由给定的网络流的图,转换成”残余网络图“,然后在这个残余网络图上,利用边上的费用信息,跑最短路算法,寻找一条”从源点到汇点而且是最少费用“的path。注意,这里的重点是:①在残余网络上;②用边上的费用值;③找最短路的path; ④用path更新残余网络。
    • 这题如何转换成MCMF问题呢?题目说可以给任意管子增加 K K K的容量,但是总增加的容量不能超过 K K K,那么就让增加的容量作为费用即可。注意:当两个点之间存在pipe时,才能增加容量,如果两个点之间不存在边,那是不能凭空增加容量的——又不能造管子…( ̄▽ ̄)"
      • 题目给的图读入到内容中,建图时,费用为0!这样算法会优先去跑0费用的管道——即尽量不增加管道的容量。
      • 对于每条存在的pipe,加一条边,容量为 K K K,因为这个管子不可能用超过 K K K次,费用为1。——扩容一格容量就花1费用。
      • 没了,其他都是费用流的板子。每次SPFA都会找出一条最短路(最小费用path),按这个path进行update残余网络值就行了。
    • 注意:当SPFA跑完,而终点的最短路(最小费用)若超过”剩余可增加的管道容量的额度“的话,说明此时已经到达算法结束的时候了!而此时,要单独分别计算”到底能最多增加几个单位容量“来加到结果上
  • 其他本题的收获: 能够让我学习、巩固最大流、费用流的写法:初始化、边的存储等知识点。其中尤其是边的存储内容,让我收益颇丰。关于此题、以及更基础的费用流的学习思考,详见此【帖子】

  • 思路参考来源:

  • 代码Code: 这份费用流、最大流的板子甚好,以后可以复用了。

#include<cstdio>
#include<cstring>
#include<queue>
using namespace std;
#define INF 0x3f3f3f3f
#define inf 55
//the index is from 0 for the whole code
int n,k;
int s,g;        //begin node and the end node
int path[inf];  //record the path of SPFA

queue<int>q;
int book[inf];      //whether the node is in the queue
int dis[inf];       //cal the shortest distance

struct Edge
{
    int to,res,cost;
    int next;   //to achieve link of edges
    void setEdge(int to,int res,int cost)
    {
        this->to=to;
        this->res=res;
        this->cost=cost;
    }
    void setNext(int next)
    {
        this->next=next;
    }
}edge[4*inf*inf];   //we add our own edges, should have more space (is 4* rather than 2*)
int head[inf];  //the first edge of nodes
int inc;      //the next index for storing edge
void addEdge(int s,int g,int res,int cost)
{
    edge[inc].setEdge(g,res,cost);
    edge[inc].setNext(head[s]);   //the origin head[s]
    head[s]=inc;  //update head[s]: the 'first' edge of node (during storing is the last)
    inc++;
}

bool SPFA()
{
    //init
    memset(book,0,sizeof book);
    memset(path,-1,sizeof path);
    for(int i=0;i<n;i++)dis[i]=INF;
    dis[s]=0;   //don't forget to initial the source node
    //begin
    q.push(s);
    book[s]=1;
    while(!q.empty())
    {
        int u=q.front();
        q.pop();
        book[u]=0;
        for(int i=head[u];i!=-1;i=edge[i].next)//i is the index of adjacent edges
        {
            int v=edge[i].to;
            // if the edge can use, meaningful to update
            if(edge[i].res>0&&dis[v]>dis[u]+edge[i].cost)
            {
                dis[v]=dis[u]+edge[i].cost;
                path[v]=i;  //record the index of 'the edge' rather than node in path
                if(!book[v])    //each node may be updated by multiple edges. Thus here should be a judgment
                {
                    q.push(v);
                    book[v]=1;
                }
            }
        }
    }
    if(path[g]==-1)return false;
    return true;
}

int MCMF()
{
    int totalCost=0;
    int maxFlow=0;
    while(SPFA())
    {
        int minRes=INF;
        for(int i=g;i!=s;i=edge[path[i]^1].to)  //i is the index of the end node, j is the index of edges
        {
            int j=path[i];
            if(edge[j].res<minRes)minRes=edge[j].res;
        }
        //dis[g] represents the total cost of per res , rather than max res in the path!!! so we should use
        if(minRes*dis[g]+totalCost>k)   //invalid result, the MCMF is over.
        {   //Every given pipes from the origin question is ran out!!!
            maxFlow+=(k-totalCost)/dis[g];  //how many units of flow can we add more.
            break;
        }
        //update the res of edges
        totalCost+=minRes*dis[g];
        maxFlow+=minRes;
        for(int i=g;i!=s;i=edge[path[i]^1].to)  //i is the index of the end node, j is the index of edges
        {
            int j=path[i];
            edge[j].res-=minRes;
            edge[j^1].res+=minRes;
        }
    }
    return maxFlow;
}

int main()
{
    memset(head,-1,sizeof head);    //it's essential to init the head array. Because 'next' of edges array is set by the initial value of head array.
    //read mp
    scanf("%d%d",&n,&k);
    s=0;
    g=n-1;
    //set residual mp
    for(int i=0;i<n;i++)
        for(int j=0;j<n;j++)
        {
            int res;
            scanf("%d",&res);
            //the cost of reverse edge is a negative number
            if(res){
                addEdge(i,j,res,0);
                addEdge(j,i,0,0);
                //we add another edge, assume that no pipe no new K
                addEdge(i,j,k,1);
                addEdge(j,i,0,-1);
            }
        }
    //MCMF
    printf("%d\n",MCMF());
    return 0;
}

第三题 Edge Spliting——即 AtCoder GC039 B-Graph Partition

  • 题意: 输入一个无向图, N < 200 N<200 N<200,小图,然后让我们输出,这个图最多可以变成“K分图”,的K是多大?不存在就输出-1.

  • 题目链接: 虽然题意相同,但因为输入数据的格式有点不同,所以我就不贴了~

  • 题目类型: 图论和K分图的理解

  • 题解: 可以参看这个博主的博客——AtCoder GC039 B-Graph Partition——CSDN RSHS

    主要是几点:

    • K分图的定义要搞清楚:三角形不能算三分图,因为这题的定义是K分图之间是串联的,各个set之间不能是环状结构!所以三角形要输出-1.
    • 如果一个图连2分图都满足不了,那肯定K分图也做不到!(二分图是最低要求)。所以先判断是否可以二分图,不行就输出-1.
    • K分图的 K最大是多少,就取决于一张图里的不重复边的情况下,最长path的长度! 即K等于图中最长的path长度加1。可以自己画个图理解一下。例如两个点,一条边,那最长的path就是1,那么最大的K就是2,二分图呗。如何求这个最长path呢,用floyd算法跑一下即可。
  • 代码Code:

#include<cstdio>
#include<cstring>
#include<vector>
using namespace std;
#define inf 209
#define INF 0x3f3f3f3f

int n,m;
vector<int>edge[inf];
int mp[inf][inf];
int book[inf];
bool flag=true;

void dfs(int u,int color)
{
    book[u]=color;
    for(int i=0;i<edge[u].size();i++)
    {
        int v=edge[u][i];
        if(book[v]==-1)dfs(v,!color);
        else if(book[v]!=-1&&book[v]!=!color) //not visited
        {
            flag=false;
            return;
        }
    }
}

int main()
{
    int i,j,k;

    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)
            mp[i][j]=INF;
    while(m--)
    {
        int u,v;
        scanf("%d%d",&u,&v);
        edge[u].push_back(v);
        edge[v].push_back(u);
        mp[u][v]=mp[v][u]=1;
    }

    memset(book,-1,sizeof book);
    for(i=1;i<=n;i++)   // whether two-color graph exists
    {
        if(book[i]==-1)
            dfs(i,0);
        if(!flag)
        {
            printf("-1\n");    // not exist bi-partition graph
            return 0;
        }
    }

    //floyd
    for(int k=1;k<=n;k++)
        for(int i=1;i<=n;i++)
            for(int j=1;j<=n;j++)
                if(i!=j&&mp[i][j]>mp[i][k]+mp[k][j])
                    mp[i][j]=mp[i][k]+mp[k][j];
    int maxV=-INF;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)
            if(mp[i][j]!=INF&&mp[i][j]>maxV)
                maxV=mp[i][j];
    printf("%d\n",maxV+1);
    return 0;
}

第四题 Bitcoin Array——即Leetcode 932 Beautiful Array

  • 题意:输入一个数字N,输出由1~N组成的数组的排列情况,要求是对于任何两个数 i < k < j i<k<j i<k<j,这个数组都不存在 A [ k ] × 2 = A [ i ] + A [ j ] A[k] \times 2 = A[i]+A[j] A[k]×2=A[i]+A[j].

  • 题目链接:Leetcode 932 Beautiful Array

  • 题目类型: 分治、构造

  • 解题思路:解题思路网上很多了,所以我就不再此赘述。可以参看各路博客,构造法讲的不错,就是费脑子理解( ̄▽ ̄)"…

  • 经历的bug: 输出格式和 Leetcode 的题目不一样,找了好久bug。

  • 个人理解: 看完他们的思路讲解还是似懂非懂,可以看一份清晰的代码,就懂了,比如看我的( ̄▽ ̄)"…

  • 代码Code: (输出格式和 Leetcode 不同)

#include<cstdio>
#include<vector>
using namespace std;

int n;
vector<int> last;      // store the last solution
vector<int> ans;    // store the now solution

int main()
{
    int i,j,k;
    int len=1;
    last.push_back(1);//initial

    scanf("%d",&n);
    while(1)
    {
        ans.clear();    // clear the vector before every while
        for(i=0;i<last.size();i++) // construct odd numbers
        {
            if(last[i]*2-1<=n) // In case that the number is bigger than n
                ans.push_back(last[i]*2-1);
        }
        for(i=0;i<last.size();i++) // then construct even numbers
        {
            if(last[i]*2<=n)   // In case that the number is bigger than n
                ans.push_back(last[i]*2);
        }
        //move 'ans' to 'e' vector
        last.clear();
        for(i=0;i<ans.size();i++)last.push_back(ans[i]);

        if(ans.size()>=n)break;
    }
    printf("[");
    for(i=0;i<n-1;i++)printf("%d,",ans[i]);
    printf("%d]\n",ans[i]);
    return 0;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值