算法作业1:倒水问题——三壶谜题


题目:三壶谜题

有一个充满水的8品脱的水壶和两个空水壶(容积分别是5品脱和3品脱)。通过将水壶完全倒满水和将水壶的水完全倒空这两种方式,在其中的一个水壶中得到4品脱的水。

在这里插入图片描述
(图源来自知乎Noever用户)


一、思路1:DFS图的深度优先遍历

续上一篇刷题博客(『牛客网』剑指offer-JZ6 从尾到头打印链表)提到的DFS深度优先遍历算法,本博客将DFS运用于本题。
第一步:建模
在任意一个时刻,我们可以且仅可以采取以下几种操作:

把 A 壶的水灌进 B壶,直至灌满或倒空;
把 B 壶的水灌进 A 壶,直至灌满或倒空;
把 A 壶灌满;
把 B 壶灌满;
把 A 壶倒空;
把 B 壶倒空。

第二步:DFS
本题可以使用深度优先搜索来解决。搜索中的每一步以 remain_x, remain_y 作为状态,即表示 A壶和 B 壶中的水量。在每一步搜索时,我们会依次尝试所有的操作,递归地搜索下去。这可能会导致我们陷入无止境的递归,因此我们还需要使用一个哈希结合(HashSet)存储所有已经搜索过的 remain_x, remain_y 状态,保证每个状态至多只被搜索一次。

为了防止递归造成的溢出现象,本题借助模拟递归。


解决方案1

using Pair = pair<int, int>;

class Solution {
public:
    bool canMeasureWater(int x, int y, int z) {

        stack<Pair> s;
        s.emplace(0, 0);
        auto hash_function = [](const Pair& o) {return hash<int>()(o.first) ^ hash<int>()(o.second);};
        unordered_set<Pair, decltype(hash_function)> seen(0, hash_function);
        while (!s.empty()) {
            if (seen.count(s.top())) {
                s.pop();
                continue;
            }
            seen.emplace(s.top());
            
            auto [remain_x, remain_y] = s.top();
            s.pop();
            if (remain_x == z || remain_y == z || remain_x + remain_y == z) {
                return true;
            }
            // 把 X 壶灌满。
            s.emplace(x, remain_y);
            // 把 Y 壶灌满。
            s.emplace(remain_x, y);
            // 把 X 壶倒空。
            s.emplace(0, remain_y);
            // 把 Y 壶倒空。
            s.emplace(remain_x, 0);
            // 把 X 壶的水灌进 Y 壶,直至灌满或倒空。
            s.emplace(remain_x - min(remain_x, y - remain_y), remain_y + min(remain_x, y - remain_y));
            // 把 Y 壶的水灌进 X 壶,直至灌满或倒空。
            s.emplace(remain_x + min(remain_y, x - remain_x), remain_y - min(remain_y, x - remain_x));
        }
        return false;
    }
};



C++11 用到的新特性-知识总结:

《C++ Primer》第5版:
using 声明:2.5.1节,P60页;
auto:2.5.2节,P61;
decltype:2.5.3节,P62;
unordered_set:11.4节,P394;
hash 模板:11.4节,P396;
lambda 表达式:10.3.2节,P345;
emplace:9.3.1节,P308;

part 01:using 声明、pair类型

using Pair = pair<int, int>;

在这里插入图片描述

  1. using用法(三种):
  • 导入整个命名空间到当前作用域:using namespace std;
  • C++ 11 通过 using 指定别名,作用等同于 typedef,但相比 typedef,逻辑更直观,可读性更好:using T = int; // 用 T 代替 int;
  • 在派生类中引用基类成员
    例如:
public:
	using Base::bValue;	//基类成员变量
	using Base::ShowName;//基类成员函数
  1. pair用法:
  • 将2个数据组合成一组数据。如stl中的map就是将key和value放在一起来保存。
  • 当一个函数需要返回2个数据的时候

其标准库类型——pair类型定义在#include <utility>头文件中,定义如下:

类模板:template<class T1,class T2> struct pair

参数:T1是第一个值的数据类型,T2是第二个值的数据类型。

功能:pair将一对值(T1和T2)组合成一个值, 这一对值可以具有不同的数据类型(T1和T2), 两个值可以分别用pair的两个公有函数first和second访问。

p1.first;                   // 返回对象p1中名为first的公有数据成员
p1.second;                 // 返回对象p1中名为second的公有数据成员

part 02:auto、decltype、unordered_set、hash模板、lambda 表达式

  auto hash_function = [](const Pair& o) {return hash<int>()(o.first) ^ hash<int>()(o.second);};
        unordered_set<Pair, decltype(hash_function)> seen(0, hash_function);

^ :按位异或运算符。将其第一操作数的每个位与其第二操作数的相应位进行比较。 如果其中一个操作数中的位为 0,而另一个操作数中的位为 1,则相应的结果位设置为 1。 否则,将对应的结果位设置为 0。(有1则1,否则为0)

