Java&LeetCode ——752. 打开转盘锁


文内代码全部采用JAVA语言。需要提前阅读leetcode上的相关内容。

队列学习

队列遵循先入先出原则,有两个重要的操作,入队 enqueue 和出队 dequeue。队尾入,队头出。java中有内置库,无需重写。
下面是使用内置队列库及其常见操作的一些示例:

// "static void main" must be defined in a public class.
public class Main {
    public static void main(String[] args) {
        // 1. Initialize a queue.产生新队列
        Queue<Integer> q = new LinkedList();
        // 2. Get the first element - return null if queue is empty.
        //获取第一个元素,也就是整个队列的头,如果返回null说明队列为空
        System.out.println("The first element is: " + q.peek());
        // 3. Push new element.队尾添加新元素
        q.offer(5);
        q.offer(13);
        q.offer(8);
        q.offer(6);
        // 4. Pop an element.移除队头的元素,也就是peek()到的元素
        q.poll();
        // 5. Get the first element.
        System.out.println("The first element is: " + q.peek());
        // 7. Get the size of the queue.队列长度
        System.out.println("The size is: " + q.size());
    }
}

广度优先搜索 - 模板

队列经常用于找最短路径的情况。模板就不贴了,leetcode上都有,自行学习。值得注意的是,如果要确保永远不会访问同一个节点的时候,需要增加Set集合。链接如下:
https://leetcode-cn.com/explore/learn/card/queue-stack/217/queue-and-bfs/870/

题目-打开转盘锁

你有一个带有四个圆形拨轮的转盘锁。每个拨轮都有10个数字: ‘0’, ‘1’, ‘2’, ‘3’, ‘4’, ‘5’, ‘6’, ‘7’, ‘8’, ‘9’ 。每个拨轮可以自由旋转:例如把 ‘9’ 变为 ‘0’,‘0’ 变为 ‘9’ 。每次旋转都只能旋转一个拨轮的一位数字。

锁的初始数字为 ‘0000’ ,一个代表四个拨轮的数字的字符串。

列表 deadends 包含了一组死亡数字,一旦拨轮的数字和列表里的任何一个元素相同,这个锁将会被永久锁定,无法再被旋转。

字符串 target 代表可以解锁的数字,你需要给出最小的旋转次数,如果无论如何不能解锁,返回 -1。

测试用例

示例 1:

输入:deadends = ["0201","0101","0102","1212","2002"], target = "0202"
输出:6
解释:
可能的移动序列为 "0000" -> "1000" -> "1100" -> "1200" -> "1201" -> "1202" -> "0202"。
注意 "0000" -> "0001" -> "0002" -> "0102" -> "0202" 这样的序列是不能解锁的,
因为当拨动到 "0102" 时这个锁就会被锁定。

示例 2:

输入: deadends = ["8888"], target = "0009"
输出:1
解释:
把最后一位反向旋转一次即可 "0000" -> "0009"

示例 3:

输入: deadends = ["8887","8889","8878","8898","8788","8988","7888","9888"], target = "8888"
输出:-1
解释:
无法旋转到目标数字且不被锁定。

示例 4:

输入: deadends = ["0000"], target = "8888"
输出:-1

个人解法

在最短路径搜索的过程中有很重要的一步:找neighbor,也就是找下一个节点。在这个问题中也就是旋转一下锁盘之后可能的结果,注意:已经尝试过的数值不再尝试,避免陷入无限循环。

首先初始化step,即旋转次数。

