7.5 倒水问题

Fill
这边笔者认为是通过priority_queue优先队列来解决此类问题,当然倒水量越少,其优先度越高
当然还需要注意的是对数据的存储,因为这里的水量是固定的,因此只要知二得一,因此实际存储各个杯子水量的时候只要存储两个杯子得水量就可以了,同时memset对于大数组得初始化可以说是很慢的,尽量避免此类情况,否则程序得大部分运行时间会被初始化占用,导致TLE

点击查看笔者代码
/*
  本题血的教训,数组能够开小一定不要开大,注意本题水量是固定的因此知二求一,因此只要10^6,而不需要10^9,
  当然更需要注意的就是使用memset的时候如果是一个非常大的数组,那么此时他的效率是很低的,或者说在非常多次的
  对一个大数组进行初始化,会使得时间成本非常高,因此此时离散化是一个必须的条件,或者使用hash,总的来说,尽可能
  的降低相同数据的存放空间大小,尽量减小不必要的空间成本 
*/

#include<iostream>
#include<cstring>
#include<queue>
using namespace std;

constexpr int maxn = 200200 + 10, base1 = 1000000, base2 = 1000;
int goal, ca, cb, cc, sign[6][2] = {0, 1, 0, 2, 1, 0, 1, 2, 2, 0, 2, 1};
bool vis[maxn];

struct Cup{
  int a, b, c, sum;
  Cup(int a = 0, int b = 0, int c = 0, int sum = 0) : a(a), b(b), c(c), sum(sum) {}
  bool operator < (const Cup & c) const {
  	return sum > c.sum;
  }
};

void getD() {
  memset(vis, 0, sizeof(vis));
  cin >> ca >> cb >> cc >> goal;
}

inline int toInt(int b, int c) {
  return b*base2 + c;
}

int main() {

  int t;
  cin >> t;
  while(t--) {
  	getD();
  	priority_queue<Cup> q;
  	q.push(Cup(0, 0, cc, 0));
  	vis[toInt(0, cc)] = true;
  	int step, ans = -1, cupMax[3] = {ca, cb, cc}; 
	while(!q.empty()) {
  	  Cup c = q.top(); q.pop();
  	  if(c.a == goal || c.b == goal || c.c == goal) {step = c.sum; ans = goal; break; }
  	  if(goal-ans > goal-c.a && goal-c.a >= 0) { step = c.sum; ans = c.a; }
  	  if(goal-ans > goal-c.b && goal-c.b >= 0) { step = c.sum; ans = c.b; }
  	  if(goal-ans > goal-c.c && goal-c.c >= 0) { step = c.sum; ans = c.c; }
  	  for(int i = 0; i < 6; i++) {
  	    int add, from = sign[i][0], to = sign[i][1], cup[3] = {c.a, c.b, c.c}; 
		if(cup[from] > cupMax[to]-cup[to]) { add = cupMax[to]-cup[to]; cup[from] -= (cupMax[to]-cup[to]); cup[to] = cupMax[to]; }
		else { add = cup[from]; cup[to] += cup[from]; cup[from] = 0; }
		int temp = toInt(cup[1], cup[2]);
		if(vis[temp]) continue;
		else {
		  vis[temp] = true;
		  q.push(Cup(cup[0], cup[1], cup[2], c.sum+add));
		}
	  }
	}
	cout << step << " " << ans << endl;
  }
  return 0;
} 

作者的分析如下:
假设在某一时刻,第1个杯子中有v0升水,第二个杯子中有v1升水,第3个杯子中有v2升水,那么称当时的系统状态为(v0,v1,v2)。
状态这个词,是理解很多概念和算法的关键,简单的说,它就是对系统当前状况的描述
把状态想象成图中的结点,可以得到一张状态图state graph
注意:本题的目标是倒的水量最少,而不是步数最少。因此,水量最少的时候,步数不一定是最少的,因此此时我们BFS的时候不能以步数为优先级,而应该以水量为优先级
每次取出倒水量最少的点进行扩展
下面的代码把状态(三元组)和dist合起来定义为了一个Node类型,是一种常见的写法。如果要打印路径,需要把访问的所有结点放在一个nodes数组中,然后在Node中加一个变量fa,表示父节点在nodes数组中的下标,而在队列中只存系欸但在nodes数组中的下标而非结点本身,如果内存重组也可以直接在Node中用一个vector存储路径,省去顺着fa往回找的麻烦

这边是因为保证了存在nodes中的结点一定是不重复的,所以可以通过fa来寻回路径,注意该种写法,这可以节省大量的系统运行时的空间

点击查看代码
#include<cstdio>
#include<cstring>
#include<queue>
using namespace std;

struct Node {
  int v[3], dist;//v存放状态,dist存放倒水量 
  bool operator < (const Node& rhs) const {
    return dist > rhs.dist;
  }
};

const int maxn = 200 + 5;
int vis[maxn][maxn], cap[3], ans[maxn];//vis标记状态,ans表示的是水量为x的时候倒入的水量 

void update_ans(const Node& u) {
  for(int i = 0; i < 3; i++) {
  	int d = u.v[i]; 
  	if(ans[d] < 0 || u.dist < ans[d]) ans[d] = u.dist; //因为ans的初始化是-1,因此小于0的时候说明还没有初始化,或者就是如果以及该初始化,那么就记录最小的倒水量 
  }
}

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 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);//开始更新ans数组 
  	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(cap[j], u.v[i] + u.v[j]) - u.v[j];//如果没有倒满,那么就是u.v[i],倒满就是cap[j]-u.v[j] 
		Node u2;
		memcpy(&u2, &u, sizeof(u));
		u2.dist = u.dist + amount;//amount表示倒水量 
		u2.v[i] -= amount;
		u2.v[j] += amount;
		if(!vis[u2.v[0]][u2.v[1]]) {//判重 
		  vis[u2.v[0]][u2.v[1]] = 1;
		  q.push(u2);
		}	
	  }
  }
  while(d >= 0) {//开始从目标位置不断往下读取数据来进行判断是否能到达该水量,如果可以的话,进行输出 
    if(ans[d] >= 0) {
	  printf("%d %d\n", ans[d], d);
	  return;	
	}
	d--;
  }
}

int main() {
  int T, a, b, c, d;
  scanf("%d", &T);
  while(T--) {
  	scanf("%d%d%d%d", &a, &b, &c, &d); 
  	solve(a, b, c, d);
  }
  return 0;
}

需要注意的是:上述算法非常直观,正确性却不是显然的。上述的算法稍加修改,就可以得到第11章中要介绍的Dijkstra算法,从而保证算法的正确性。等学完Dijkstra算法之后,笔者再回来补补这道题目-_-,该道例题可以看出搜索和图论这两个专题的联系

posted @   banyanrong  阅读(65)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示