今天看紫书看到倒水问题,虽说是暴力解法,但其中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足够大而不能继续进队,最终导致队列空,退出循环。