public int openLock(String[] deadends, String target) {
		int step=0;//旋转次数
		//将deadends转为list,方便判断字符串是否是禁忌字符串
		List<String> list=Arrays.asList(deadends);
		Queue<String> queue=new LinkedList<>();//生成队列
		Set<String> used=new HashSet<>();//存放已经尝试过的密码值
		queue.add("0000");//初始密码,也就是根节点
		used.add("0000");//“0000”已经尝试,不再允许使用
		while (queue.isEmpty()==false) {
			int size=queue.size();//队列长度
			for (int i = 0; i < size; i++) {
				String cur=queue.peek();//当前尝试的密码
				if (cur.equals(target)) {
					return step;//如果当前密码与target相同,则成功,直接返回旋转次数
				}
				String[] neib=neighbour(cur);//生成邻居,也就是旋转一次之后可能的字符串
				for (String str1 : neib) {
				//对邻居检查,如果没有使用过并且不是禁忌字符串,则放入队列
					if (used.contains(str1)==false && list.contains(cur)==false) {
						queue.add(str1);
						used.add(str1);
					}
				}
				queue.poll();//已经尝试过的上层节点释放
			}
			step=step+1;
			//尝试结束之后部署+1;因为进入队列的数需要下一次循环才能判断是否是正确密码
		}
		return -1;//如果没有返回值说明打开失败
	}

	public String[] neighbour(String a) {
	//生成邻居,一个字符串有4位数字,每位数字可以增加1,或减少1,所以总共有4*2=8个邻居
		String ans[]=new String[8];
		for (int i = 0; i < 4; i++) {
			char[] aa=a.toCharArray();
			int a1=aa[i]-'0';
			if (a1!=0) {
				aa[i]=Character.forDigit(a1-1, 10);
			}else {
				aa[i]='9';
			}
			ans[i*2]=String.valueOf(aa);
			if(a1!=9) {
				aa[i]=Character.forDigit(a1+1, 10);
			}else {
				aa[i]='0';
			}
			ans[i*2+1]=String.valueOf(aa);
		}
		return ans;
	}
	

个人解法感觉还是比较好理解,只要能理解leetcode上面提供的模板就行。但是执行起来速度堪忧,289ms,打败23.04%。下面学习一下大神的解法

大神解法

最快解法

执行用时: 44 ms, 在Open the Lock的Java提交中击败了96.20% 的用户

class Solution {
    public int openLock(String[] deadends, String target) {
        boolean[] isVisit=new boolean[10000];//0~9999共10000种可能
        boolean[] isDead=new boolean[10000];
        for (String deadEnd : deadends) {
            int deadNum = Integer.parseInt(deadEnd);//字符串转十进制数,“0101”=101;
            isDead[deadNum] = true;//标记deadends里的数字是禁忌数
        }
        //如果"0000"在deadEnds中,返回-1
        if(isDead[0]) {
            return -1;
        }
        if("0000".equals(target)) {
            return 0;
        }

        int step=0;
        Queue<Integer> queue=new ArrayDeque<>();
        queue.offer(Integer.parseInt(target));//放入target作为根节点,查找到“0000”退出
        isVisit[Integer.parseInt(target)]=true;//标记target已经访问 
        int last=Integer.parseInt(target);//当前最后一个数
        int cenglast=last;
        while(!queue.isEmpty())
        {
            while(true)
            {
                int[] neighbor=new int[8];
                int head=queue.poll();
                if(head==0) {
                //查找到“0000”退出
                    return step;
                }
                int[] nei=neighbor(head);//邻居节点
                for(int i=0;i<8;i++)
                {
                    int trans=nei[i];
                    if(isVisit[trans] || isDead[trans]) {
                    //如果邻居节点已经访问或者是禁忌数,则跳过下面的步骤,直接i++
                        continue;
                    }
                    //如果不是将这个数放入队列
                    queue.offer(trans);
                    last=trans;
                    isVisit[trans]=true;//标记为已尝试
                }
                if(head==cenglast) {
                //如果当前的节点和上一层的最后一个节点相同,说明已经遍历了这一整层
                //退出第二层while循环
                    break;
                }
            }
            step++;
            cenglast=last;
        }
        return -1;


    }

