题目叙述
https://vjudge.net/problem/UVA-10603
有三个杯子,容量分别为 a,b,c(1≤a,b,c≤200)。初始状态前两个杯子(a,b)为空,第三个杯子(c)装满了水。由于杯子没有刻度,故每次从一个杯子向另一个杯子倒水的时候,在第一个杯子被倒空或第二个杯子被倒满之前不允许停止。
问:通过一定次数的倒水,寻找某个杯子能达到的盛水量d',使得d'≤d且d'最大,输出达到d'所需要的最小总倒水量。
输入样例
第一行为测试用例的数量T,加下来的T行每行包含四个整数a,b,c,d,含义同题目叙述。
2
2 3 4 2
96 97 199 62
输出样例
每个用例输出一行,包含两个整数:最少倒水量与d'。
2 2
9859 62
题目分析
将某时刻三个杯子各自的盛水量(v0,v1,v2)看作一个状态,通过一次倒水,盛水量发生变化,即两个状态之间的转移,显然这样的状态转移是有向的,可将这样的状态转移看作一条有向边。注意到题目需要的是从初始状态转移到目标状态所需要的最少倒水量(而非倒水次数),故可以将倒水量看作该有向边的权值。这样,各个状态及其转移构成一个有向图,题目转化为寻找目标状态并计算初始状态与目标状态之间的最短路径长度。
例如,a=1,b=3,c=6,d=4,初始状态为(0,0,6),存在路径(0,0,6)->(1,0,5)->(0,1,5)->(1,1,4),终止状态为(1,1,4),倒水量为3。
可以用基于优先队列实现的Dijkstra算法作为基础,在建图的同时完成最短路径的寻找。
算法与实现
由于水的总量为c,故当v0,v1确定时,v2=c-v0-v1,也是确定的,故最多201*201=40401状态。
实现Dijkstra算法时用优先队列可以在O(logn)时间内选择出未标记的、到初始状态距离最短的状态。
此外,一旦发现某状态已经出现盛水量为d的杯子时可直接退出算法,Dijkstra算法的贪心选择性质保证了这时的路径长度一定为最短的(倒水量一定最小)。
#include<cstdio> #include<cstring> #include<queue> #include<vector> #include<map> #define min(x,y) ((x)<(y)?(x):(y)) using namespace std; const int maxn=40401,maxd=201;//maxn为最多的状态数目、maxd为目标盛水量的最大数目 int T,c[3],d;//T为样例个数,c[3]为三杯子的容量,d为目标盛水量 struct Vertex {//状态的定义、也是有向图节点的定义 int v[3];//三个杯子各自的盛水量 Vertex(int a,int b,int c) {v[0]=a;v[1]=b;v[2]=c;} }; struct Node {//优先队列中的节点定义 int v,w;//v为状态的编号、w为初始状态到状态v的最短路径 Node(int _v,int _w):v(_v),w(_w) {} bool operator<(const Node &node) const { return w>node.w; } }; map<int,int> id;//为每个状态分配唯一的id vector<Vertex> V;//状态(边)的集合 int n,dist[maxn],ans[maxd],//n为状态总数、dist为每个状态到初始状态的距离、ans保存每个盛水量所需要的最少倒水量
dir[6][2]={{0,1},{1,0},{0,2},{2,0},{1,2},{2,1}};//dir表示3个杯子的6种倒水方式 bool vis[maxn],done[maxd];//vis标记每个状态是否已经找到最短路、done标记每个盛水量是否计算过 void dijkstra(void) { id.clear(); V.clear(); memset(vis,false,maxn*sizeof(bool)); memset(done,false,(d+1)*sizeof(bool)); priority_queue<Node> Q;//优先队列 dist[id[c[2]]=n=0]=0;//初始状态到自身距离为0 if(c[2]<=d) { done[c[2]]=true; ans[c[2]]=0; } done[0]=true;ans[0]=0;//盛水量为0的初始化很重要!!! Q.push(Node(n,dist[n]));//初始状态入队 V.push_back(Vertex(0,0,c[2]));++n; while(!Q.empty()) { Node node=Q.top();Q.pop();//到初始状态距离最小的未标记状态出队 int u=node.v,p[3];//u记录当前状态的编号 if(V[u].v[0]==d||V[u].v[1]==d||V[u].v[2]==d) break;//若已经出现目标状态d则算法结束 if(vis[u]) continue;//已经找到最短路的状态跳过 vis[u]=true; for(int i=0;i<6;i++) {//枚举6种倒水方式 int x=dir[i][0],y=dir[i][1];//从杯子x向y倒水 if(!V[u].v[x]||V[u].v[y]==c[y]) continue;//x没水可倒或y已满时不考虑 p[0]=V[u].v[0];p[1]=V[u].v[1];p[2]=V[u].v[2]; int w=min(p[x],c[y]-p[y]); p[x]-=w;p[y]+=w; int key=1000000*p[0]+1000*p[1]+p[2];//每种状态的键值用最大9位数表示,如key[(1,1,4)]=1001004 if(id.find(key)==id.end()) {//没计算过的状态保存并入队 dist[id[key]=n]=node.w+w; Q.push(Node(n,dist[n])); V.push_back(Vertex(p[0],p[1],p[2]));++n; } else { int v=id[key];//已经计算过的状态更新距离 if(vis[v]) continue; if(node.w+w<dist[v]) {//松弛操作 dist[v]=node.w+w; Q.push(Node(v,dist[v])); } } if(!done[p[x]]) {//更新盛水量所需要的最小倒水量 ans[p[x]]=node.w+w; done[p[x]]=true; } else ans[p[x]]=min(ans[p[x]],node.w+w); if(!done[p[y]]) { ans[p[y]]=node.w+w; done[p[y]]=true; } else ans[p[y]]=min(ans[p[y]],node.w+w); } } } int main(void) { for(scanf("%d",&T);T--;) { scanf("%d%d%d%d",c,c+1,c+2,&d); dijkstra(); while(d>=0&&!done[d]) --d;//找到首个能作为目标的盛水量并输出 printf("%d %d\n",ans[d],d); } return 0; }