华为笔试算法:清一色胡牌问题(java)

题目描述
清一色是麻将番种之一,指由一种花色的序数牌组成的和牌。数字1-9,每个数字最多有4张牌。我们不考虑具体花色,我们只看数字组合。
刻子:三张一样的牌;如: 111, 222, 333, ..., 999
顺子:三张连续的牌;如: 123, 234, 345, ..., 789
对子:两张相同的牌;如: 11, 22, 33, ..., 99
需要实现一个程序,判断给定牌,是否可以和牌(胡牌)。
和牌要求
- 麻将牌张数只能是 2, 5, 8, 11, 14
- 给定牌可以组合成,除1个对子以外其他都是刻子或顺子
举例

- "11"                    -> "11", 1对子,可以和牌
- "11122233"        -> "111"+"222"+"33", 2刻子,1对子,可以
- "11223344567"  -> "11"+"234"+"234"+"567", 1对子,3顺子,可以
                             -> "123"+"123"+"44"+"567", 另一种组合,也可以
输入描述:
合法C字符串,只包含'1'-'9',且已经按从小到大顺序排好;字符串长度不超过15。同一个数字最多出现4次,与实际相符。
输出描述:
C字符串,"yes"或者"no"
示例1
输入:2244

输出:no
————————————————

思路

先找刻子,再找顺子,找到顺子或刻子,则递归调用。剩下两张如果是对子,则胡牌

以下为具体代码(含分析)

public class Main {
    public static void main(String[] args){
        Scanner sc = new Scanner(System.in);
        while(sc.hasNext()) {
            String string = sc.nextLine();
            if(string.isEmpty()){
                System.out.println("请重新输入!");
                continue;
            }
            boolean matches = string.matches("[1-9]+");
            if(!matches){
                System.out.println("非法输入, 请重新输入!");
                continue;
            }
            int length = string.length();
            if(length != 2 && length != 5 && length != 8 && length != 11 && length != 14 ){
                System.out.println("no!");
                continue;
            }
            String[] split = string.split("");
            if(length == 2 && split[0] == split[1]){
                System.out.println("yes!");
                continue;
            }
            LinkedList<Integer> list = new LinkedList<>(Arrays.asList(split)
                    .stream().map(Integer::valueOf).collect(Collectors.toList()));
            boolean result = fun(list);
            System.out.println(result ? "yes" : "no");
        }
    }

    /**
     * 解析:
     * 1. 判断只剩对子的情况
     * 2. 先判断有没有刻子?
     *   2.1. 如果没有,则循环结束开始查询有没有顺子;
     *   2.2. 如果有,删除刻子元素后递归调用;
     *      2.2.1. 递归返回true, 则证明可以胡牌,继续返回即可;
     *      2.2.2. 递归返回false, 则恢复刚刚删除的刻子元素并跳出循环,让其找顺子
     *      依次递归...
     * 3. 判断有没有顺子?
     *   3.1. 如果没有,则循环结束自动返回false;
     *   3.2. 如果有,则删除顺子元素后递归调用;
     *      3.2.1. 递归返回true, 则可以胡牌,继续返回false即可
     *      3.2.2. 递归返回false, 则恢复刚刚删除的顺子元素,顺延开始元素换其他顺子方案继续查找
     *      (eg: "12344", 上次从链表最后一个开始找,找到234,剩下14,无法胡牌;
     *           则恢复删除的234,从列表的倒数第二个开始找顺子;
     *           依次顺延,最后找到123,剩下44)
     *
     * @param list
     * @return
     */
    private static boolean fun(LinkedList<Integer> list) {
        if(list.size() == 2){
            if(list.get(0) == list.get(1)){
                return true;
            }else{
                return false;
            }
        }
        boolean flag;
        for(int start = list.size()-1; start > 1; start--){
            //找刻子
            if(list.get(start) == list.get(start-1) && list.get(start-1) == list.get(start-2)){
                Integer a = list.get(start);
                list.remove(a);
                list.remove(a);
                list.remove(a);
                flag = fun(list);
                if(flag){
                   return true;
                }else{
                    list.add(start-2, a);
                    list.add(start-2, a);
                    list.add(start-2, a);
                    break;
                }
            }
        }
        int temp = list.size()-1;
        for(int start = temp; start > 1; start--){
            //找顺子
            if(list.get(start)-1 == list.get(start-1)){
                //找到第一个不相同的点
                int i = start-2;
                while(i>=0){
                    if(list.get(start-1)-1 == list.get(i)){
                        Integer a = list.get(start);
                        Integer b = list.get(start-1);
                        Integer c = list.get(i);
                        list.remove(a);
                        list.remove(b);
                        list.remove(c);
                        flag = fun(list);
                        if(!flag){
                            list.add(i, c);
                            list.add(start-1, a);
                            list.add(start-1, b);
                            start = temp--;
                            break;
                        }else{
                            return true;
                        }
                    }
                    i--;
                }
            }
        }
        return false;
    }
}

 