    private int[] neighbor(int code)
    //转成int型之后用 +9,+1来取余数,+9相当于-1,还避免了用if来求解。
    {
        int[] res=new int[8];
        int a=code%10;
        int b=(code/10)%10;
        int c=(code/100)%10;
        int d=(code/1000)%10;
        res[0]=d*1000+c*100+b*10+(a+10-1)%10;
        res[1]=d*1000+c*100+b*10+(a+1)%10;
        res[2]=d*1000+c*100+((b+10-1)%10)*10+a;
        res[3]=d*1000+c*100+((b+1)%10)*10+a;
        res[4]=d*1000+((c+10-1)%10)*100+b*10+a;
        res[5]=d*1000+((c+1)%10)*100+b*10+a;
        res[6]=((d+10-1)%10)*1000+c*100+b*10+a;
        res[7]=((d+1)%10)*1000+c*100+b*10+a;
        return res;
    }
}

双向广度优先搜索(作者:青云算法,非队列解法)

作者:青云算法
来源:CSDN
原文:https://blog.csdn.net/QingyunAlgo/article/details/80589440
版权声明:本文为博主原创文章,转载请附上博文链接!

这个问题是单个源点(“0000”)单个目标(输入的target)的广度优先搜索。我们可以想象当我们到达目标节点的时候,我们同时还遍历了求解树中位于同一层的其他节点。尽管在那一层中我们只需要遍历一个节点,我们却实际上遍历很多不必要的节点,因此单向搜索是存在优化空间的。

我们可以把单项搜索改为双向搜索,也就是既从源点出发向着目标搜索,也从目标出发向着源点搜索。如果两个方向搜索最终能够在中间某个位置相遇,那么表明存在从源点到目标的路径。

我们只需要在单向搜索的代码上稍微作些改动,就能实现双向搜索,如下所示:

76 ms, 在Open the Lock的Java提交中击败了86.83% 的用户

class Solution {
   public int openLock(String[] deadends, String target) {
    Set<String> dead = new HashSet<>(Arrays.asList(deadends));//禁忌字符串集合
    Set<String> visited = new HashSet<>();//已访问字符串集合
    String init = "0000";
    if (dead.contains(init) || dead.contains(target)) {
        return -1;//初始密码或目标密码是禁忌值
    }
 
    if (target.equals(init)) {
        return 0;//初始密码是目标密码
    }
 
    Set<String> set1 = new HashSet<>();//存放从初始值衍生的节点
    set1.add(init);
    Set<String> set2 = new HashSet<>();//存放从目标衍生来的节点
    set2.add(target);
 
    int steps = 0;
    while (!set1.isEmpty() && !set2.isEmpty()) {
        if (set1.size() > set2.size()) {
            Set<String> temp = set1;
            set1 = set2;
            set2 = temp;
        }//永远搜索短的那个结集合
 
        Set<String> set3 = new HashSet<>();
        for (String cur : set1) {
            for (String next : getNexts(cur)) {
            //访问邻居节点
                if (set2.contains(next)) {
                    return steps + 1;
                }//如果两端相遇,则直接输出
 
                if (!dead.contains(next) && !visited.contains(next)) {
                    visited.add(next);
                    set3.add(next);
                }
            }
        }
 
        steps++;
        set1 = set3;//新集合覆盖
    }
 
    return -1;
}

    private List<String> getNexts(String cur) {
    List<String> nexts = new LinkedList<>();
    for (int i = 0; i < cur.length(); ++i) {
        char ch = cur.charAt(i);
 
        char newCh = ch == '0' ? '9' : (char)(ch - 1);
        StringBuilder builder = new StringBuilder(cur);
        builder.setCharAt(i, newCh);
        nexts.add(builder.toString());
 
        newCh = ch == '9' ? '0' : (char)(ch + 1);
        builder = new StringBuilder(cur);
        builder.setCharAt(i, newCh);
        nexts.add(builder.toString());
    }
 
    return nexts;
}
}
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值