算法竞赛入门经典——倒水问题

    今天看紫书看到倒水问题,虽说是暴力解法,但其中Dijkstra的影子还是让人费解,对于一些细节,本渣琢磨了好久,终于搞清楚了这个算法的正确性,题目如下:

    有三个杯子,容量分别为a,b,c,最初只有第三个杯子装了c升水,要解决的问题是:最少需要倒多少升水,才能让某一个杯子中有d升水。如果无法做到d升,就做到d'升,使d'尽量接近d。

    由于我自己苦于代码中注释太少,接下来我解释一下我对算法中每步的一些理解。

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

const int maxn=200+5;                                        //水量限制
int vis[maxn][maxn],cap[3] ,ans[maxn];                        //ans[i]记录移动至某个杯子含有i升水所移动的最少水量

struct Node
{
    int v[3];                                                  //将三个杯子的水量依次排开
    int dist;                                                //dist是记录移动水量的变量
    bool operator < (const Node &rhs) const{ return dist > rhs.dist;}    //因为求移动量最小,所以BFS的标准是dist
};

update_ans函数的用途是更新目前状态下达到ans[d]所需的最小移动量。

void update_ans(const Node& u)
{
    for(int i=0;i<3;i++)                              //检查每个杯子的容量,如果小于已知的最小移动量,或者目前还没有达到此状态的方法就更新
    {
        int d=u.v[i];
        if(ans[d]<0||ans[d]>u.dist)
            ans[d]=u.dist;
    }
}

solve主函数

void solve(int a,int b,int c,int d)
{
    cap[0]=a,cap[1]=b,cap[2]=c;                                //cap数组存储的是杯子容量
    memset(vis,0,sizeof(vis));
    memset(ans,-1,sizeof(ans));
    priority_queue<Node> q;                                    //已定义Node型的排序标准

    Node start;
    start.dist=0;
    start.v[0]=0,start.v[1]=0,start.v[2]=c;                    //只有第三个杯子是满的,其他两个都是空的
    q.push(start);

    vis[0][0]=1;                                                //前两个杯子空的状态已经有过了
    while(!q.empty())
    {
        Node u =q.top();                                        //取出目前队列中的移动水量最小的点
        q.pop();
        update_ans(u);
        if(ans[d]>=0) break;
        for(int i=0;i<3;i++)
            for(int j=0;j<3;j++)                                //取任意两个杯子,从i向j里倒水
        if(i!=j)
        {
            if(u.v[i]==0||u.v[j]==cap[j]) continue;             //可以倒水的条件是i杯子不空,j杯子不满
            int amount = min(u.v[i],cap[j]-u.v[j]);                //倒水量既不能多余i现在有的,也不能让j溢出
            Node u2;
            memcpy(&u2,&u,sizeof(u));
            u2.dist=u.dist+amount;
            u2.v[i]-=amount;
            u2.v[j]+=amount;
          
            if(!vis[u2.v[0]][u2.v[1]])                            //如果此状态还未被记录,则加入队列
            {
                q.push(u2);
                vis[u2.v[0]][u2.v[1]]=1;
            }
        }
    }

    while(d>=0)
    {
        if(ans[d]>=0)
        {
            printf("%d\n",ans[d]);
            return w;
        }
        d--;
    }
}

【输出路径】

通篇的代码或许很让人头疼,接下来实现书中输出路径的方案,只标注出需要改动的地方。

起初我在Node结构中添加变量*fa,作为指针指向它的父节点,然后构建一个数组容纳节点状态,结果崩了。。。

解决方案:

    优先队列只存储int型,代表节点在数组中的下标,Node加入一个int型新变量fa,表示它的父状态节点的下标,注意:此时优先队列需要重新写排序函数。数组nodes存储节点,另外由于水量限制,节点状态小于201^2=40401,大小还可以容忍。

int rear=2;
struct Node
{
    int v[3];
    int dist;
    int fa;                        \\父节点下标
};
Node nodes[40401];

struct cmp
{
    bool operator () (const int& a,const int& b) const
    {
        return nodes[a].dist>nodes[b].dist;                //排序方法参考Node型优先队列的
    }
};

update_ans函数内容不变,不影响。

priority_queue<int,vector<int>,cmp> q;

优先队列q,注意排序方式。

int w;
while(!q.empty())
{
    w=q.top();
    Node u = nodes[w];
    //printf("杯中水量: %d %d %d\n",u.v[0],u.v[1],u.v[2]);
    q.pop();
    update_ans(u);
    if(ans[d]>=0) break;
    for(int i=0;i<3;i++)
        for(int j=0;j<3;j++)
    if(i!=j)
    {
        if(u.v[i]==0||u.v[j]==cap[j]) continue;
        int amount = min(u.v[i],cap[j]-u.v[j]);
        //printf("从%d向%d移动%d升水\n",i+1,j+1,amount);
        Node u2;
        memcpy(&u2,&u,sizeof(u));
        u2.dist=u.dist+amount;
        u2.v[i]-=amount;
        u2.v[j]+=amount;
        u2.fa=w;
        //printf("新节点: %d %d %d %d 由 %d %d %d %d 产生\n",u2.v[0],u2.v[1],u2.v[2],u2.dist,u.v[0],u.v[1],u.v[2],u.dist);
        if(!vis[u2.v[0]][u2.v[1]])
        {
            q.push(rear);
            nodes[rear++]=u2;
            vis[u2.v[0]][u2.v[1]]=1;
        }
    }
}

如果你明白了不输出路径的,这段应该不是问题,只是添加fa,然后记得记录fa和节点下标。

将solve改为int型,返回w。

void print_path(int w)
{
    Node u = nodes[w];
    if(!u.fa)
    {
        printf("%d %d %d %d\n",u.v[0],u.v[1],u.v[2],u.dist);
        return;
    }
    print_path(u.fa);
    printf("%d %d %d %d\n",u.v[0],u.v[1],u.v[2],u.dist);
}

int main()
{
    int a,b,c,d;
    scanf("%d%d%d%d",&a,&b,&c,&d);
    int w=solve(a,b,c,d);
    print_path(w);
    return 0;
}

    通过此方法可以输出路径。

另: 不使用优先队列每次都会排序的特点,而是出队新的u的时候,如果ans[d]已经非负不要着急break,而是继续计算队列中其他节点,如果其他u.dist>ans[d],剪枝,否则继续加子状态进队列,直到所有的子状态都因为dist足够大而不能继续进队,最终导致队列空,退出循环。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值