<think>好的,我现在需要帮助用户深入解析JAVA实现麻将算法,并提供代码示例。首先,用户提到了回溯法和查表法,参考了站内的两个引用,其中引用[2]是关于华为清一色问题,用Java实现。我需要结合这些信息,详细解释算法思路和代码结构。 首先,我应该先回顾麻将的基本规则。根据用户提供的引用,需要满足特定的数,并且组合成一个对子加上刻子或顺子。例如,清一色的情况,数字1-9,每个最多4张。所以,的条件是:数符合2、5、8、11、14,且除去一个对子后,剩下的能组成刻子或顺子。 接下来,我需要考虑如何用Java实现这个算法。回溯法可能是一个可行的方法,因为需要尝试不同的组合方式。回溯法的基本思路是递归地尝试每一种可能的刻子或顺子,并验证是否满足条件。同时,考虑到性能问题,可能需要剪枝来优化。 然后,查表法也是一个可能的优化方法。查表法可能预先计算所有可能的组合,然后通过查表快速判断。但这种方法可能需要较大的存储空间,特别是当型变化多的时候。不过对于清一色来说,可能还是可行的,因为数字只有1-9,相对有限。 现在,我需要确定用户具体需要的是哪种实现方式。用户的问题中提到了希望详细解析和代码示例,所以可能更适合用回溯法,因为查表法的代码可能较为复杂,且需要预先生成表。而回溯法可以更直观地展示算法步骤。 接下来,我需要设计算法步骤。首先,检查的数量是否符合条件。然后,遍历所有可能的对子,移除对子后,检查剩下的是否能被分解为刻子或顺子。对于剩下的,递归或迭代地尝试移除刻子或顺子,直到所有都被处理完毕。 在代码实现上,可能需要使用一个数组或Map来记录每个数字的数量。例如,使用一个长度为10的数组(索引1-9),每个元素表示该数字的数量。然后,递归地处理每个位置,尝试作为刻子或顺子的开始。 需要注意的是,顺子必须是三个连续的数字,所以处理时要检查i+1和i+2的位置是否有足够的。刻子则是三个相同的数字。 同时,要考虑回溯的剪枝条件。例如,当某个位置的数量为0时,直接跳过。或者,当处理完某个位置后,后面的位置不需要再重复处理。 现在,我需要将这些思路转化为具体的Java代码。例如,定义一个方法isHu,接收一个数组作为参数。首先检查数是否合法,然后尝试每个可能的对子,移除对子后调用另一个方法检查剩余是否能组成刻子或顺子。 在检查剩余的方法中,递归地处理每个位置。对于当前的位置,如果数量为0,则跳过。否则,尝试作为刻子或顺子。如果是刻子,减去三个,继续递归。如果是顺子,检查后续两个位置是否有足够的,然后各减一,继续递归。如果两者都不行,则返回false。 需要考虑数组的复制问题,避免修改原数组。或者在处理时使用深拷贝,或者在回溯时恢复状态。但为了效率,可能需要在原数组上操作,并在递归返回时恢复状态。但Java是值传递,数组作为参数传递的是引用,所以需要注意这一点。可能需要复制数组后再处理。 或者,使用回溯时,在尝试刻子或顺子后,修改数组,递归后再恢复原来的值。这样可以在同一个数组上操作,节省空间。例如,在处理完刻子后,减去三个,递归检查,然后恢复这三个。类似地,顺子处理时减去三个连续的,递归后恢复。 这样的方式可以避免频繁复制数组,提高效率。但需要注意递归中的状态管理。 现在,编写代码的大致步骤: 1. 统计每个数字的数量,存入数组counts。 2. 检查数是否是2、5、8、11、14中的一个,否则返回false。 3. 遍历每个可能的对子(i从1到9): a. 如果counts[i] >= 2,则减去2。 b. 调用检查剩余是否能组成刻子或顺子的方法。 c. 如果返回true,则恢复counts[i]并返回true。 d. 否则,恢复counts[i]继续下一个可能的对子。 4. 如果没有找到符合条件的对子,返回false。 检查剩余的方法: 递归函数,参数是当前处理的起始位置和counts数组: 1. 从起始位置开始,找到第一个数量不为0的位置i。 2. 如果i超过9,说明所有都处理完毕,返回true。 3. 尝试处理刻子:如果counts[i] >=3,则减去3,递归调用,如果成功返回true,否则恢复3。 4. 尝试处理顺子:如果i <=7且counts[i] >=1、counts[i+1] >=1、counts[i+2] >=1,则各减1,递归调用,如果成功返回true,否则恢复各1。 5. 如果两种方式都不行,返回false。 需要注意的是,递归时应该从当前i开始,而不是i+1,因为处理完刻子或顺子后,当前i的数量可能已经减少到0,或者还有剩余。例如,如果处理了刻子,i的数量变为0,那么下一个处理的位置应该是i+1。但如果处理了顺子,i的位置减1后可能还有剩余,所以需要继续处理i的位置。 或者,在递归时,每次处理完i后,继续处理i,直到i的数量为0,再移动到i+1。这可能更高效,但需要正确管理。 例如,在检查剩余的函数中,首先找到第一个非零的i,然后处理该i的可能组合,然后递归处理剩下的部分。 现在,将这些思路转化为代码。 可能的代码结构: public class Mahjong { public static boolean isHu(int[] counts) { int total = 0; for (int i = 1; i <= 9; i++) { total += counts[i]; } if (!(total == 2 || total ==5 || total ==8 || total ==11 || total ==14)) { return false; } // 尝试每个可能的对子 for (int i = 1; i <=9; i++) { if (counts[i] >=2) { counts[i] -=2; if (checkRemaining(counts)) { counts[i] +=2; // 恢复 return true; } counts[i] +=2; // 恢复 } } return false; } private static boolean checkRemaining(int[] counts) { // 复制数组,避免修改原数组 int[] copy = Arrays.copyOf(counts, counts.length); return backtrack(copy, 1); } private static boolean backtrack(int[] counts, int start) { if (start >9) return true; if (counts[start] ==0) { return backtrack(counts, start+1); } // 尝试刻子 if (counts[start] >=3) { counts[start] -=3; boolean res = backtrack(counts, start); counts[start] +=3; if (res) return true; } // 尝试顺子 if (start <=7 && counts[start] >=1 && counts[start+1] >=1 && counts[start+2] >=1) { counts[start]--; counts[start+1]--; counts[start+2]--; boolean res = backtrack(counts, start); counts[start]++; counts[start+1]++; counts[start+2]++; if (res) return true; } return false; } } 但这里可能存在错误。例如,在backtrack函数中,处理刻子时,是否应该移动到下一个start?或者继续处理当前start? 例如,当处理刻子后,counts[start]可能变为0,那么下一次递归应该处理start+1。否则,如果还有剩余,比如原来的counts[start]是4,减去3后还剩1,这时候需要继续处理start的位置。 但当前的代码中,调用backtrack(counts, start),即处理同一个start位置,这可能有问题。例如,假设start=1,处理刻子后counts[1]变为1,那么再次递归进入backtrack时,会再次检查start=1,此时counts[1]=1,无法组成刻子或顺子,导致返回false,但可能漏掉了其他组合方式。 这说明递归处理方式存在问题。正确的做法应该是,处理完当前start位置的刻子或顺子后,继续处理start位置,直到其数量为0,然后移动到start+1。或者,每次处理完一个组合后,继续处理剩下的部分。 可能的解决方法是,在backtrack函数中,处理完刻子或顺子后,再次从start开始处理,而不是start+1。例如: 比如,处理刻子后,counts[start]减少了3,此时可能counts[start]还有剩余,例如原本是4,减3后还剩1。这时候需要继续处理start的位置。所以在调用backtrack时,参数仍然是start,而不是start+1。 同样,处理顺子后,可能start的位置还有剩余,所以继续从start处理。 这样,递归会一直处理当前start,直到该位置的counts为0,或者无法再组成组合,此时才会移动到start+1。 这可能正确,但需要测试。 例如,假设counts数组为:[0,2,2,2,0,...]。当处理顺子1-2-3后,counts变为[0,1,1,1,...],然后递归调用backtrack(start=1)。此时,再次检查start=1的位置,发现counts[1]=1,无法组成刻子或顺子,返回false。但实际上,可能存在其他组合方式,比如另一个顺子1-2-3,但此时数量不够。 这说明,这样的处理方式可能无法覆盖所有情况。因此,可能需要调整递归的逻辑。 另一个思路是,每次处理完刻子或顺子后,递归调用时,仍然从start开始,而不是start+1,因为可能在该位置还有剩余的需要处理。只有当当前start位置的counts为0时,才移动到下一个位置。 或者,在backtrack函数中,每次处理完当前start的位置后,不管是否处理了刻子或顺子,都继续处理start位置,直到该位置的counts为0,然后才移动到start+1。这可能更合理。 例如,修改backtrack函数: private static boolean backtrack(int[] counts, int start) { // 找到第一个非零的位置 while (start <=9 && counts[start] ==0) { start++; } if (start >9) return true; // 处理当前start位置 // 尝试刻子 if (counts[start] >=3) { counts[start] -=3; boolean res = backtrack(counts, start); counts[start] +=3; if (res) return true; } // 尝试顺子 if (start <=7 && counts[start] >=1 && counts[start+1] >=1 && counts[start+2] >=1) { counts[start]--; counts[start+1]--; counts[start+2]--; boolean res = backtrack(counts, start); counts[start]++; counts[start+1]++; counts[start+2]++; if (res) return true; } // 如果两种方式都不行,返回false return false; } 这样,每次递归调用时,首先跳过所有已经处理完的位置,找到当前第一个非零的start。然后尝试处理该位置的刻子或顺子。如果处理了刻子或顺子,递归调用时会再次从当前start开始,处理剩下的。这可能更合理,因为处理完刻子或顺子后,当前start的位置可能还有剩余的需要处理。 例如,假设start=1,counts[1]=4。处理刻子(减3)后,counts[1]=1,然后递归调用backtrack(counts, 1)。此时,进入函数后,start=1,但counts[1]=1,无法组成刻子或顺子,返回false。这时候,回溯到之前的步骤,恢复counts[1]到4,然后尝试顺子。 但counts[1]为4的情况下,可能可以先处理顺子。例如,处理顺子1-2-3,减去各1,此时counts[1]=3,counts[2]=剩下的,然后递归处理。但这样可能更复杂。 这样的逻辑是否能覆盖所有情况呢?例如,假设型是111222333,那么需要分解为三个顺子。处理方式可能是,先处理顺子1-2-3,但每个位置的数量都是3,处理顺子后,每个位置减1,变为2,然后继续处理start=1,此时再次处理顺子,直到每个位置的数量为0。 但可能需要多次处理顺子。例如,第一次处理顺子后,counts变为[2,2,2,...]。第二次处理顺子后变为[1,1,1,...]。第三次处理顺子后变为0。这样就能成功。 而如果使用上述的backtrack函数,处理顺子后递归调用时,start仍然是1,所以会继续处理剩下的。 这样的逻辑是否正确? 例如,当前start=1,counts[1]=3,counts[2]=3,counts[3]=3。第一次处理刻子的话,counts[1]减3,变为0,递归调用backtrack(1)会进入start=1,发现counts[1]=0,start++到2,然后处理刻子,counts[2]减3,递归调用backtrack(2)变为0,然后处理start=3,减3,最后成功返回true。这样就能正确识别刻子组合。 如果处理顺子的话,第一次处理顺子1-2-3,各减1,counts变为2,2,2。递归调用backtrack(1),再次处理start=1。此时counts[1]=2,无法组成刻子或顺子,所以返回false。然后回溯,恢复counts,尝试处理刻子。此时counts[1]=3,处理刻子,减3,然后处理剩下的counts[2]=3和counts[3]=3,最终成功。 所以,这样的逻辑可能正确。 但可能存在其他情况,比如型为1112345678999,这时候需要组合顺子和刻子。算法是否能正确处理? 可能还需要更多的测试用例来验证。 现在,用户需要一个详细的解析和代码示例,所以需要将上述思路整理成步骤,并给出代码。 另外,需要处理输入如何转化为counts数组。例如,用户的输入可能是一个列表,如[1,1,2,2,3,3,4,4,5,5,6,6,7,7],需要统计每个数字出现的次数。 在代码示例中,可以给出一个测试案例,例如引用[2]中的例子:输入是1,1,1,2,2,3,3,4,4,数为9,符合条件吗?根据题目描述,和需要除一个对子外都是刻子或顺子。例如,这个例子可能有对子1(两个1),然后剩下的1、2、2、3、3、4、4。这似乎无法组成刻子或顺子,所以可能不能。但需要具体分析。 假设用户输入的已经转换为counts数组,例如counts[1]=3, counts[2]=2, counts[3]=2, counts[4]=2。则对子可能是2,3,或4中的一个。例如,选择对子2,减去2后,counts[2]=0,剩下的是1的三个,2的0,3的两个,4的两个。然后检查剩下的是否能组成刻子或顺子。剩下的counts[1]=3,可以组成一个刻子,减去3后,剩下counts[3]=2和4=2。这两个无法组成顺子或刻子,所以返回false。因此,这个例子不能。 另一个例子:1,1,1,2,3,4,5,6,7,8,9,9,9。数是13,不符合条件。所以需要确保数正确。 现在,将上述思路整理成步骤: 1. 输入校验:检查数是否符合2、5、8、11、14。 2. 尝试所有可能的对子。 3. 对每个对子,移除后检查剩余是否能组成刻子或顺子。 4. 检查剩余的方法是回溯法,递归尝试刻子或顺子。 代码中的关键点是正确实现回溯,确保所有可能的组合都被尝试。 可能的优化点:在回溯时,按顺序处理每个数字,避免重复计算。例如,处理完位置i后,不需要再处理i之前的数字,因为前面的已经处理完毕。 现在,编写代码时需要注意Java的数组操作,以及递归的正确性。 最后,给出代码示例,并解释关键步骤。 同时,用户可能需要知道如何处理输入,例如将列表转换为counts数组。可以补充说明这部分,但用户的问题主要是算法实现,所以代码示例可以假设输入已经是counts数组。 总结,回答的结构应该是: 1. 算法基础:条件。 2. 实现思路:回溯法,分步骤解析。 3. 代码示例:Java实现,包括输入校验、对子遍历、回溯检查。 4. 示例测试:演示如何使用代码。 5. 优化提示:剪枝、查表法等。</think>### JAVA实现麻将算法解析 #### 一、算法基础 麻将需满足两个核心条件: 1. 数符合$2,5,8,11,14$张的约束 2. 组合结构为$1$个对子 + 若干刻子/顺子 其中: - **刻子**:三张相同,如$111$ - **顺子**:三张连续,如$123$ - **对子**:两张相同,如$11$ #### 二、实现思路(回溯法) 1. **输入验证** 检查总数是否符合合法数值,若不符合直接返回`false` 2. **遍历对子** 尝试从每个数字中取出对子(需至少2张),剩余进入组合验证 3. **递归验证剩余** 使用回溯法验证剩余是否能组成刻子或顺子: - **终止条件**:所有处理完毕 - **剪枝策略**:跳过数量为0的数字 - **选择路径**: - 优先尝试刻子组合(减少递归深度) - 再尝试顺子组合(需连续三张) #### 三、代码实现 ```java import java.util.Arrays; public class MahjongHuChecker { public static boolean isHu(int[] tiles) { int[] counts = new int[10]; // 索引0不使用,1-9对应数字 for (int tile : tiles) counts[tile]++; int total = tiles.length; // 验证数合法性 if (!(total == 2 || total == 5 || total == 8 || total == 11 || total == 14)) { return false; } // 尝试所有可能的对子 for (int i = 1; i <= 9; i++) { if (counts[i] >= 2) { counts[i] -= 2; // 移出对子 if (checkRemaining(counts)) { counts[i] += 2; // 回溯恢复 return true; } counts[i] += 2; // 回溯恢复 } } return false; } private static boolean checkRemaining(int[] counts) { int[] copy = Arrays.copyOf(counts, counts.length); return backtrack(copy, 1); } private static boolean backtrack(int[] counts, int start) { // 跳过已处理完毕的数字 while (start <= 9 && counts[start] == 0) start++; if (start > 9) return true; boolean result = false; // 尝试刻子组合 if (counts[start] >= 3) { counts[start] -= 3; result = backtrack(counts, start); counts[start] += 3; if (result) return true; } // 尝试顺子组合(需确保后续两个数字存在) if (start <= 7 && counts[start] >= 1 && counts[start + 1] >= 1 && counts[start + 2] >= 1) { counts[start]--; counts[start + 1]--; counts[start + 2]--; result = backtrack(counts, start); counts[start]++; counts[start + 1]++; counts[start + 2]++; if (result) return true; } return false; } // 测试用例 public static void main(String[] args) { // 示例1:组合 11 123 456 789 99(含1对子+4顺子) int[] case1 = {1,1,1,2,3,4,5,6,7,8,9,9,9}; System.out.println(isHu(case1)); // 输出true // 示例2: 不可组合(缺少有效组合) int[] case2 = {1,1,2,2,3,3,4,4,5}; System.out.println(isHu(case2)); // 输出false } } ``` #### 四、性能优化方向 1. **剪枝优化** - 优先处理刻子可减少递归深度 - 预处理排除明显不合法型(如单张超过4张) 2. **查表法** 预生成所有合法组合,通过哈希表快速查询[^2] 3. **状态压缩** 使用位运算表示型组合,提升处理速度 #### 五、应用场景 该算法适用于: - 麻将游戏判定 - 型分析工具开发 - 游戏AI决策模块[^1]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值