unordered_set是基于 hash 实现的。从名字可知不支持有序遍历。目前 unordered_set 默认的 hash 函数仅支持 build-in 类型及 string 等少数类型。其他类型或用户自定义类型,需要自己编写 hash 函数

hash<int>是实例化了一个类模板; hash()是调用了这个类的默认构造函数,创建了一个临时对象; hash<int>()(os.first)是调用了这个类里重载的函数调用运算符 。

在这里插入图片描述
在这里插入图片描述在这里插入图片描述
在这里插入图片描述


part03:emplace用法

 s.emplace(0, 0);

在这里插入图片描述


java解决方案(DFS)

由于上面解题方法使用c++11语法较多,使得题目更关注语法使用而忽略解题思路,本末倒置,所有此处加一份java版本解题方案。

class Solution {
    public boolean canMeasureWater(int x, int y, int z) {
        Deque<int[]> stack = new LinkedList<int[]>();
        stack.push(new int[]{0, 0});
        Set<Long> seen = new HashSet<Long>();
        while (!stack.isEmpty()) {
            if (seen.contains(hash(stack.peek()))) {
                stack.pop();
                continue;
            }
            seen.add(hash(stack.peek()));
            
            int[] state = stack.pop();
            int remain_x = state[0], remain_y = state[1];
            if (remain_x == z || remain_y == z || remain_x + remain_y == z) {
                return true;
            }
            // 把 X 壶灌满。
            stack.push(new int[]{x, remain_y});
            // 把 Y 壶灌满。
            stack.push(new int[]{remain_x, y});
            // 把 X 壶倒空。
            stack.push(new int[]{0, remain_y});
            // 把 Y 壶倒空。
            stack.push(new int[]{remain_x, 0});
            // 把 X 壶的水灌进 Y 壶,直至灌满或倒空。
            stack.push(new int[]{remain_x - Math.min(remain_x, y - remain_y), remain_y + Math.min(remain_x, y - remain_y)});
            // 把 Y 壶的水灌进 X 壶,直至灌满或倒空。
            stack.push(new int[]{remain_x + Math.min(remain_y, x - remain_x), remain_y - Math.min(remain_y, x - remain_x)});
        }
        return false;
    }

    public long hash(int[] state) {
        return (long) state[0] * 1000001 + state[1];
    }
}

作者:LeetCode-Solution


二、思路2:BFS图的广度优先遍历

借助队列,将(0,0)作为初始A,B水壶的状态,入队并标志为已访问。BFS过程:队列不空,队头元素出队,获取当前A、B水壶的状态,若任意一个水壶达到目标水容量则退出循环。否则,进行下一步状态转换:
装满任意一个水壶,定义为「操作一」,分为:
(1)装满 A,包括 A 为空和 A 非空的时候把 A 倒满的情况;
(2)装满 B,包括 B 为空和 B 非空的时候把 B 倒满的情况。

清空任意一个水壶,定义为「操作二」,分为
(1)清空 A;
(2)清空 B。

从一个水壶向另外一个水壶倒水,直到装满或者倒空,定义为「操作三」,其实根据描述「装满」或者「倒空」就知道可以分为 4 种情况:

(1)从 A 到 B,使得 B 满,A 还有剩;
(2)从 A 到 B,此时 A 的水太少,A 倒尽,B 没有满;
(3)从 B 到 A,使得 A 满,B 还有剩余;
(4)从 B 到 A,此时 B 的水太少,B 倒尽,A 没有满。


解决方案2-java

import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Queue;
import java.util.Set;

public class Solution {

    public boolean canMeasureWater(int x, int y, int z) {
        // 特判
        if (z == 0) {
            return true;
        }
        if (x + y < z) {
            return false;
        }

        State initState = new State(0, 0);

        // 广度优先遍历使用队列
        Queue<State> queue = new LinkedList<>();
        Set<State> visited = new HashSet<>();

        queue.offer(initState);
        visited.add(initState);

        while (!queue.isEmpty()) {
            State head = queue.poll();

            int curX = head.getX();
            int curY = head.getY();

            // curX + curY == z 比较容易忽略
            if (curX == z || curY == z || curX + curY == z) {
                return true;
            }

            // 从当前状态获得所有可能的下一步的状态
            List<State> nextStates = getNextStates(curX, curY, x, y);
            
            // 打开以便于观察,调试代码
            // System.out.println(head + " => " + nextStates);
            
            for (State nextState : nextStates) {
                if (!visited.contains(nextState)) {
                    queue.offer(nextState);
                    // 添加到队列以后,必须马上设置为已经访问,否则会出现死循环
                    visited.add(nextState);
                }
            }
        }
        return false;
    }

