class Solution {
public int openLock(String[] deadends, String target) {
if ("0000".equals(target)) {
return 0;
}
// 将deadends里的值加入哈希表中
Set<String> dead = new HashSet<String>();
for (String deadend : deadends) {
dead.add(deadend);
}
// 若0000在其中,无法解锁
if (dead.contains("0000")) {
return -1;
}
// 旋转次数
int step = 0;
// 每项元素都是四位数字符串
// 用于记录当前形式的四位数,以该四位数对之展开预测下一步的操作
Queue<String> queue = new LinkedList<String>();
queue.offer("0000");
// 用于历史查询的哈希表,防止我们在转成如1000形式后又在后几步转会了1000形式,避免重复
Set<String> seen = new HashSet<String>();
seen.add("0000");
while (!queue.isEmpty()) {
// 旋转了一次
++step;
int size = queue.size();
// 遍历queue队列里的所有值
// 这里面存有在旋转了step次时所有满足条件:不在dead、seen里的四位数可能形式
for (int i = 0; i < size; ++i) {
// 取出当前项(以弹出的形式)
String status = queue.poll();
// 调用 get 方法,得到存有以status为基础的旋转一次后所有可能形式的数组
// 然后循环遍历每一种可能性
for (String nextStatus : get(status)) {
// 只有 seen、dead 中都不包含当前可能性时才会进入内部
if (!seen.contains(nextStatus) && !dead.contains(nextStatus)) {
// 若当前可能性满足结果,那就直接返回
if (nextStatus.equals(target)) {
return step;
}
// 不满足当前结果,就将其存入队列中
queue.offer(nextStatus);
// 记录当前形式,避免下次又旋转成和这次相同的形式
seen.add(nextStatus);
}
}
}
}
return -1;
}
// 返回当前位数字旋转后的可能形式(自身-1)
public char numPrev(char x) {
// 若当前为是0,那就变为9,否则-1
return x == '0' ? '9' : (char) (x - 1);
}
// 返回当前位数字旋转后的可能形式(自身+1)
public char numSucc(char x) {
// 若当前为是9,那就变为0,否则+1
return x == '9' ? '0' : (char) (x + 1);
}
// 枚举 status(四位数) 通过一次旋转得到的数字
public List<String> get(String status) {
// 创建动态数组组,用于存储可能的所有旋转一次后结果
List<String> ret = new ArrayList<String>();
// 将传来的四位数字符串字符数组化
char[] array = status.toCharArray();
// 遍历四位数的每一位
for (int i = 0; i < 4; ++i) {
// 取出当前位
char num = array[i];
// 存储其下次旋转可能的形式(-1)
array[i] = numPrev(num);
ret.add(new String(array));
// 存储其下次旋转可能的形式(+1)
array[i] = numSucc(num);
ret.add(new String(array));
// 防止影响下一轮循环
array[i] = num;
}
return ret;
}
}
1、针对get方法:
get方法中的 ret.add(new String(array)); 的说明:首先我们要知道 get 方法是用于返回当前四位数经历一次旋转后所有可能的形式。
array 在 for 循环外时本身的数据为方法传入的四位数。
循环负责对四位中的每一位进行预测下一次旋转后独有的结果并将之保存在 ret 中:
我们取出当前位,假设是第一位,然后调用 -1 方法返回对应结果将之覆盖原先第一位的值,并且将此时产生出的新的四位数String化并添加到 ret 中:
(假设array本来是0123) char num = array[i]; 让num记录当前位(假设是第一位),这样即便 array[i] 后续发生变化也没事。然后调用numPrev函数获得当前位 -1 的结果,并将其覆盖原先值 0 ,让array变为9123,并调用new String(array)将其String化添加到ret中,后序同理。在最后用num复原array[i],以便下一轮循环操作第二位时第一位是原先的样子。
假设传入get方法的四位数是0000,那么在方法的最后我们返回的 ret 里将存在四位数 9000、1000、0900、0100、…等0000旋转一次后的所有可能性。
该循环功能:在这个循环里我们会返回最小次数,若循环结束还没有返回说明无法解锁。
queue队列里保存的是在旋转了step次时所有可能的四位数。
while循环每进行一次,就说明旋转了一次,所以step++。然后我们调出queue的长度,也就是这轮旋转还没开始时上一轮旋转后得到的所有可能四位数形式的数量。
开始循环遍历这些可能性,因为每种可能性都有旋转后的特有形式,所以我们还需再一层for循环,用来得到各个形式衍生出的全部形式。
对第一层for循环,先取出一个可能性,然后将其弹出(因为它已经要被预测下一步旋转后的结果了,我们只需要结果不再需要它了);
开始第二层for循环,调用get方法,得到包含这个可能性所有衍生可能性的数组。然后遍历,判断当前结果是否结束条件,若满足再判断其是否为结果,若不是结果,那就将其作为这次旋转后的所有结果之一保存起来,用于下一次旋转使用。同时加入到seen哈希表中,防止这次旋转得到的结果在以后的旋转中再次被存入queue中,避免重复运算。
第二层循环遍历完,说明当前可能性在旋转一次后所有的衍生结果都不满足结束条件,但其中符合条件的结果都被存入到了queue中。然后开始遍历下一种可能性在旋转一次后所有的衍生结果。
由于队列的特性是先进先出,所以我们在前一次(第二层)for循环添加的值并不会被用在这次(第二层)for循环中。我们的循环条件size也保障了这点。
当第一层for循环全部走完后,说明当前旋转次数无法得到结束条件,就再旋转一次,while循环再进行一轮。此时queue中删除了上上一轮的所有可能性,并保存了上一轮的所有可能性。而seen则保存了所有符合条件的可能性。
while循环什么时候会结束:当queue为空时,queue怎样才能为空:由于每次for循环都会弹出queue的值,而queue的值添加是在符合判断 !seen.contains(nextStatus) && !dead.contains(nextStatus) 后才会进行的,所以当四位数的所有符合条件形式都被记录在seen后这个判断将永远不会被执行,queue迟早会空。
由于每一次旋转,我们都会遍历所有上一轮形式各自的所有衍生形式,判断其是否满足结束条件,而只有在这些形式都被判断完后才会进行下一次旋转。因此,假设这次旋转中存在结束条件,那它一定不会被漏掉。假设这次旋转没有找到结束条件,但这轮旋转后的所有符合条件的形式都被记录下了,所以我们在下次旋转时不会漏掉任何一种衍生可能性(也就是不会漏掉下次可能出现的结束条件)。
class Solution {
public int openLock(String[] deadends, String target) {
if ("0000".equals(target)) {
return 0;
}
Set<String> dead = new HashSet<String>();
for (String deadend : deadends) {
dead.add(deadend);
}
if (dead.contains("0000")) {
return -1;
}
PriorityQueue<AStar> pq = new PriorityQueue<AStar>((a, b) -> a.f - b.f);
pq.offer(new AStar("0000", target, 0));
Set<String> seen = new HashSet<String>();
seen.add("0000");
while (!pq.isEmpty()) {
AStar node = pq.poll();
for (String nextStatus : get(node.status)) {
if (!seen.contains(nextStatus) && !dead.contains(nextStatus)) {
if (nextStatus.equals(target)) {
return node.g + 1;
}
pq.offer(new AStar(nextStatus, target, node.g + 1));
seen.add(nextStatus);
}
}
}
return -1;
}
public char numPrev(char x) {
return x == '0' ? '9' : (char) (x - 1);
}
public char numSucc(char x) {
return x == '9' ? '0' : (char) (x + 1);
}
// 枚举 status 通过一次旋转得到的数字
public List<String> get(String status) {
List<String> ret = new ArrayList<String>();
char[] array = status.toCharArray();
for (int i = 0; i < 4; ++i) {
char num = array[i];
array[i] = numPrev(num);
ret.add(new String(array));
array[i] = numSucc(num);
ret.add(new String(array));
array[i] = num;
}
return ret;
}
}
class AStar {
String status;
int f, g, h;
public AStar(String status, String target, int g) {
this.status = status;
this.g = g;
this.h = getH(status, target);
this.f = this.g + this.h;
}
// 计算启发函数
public static int getH(String status, String target) {
int ret = 0;
for (int i = 0; i < 4; ++i) {
int dist = Math.abs(status.charAt(i) - target.charAt(i));
ret += Math.min(dist, 10 - dist);
}
return ret;
}
}
代码原理:和752的方法一本质相同,先阅读完752再看本题解析,否则可能会看不懂
基本解释:列举出所有可能的排列情况,同时记录更新每次符合条件的最小数量
class Solution {
public static int numSquares(int n) {
// 创建动态数组存储小于等于传入数 n 的所有完全平方数(只有这些才能成为组成 n 的数)
List<Integer> ku = new ArrayList<Integer>();
for(int i = 1; i*i <= n; i++){
ku.add(i*i);
}
int size = ku.size();
// 若传入数 n 属于完全平方数,那么直接调用自己返回 1
if(ku.contains(n)){
return 1;
}
// 用于存储最少数量,永远只存储一个值
Queue<Integer> total = new LinkedList<Integer>();
// 由于要在下方做比较,所以开始值设置为 int 最大值
total.add(Integer.MAX_VALUE);
// 假如传入数为12,那么组合方式分为
// 以 1 起始,再加上 1、1、1、4、4 或
// 以 2 起始,再加上...
// 所以考虑到这些情况,我们从ku的第一项作为起始
for(int start = 0; start < size; start++){
// 这一块往下和752基本一致
Queue<Integer> queue = new LinkedList<Integer>();
queue.offer(ku.get(start));
// 由于当前queue中以存有ku.get(start),所以第一项已经被确定了
// 开始选择第二项往后,所以我们设置sum初始值为1
int sum = 1;
// 假设我们得到一个等于 n 的组合,那就设置bool为false,这样就可以结束下面的while、for循环
// 因为已经证实当前数量下可以等于 n,没必要再进行下去,直接以上面for循环的下一项作为起始再开始
boolean bool = true;
while(bool && !queue.isEmpty()) {
++sum;
int len = queue.size();
for(int i = 0; bool && i < len; i++){
int now = queue.poll();
// 由于组合序列的特点为起始项往后添加的项都大于等于起始项
// 所以的下标从start开始,而不是0
// 比如 4 9 是 13 的最小数量,但不以 9 4 的形式,
// 因为以 4 开头时已经有4、9这个序列了,没必要重复
for(int j = start; bool && j < size; j++){
if(now + ku.get(j) == n){
int pre = total.poll();
// 若历史中得到等于 n 的最少数量少于当前这次,那就保持total里的原值不变
if(pre <= sum){
total.add(pre);
bool = false;
}else{
// 否则替换
total.add(sum);
bool = false;
}
}
// 只有小于 n 时才加入,大于不行
if(now + ku.get(j) < n){
queue.offer(now + ku.get(j));
}
}
}
}
}
if(!total.isEmpty()){
return total.poll();
}
return -1;
}
}
为了优化时间复杂度:我尝试缩短遍历次数:
(1)将起始点根据 ku 的 size 大小做出动态变化,失败,可能存在 1、36 构成 37 的最短数量。
(2)所以我需要减少的地方不是开头的起始点,而是起始点往后的部分
原理:
dp[i]:表示完全平方数和为 i 的最小个数
我们需要得到组成 n 的最短序列和:首先找到 n 前面最大的一个完全平方数K(不从小的开始找而是从大的开始找,这样能大幅缩短时间,因为最短数量一定是由较大数们组成的),记为一个个数;
那么还剩n - K * K(也就是我们需要得到可以组成n - K * K的最短序列和),也就是说只要将n - k * k的解 dp[n-k*k](也就是前面我们提到的组成n - k * k 的最短序列和)加上上面那个1(我们找到的n前最大完全平方数 K 算序列里的一个值),这就是 n 的解,这就是最短的
class Solution {
public int numSquares(int n) {
// 默认初始化值都为0
int[] dp = new int[n + 1];
// 从 1 开始计算当前值的最少数量
for (int i = 1; i <= n; i++) {
// 最坏的情况就是每次+1,也就是n为4时最短序列由4个1构成,所以 = i;
dp[i] = i;
// 取得构成当前值的最少数量:
// 也就是我们从 1 开始顺次取 j,那么组成 i 的剩下的就是 i - j * j,但是这是有前提的
// 也就是 i - j * j >= 0,这正好体现了我们上面的原理部分。
for (int j = 1; i - j * j >= 0; j++) {
// 根据原理部分,我们开始将构成 i - j * j 的最少数量 +1 和 dp[i] 自身的值进行取更小
dp[i] = Math.min(dp[i], dp[i - j * j] + 1); // 动态转移方程
}
}
return dp[n];
}
}
为什么直接 dp[i] = Math.min(dp[i], dp[i - j * j] + 1); 就可以得到我们所需的值:
因为我们是从 n = 1 开始逐次计算最少数量,所以 dp 数组中下标 i 之前的所有值都是对应自己的最少数量,直接调用即可。
// 相同思路的优化版:虽然官方题解给的时间复杂度和上面的代码给的时间复杂度相同,但执行用时一个超越40,一个90
class Solution {
public int numSquares(int n) {
int[] f = new int[n + 1];
for (int i = 1; i <= n; i++) {
int minn = Integer.MAX_VALUE;
for (int j = 1; j * j <= i; j++) {
minn = Math.min(minn, f[i - j * j]);
}
f[i] = minn + 1;
}
return f[n];
}
}