    private List<State> getNextStates(int curX, int curY, int x, int y) {
        // 最多 8 个对象,防止动态数组扩容,不过 Java 默认的初始化容量肯定大于 8 个
        List<State> nextStates = new ArrayList<>(8);

        // 按理说应该先判断状态是否存在,再生成「状态」对象,这里为了阅读方便,一次生成 8 个对象

        // 以下两个状态,对应操作 1
        // 外部加水,使得 A 满
        State nextState1 = new State(x, curY);
        // 外部加水,使得 B 满
        State nextState2 = new State(curX, y);

        // 以下两个状态,对应操作 2
        // 把 A 清空
        State nextState3 = new State(0, curY);
        // 把 B 清空
        State nextState4 = new State(curX, 0);

        // 以下四个状态,对应操作 3
        // 从 A 到 B,使得 B 满,A 还有剩
        State nextState5 = new State(curX - (y - curY), y);
        // 从 A 到 B,此时 A 的水太少,A 倒尽,B 没有满
        State nextState6 = new State(0, curX + curY);

        // 从 B 到 A,使得 A 满,B 还有剩余
        State nextState7 = new State(x, curY - (x - curX));
        // 从 B 到 A,此时 B 的水太少,B 倒尽,A 没有满
        State nextState8 = new State(curX + curY, 0);

        // 没有满的时候,才需要加水
        if (curX < x) {
            nextStates.add(nextState1);
        }
        if (curY < y) {
            nextStates.add(nextState2);
        }

        // 有水的时候,才需要倒掉
        if (curX > 0) {
            nextStates.add(nextState3);
        }
        if (curY > 0) {
            nextStates.add(nextState4);
        }

        // 有剩余才倒
        if (curX - (y - curY) > 0) {
            nextStates.add(nextState5);
        }
        if (curY - (x - curX) > 0) {
            nextStates.add(nextState7);
        }

        // 倒过去倒不满才倒
        if (curX + curY < y) {
            nextStates.add(nextState6);
        }
        if (curX + curY < x) {
            nextStates.add(nextState8);
        }
        return nextStates;
    }

    private class State {
        private int x;
        private int y;

        public State(int x, int y) {
            this.x = x;
            this.y = y;
        }

        public int getX() {
            return x;
        }

        public int getY() {
            return y;
        }

        @Override
        public String toString() {
            return "State{" +
                    "x=" + x +
                    ", y=" + y +
                    '}';
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }
            State state = (State) o;
            return x == state.x &&
                    y == state.y;
        }

        @Override
        public int hashCode() {
            return Objects.hash(x, y);
        }
    }


    public static void main(String[] args) {
        Solution solution = new Solution();

        int x = 3;
        int y = 5;
        int z = 4;

//        int x = 2;
//        int y = 6;
//        int z = 5;

//        int x = 1;
//        int y = 2;
//        int z = 3;
        boolean res = solution.canMeasureWater(x, y, z);
        System.out.println(res);
    }
}

作者:liweiwei1419
链接:https://leetcode.cn/problems/water-and-jug-problem/solution/tu-de-yan-du-you-xian-bian-li-by-liweiwei1419/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
  • 5
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
算机图形学中的纹理映射是一种将图像或纹理映射到三维对象表面的技术。纹理映射可以用于增强三维对象的视觉效果,例如在三维场景中添加木纹或石纹等纹理。下面是一个使用OpenGL实现的纹理映射的水壶代码示例: ```OpenGL #include <GL/glut.h> #include <stdio.h> #include <stdlib.h> #include <math.h> #define PI 3.14159265358979323846 GLuint texture[1]; void init(void) { glClearColor(0.0, 0.0, 0.0, 0.0); glShadeModel(GL_SMOOTH); glEnable(GL_DEPTH_TEST); // 加载纹理 glGenTextures(1, texture); glBindTexture(GL_TEXTURE_2D, texture[0]); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); int width, height; unsigned char* image = SOIL_load_image("texture.jpg", &width, &height, 0, SOIL_LOAD_RGB); if (image == NULL) { printf("Failed to load texture image.\n"); exit(1); } glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image); SOIL_free_image_data(image); } void display(void) { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glEnable(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, texture[0]); // 绘制水壶 glColor3f(1.0, 1.0, 1.0); glutSolidTeapot(1.0); glFlush(); glDisable(GL_TEXTURE_2D); } void reshape(int w, int h) { glViewport(0, 0, (GLsizei)w, (GLsizei)h); glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluPerspective(60.0, (GLfloat)w / (GLfloat)h, 1.0, 20.0); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); gluLookAt(0.0, 0.0, 5.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0); } int main(int argc, char** argv) { glutInit(&argc, argv); glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB | GLUT_DEPTH); glutInitWindowSize(500, 500); glutInitWindowPosition(100, 100); glutCreateWindow(argv[0]); init(); glutDisplayFunc(display); glutReshapeFunc(reshape); glutMainLoop(); return 0; } ``` 在这个示例中,我们使用了SOIL库来加载纹理图像。在初始化函数中,我们首先生成了一个纹理对象,并将其绑定到GL_TEXTURE_2D目标上。然后,我们设置了纹理的一些参数,例如纹理的重复方式和过滤方式。接下来,我们加载了一张名为“texture.jpg”的纹理图像,并将其绑定到纹理对象上。在显示函数中,我们启用了纹理映射,并将纹理对象绑定到当前的纹理单元上。最后,我们使用OpenGL提供的glutSolidTeapot函数绘制了一个水壶,并在绘制过程中应用了纹理映射。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值