搭船问题(父母兄妹一家四口和隔壁杰哥出去旅游...)
某日在扣扣群里见到的沙雕网友发的奇怪东西,稍微记录一下...
原问题

喂喂,这些牙白的组合真的是去旅游的吗?
另外提问
这玩意看着挺绕但确实有解
但问题来了,有多少个不同的解?
先规定一下嗷,一个解里面不能重复出现相同的状态,否则该解无效
这里的状态简单来说就是:某时刻船的位置(对岸还是本岸)+对岸人员组成
比如,一个有效解如下:
父+子 去,父 回
父+杰 去,杰 回
母+女 去,父 回
父+杰 去
但无效解如下:
父+子 去,父 回 <- (与这里的“父亲回来”后的状态重复)
父+杰 去,父+杰 回 <-“父+杰”回来之后,与之前状态重复,简单来说就是有做无用功的部分
父+杰 去,杰 回
母+女 去,父 回
父+杰 去
揭晓答案
估计很多人也只想知道答案,那么看完这里就可以啦~
实际上不同的解有256种,其中步骤最少的解法有8种,步骤最长的解法有6种
实际上尽管没有重复状态,但因为有各种排列组合才构成这么多解的
其中8种步骤最短的解法是:
(去对岸) (回本岸) ===== 方案1 ===== 父 + 杰 父 父 + 子 杰 母 + 女 父 父 + 杰 ===== 方案2 ===== 父 + 杰 父 父 + 子 子 母 + 女 父 父 + 子 ===== 方案3 ===== 父 + 杰 父 母 + 女 杰 父 + 杰 父 父 + 子 ===== 方案4 ===== 父 + 杰 父 母 + 女 杰 父 + 子 父 父 + 杰 ===== 方案5 ===== 父 + 子 父 父 + 杰 杰 母 + 女 父 父 + 杰 ===== 方案6 ===== 父 + 子 父 父 + 杰 子 母 + 女 父 父 + 子 ===== 方案7 ===== 父 + 子 父 母 + 女 子 父 + 杰 父 父 + 子 ===== 方案8 ===== 父 + 子 父 母 + 女 子 父 + 子 父 父 + 杰
6种步骤最长的解法是:
(去对岸) (回本岸) ===== 方案1 ===== 父 + 杰 杰 子 父 母 + 女 子 父 + 杰 母 + 女 子 父 + 子 母 + 女 母 父 + 子 杰 + 女 母 + 女 父 杰 子 父 + 子 ===== 方案2 ===== 父 + 杰 杰 子 父 母 + 女 子 父 + 杰 母 + 女 子 父 + 子 母 + 女 杰 + 女 父 + 子 母 母 + 女 父 杰 子 父 + 子 ===== 方案3 ===== 父 + 杰 杰 子 父 母 + 女 子 父 + 子 母 + 女 母 父 + 子 杰 + 女 母 + 女 父 + 子 子 母 + 女 父 子 杰 父 + 杰 ===== 方案4 ===== 父 + 杰 杰 子 父 母 + 女 子 父 + 子 母 + 女 杰 + 女 父 + 子 母 母 + 女 父 + 子 子 母 + 女 父 子 杰 父 + 杰 ===== 方案5 ===== 父 + 子 子 杰 父 母 + 女 母 父 + 子 杰 + 女 母 + 女 父 + 子 子 母 + 女 父 + 杰 子 母 + 女 父 子 杰 父 + 杰 ===== 方案6 ===== 父 + 子 子 杰 父 母 + 女 杰 + 女 父 + 子 母 母 + 女 父 + 子 子 母 + 女 父 + 杰 子 母 + 女 父 子 杰 父 + 杰
另外若有兴趣看全部的解法,可以点这里
问题分解简化
已知:
- T1:船一次只能装2人,想开船至少得有1个人在船上
- T2:父亲和妹妹独处会XX
- T3:母亲和男性独处会XX
- T4:哥哥和妹妹独处会XX
- T5:妹妹没法单独开船
- T6:杰哥和哥哥独处会XX
问:有没有办法在不XX的情况下让所有人过河?
符号化表述
令父亲为,杰哥为,哥哥为,母亲为,女儿为,则有
已知:
- 全集:
- 禁止的组合:
- 船上的人:
- 对岸的人:
- 本岸的人:
问:
是否存在有序序列
以及有序序列
使得
这个问法可能很绕,但没关系这不是重点,可以先放一放
代码思路说明
集合的表达(编码和翻译)
因为就5个人,所以对岸或本岸上的人员组成情况可以只用五位二进制数表示,这样表示的集合运算很高效
规定最高位(从左到右第一位数字)代表a(父亲,就像符号化中所规定的),次高位代表b,以此类推...
所以对于集合B(禁止的组合),可以表示为
{10010
,10001
,01100
,01010
,00110
,00101
}
对于集合T(船上的人),可以用代码求出来,或者直接列出来
{10000
,01000
,00100
,00010
,11000
,10100
,01001
,00011
}
// 计算集合T public List<Integer> getSetT(Integer setU, HashSet<Integer> setB, Integer elementE){ List<Integer> setT = getSetWithAllNElements(32, 1, 0, null); // T=由所有只有一个元素的集合构成的集 setT.addAll(getSetWithAllNElements(32, 2, 0, null)); // T=T∪由所有只有两个元素的集合构成的集 setT.removeAll(setB); // T=T-B setT.remove(elementE); // T=T-{e} return setT; } // 生成:由所有只有n个元素的集合构成的集,(setSize幂集的元素个数,numberPrefix之前递归积累下来的前缀,nElementsSet收集容器) public List<Integer> getSetWithAllNElements(int setSize, int n, int numberPrefix, List<Integer> nElementsSet){ if(nElementsSet == null) nElementsSet = new ArrayList<>(setSize); while(setSize > 1){ setSize >>= 1; // setSize只有一个二进制位为1,且每次右移一位 if(n == 1){ nElementsSet.add(numberPrefix | setSize); // 之前累积的前缀与当前位结合 }else{ getSetWithAllNElements(setSize, n-1, numberPrefix | setSize, nElementsSet); // 要求的元素个数n减一,递归继续 } } return nElementsSet; }
在求解结束后我们将得到集合序列,即对岸组成人员的序列,将序列中相邻的集合进行异或处理就可以得到渡船过程
求简单路径
一些定义
对岸的状态标识为(有顺序地):当前船的位置(对岸还是本岸)+当前对岸人员组成
对岸的状态转换标识为(有顺序地):上次对岸人员组成+当前对岸人员组成
注意到这里的状态转换是经过简化的,当然也可以使用较冗余的“上次对岸状态+当前对岸状态”来标识,它们都包含着相同的信息
将状态视作图中结点,状态转换视作结点间的有向边,这样构成一张有向图
起点即“船在本岸+对岸无人”,终点即“船在对岸+对岸满人”
则我们的目标为:找到从起点到终点的所有简单路径
深搜思路
和求解图的简单路径的一般方法相同
每搜索一个未封禁的邻近结点就暂时封禁该结点,直到得出该结点到终点的所有简单路径后才解封
访问结点同时记录其中的路径
到终点时,把记录的路径添加到简单路径集合中(因为路径有顺序信息,所以此时状态也可以简化为人员组成)
另外附上由所有简单路径构成的图信息点这里
coding♪
全部代码如下
import java.util.Arrays; import java.util.List; import java.util.ArrayList; import java.util.HashSet; class Solution { // 压位,一个数字代表一个集合,最高位=1则有a,次高位=1则有b,以此类推... private String[] codeName = {"女", "母", "子", "杰", "父"}; // 5个人对应的称呼 private int sizeH = 5; // 5个人 private int sizeP = 1 << sizeH; // 5个人的所有可能组合(包括无人),即幂集的元素个数 private int mod = sizeP - 1; // 共用的模 private Integer setEmpty = 0; // 空集 private Integer setU = sizeP - 1; // 满集 private Integer setFinal = setU; // 最终状态是满集 // 集合B,禁止独处集合 private HashSet<Integer> setB = new HashSet<>(Arrays.asList( b2d("10010"), b2d("10001"), b2d("01100"), b2d("01010"), b2d("00110"), b2d("00101"))); // 集合T,允许渡船集合 private List<Integer> setT = Arrays.asList(b2d("10000"), b2d("01000"), b2d("00100"), b2d("00010"), b2d("10100"), b2d("01001"), b2d("00011")); // 封禁重复的结点 private HashSet<Integer> banMark = new HashSet<>(); // 记录单条简单路径(能成功到齐对岸的单个方案,记录对岸的变换过程) ArrayList<Integer> record = new ArrayList<>(); // 记录所有简单路径 private List<ArrayList<Integer>> allRecords = new ArrayList<>(); public static void main(String[] args) { Solution solu = new Solution(); solu.dealing(); solu.verbose(); } public void dealing(){ dfs(setEmpty, setEmpty, 0); } public void verbose(){ //printAnswers(allRecords); printSpecialAnswers(allRecords, true); } // 二进制字符串转十进制整形 public int b2d(String binaryString){ return Integer.parseInt(binaryString, 2); } // 十进制整形转二进制字符串 public String d2b(int decimalVal){ return Integer.toBinaryString(decimalVal); } // 深搜validMark,生成有效路径图,(lastSetA上一状态中的人员组成,当前状态中的人员组成,step为0船在本岸,step为1船在对岸) public boolean dfs(Integer lastSetA, Integer setA, int step){ int state = step << sizeH | setA; // 当前状态(代表当前结点) if(banMark.contains(state)) { return false; // 若当前状态被禁,则退出搜索 } banMark.add(state); // 暂时封禁当前状态 if(setB.contains(setA) || setB.contains(setA ^ mod)) { return false; // 若有岸上的人会发生关系,则退出当前搜索,永封当前状态 } boolean haveValid = false; // 当前状态是否合理(是否有当前结点到终点的简单路径) int lend = setA ^ mod ^ mod*step; // 船从哪个岸出发 for (Integer trans : setT) { // 遍历所有船可以载人的情况 if((lend & trans) == trans){ // 船要载的人都在岸上 int nextSetA = setA ^ trans; // 下一状态中的人员组成 record.add(nextSetA); // 记录路径中的结点 if(nextSetA == setFinal){ // 终点,保存路径 haveValid = true; allRecords.add(new ArrayList<Integer>(record)); }else if(dfs(setA, nextSetA, step ^ 1)){ // 继续深搜下一状态 haveValid = true; } record.remove(record.size()-1); // 回溯 } } banMark.remove(state); // 解禁 return haveValid; // 当前状态不能导向全员到齐的状态,返回失败且不解禁当前索引 } // 翻译并输出指定的方案answers public void printAnswers(List<ArrayList<Integer>> answers){ System.out.println("总共有"+answers.size()+"种不同的过河方案"); int t = 0; for (ArrayList<Integer> answer : answers) { System.out.println("\n\n===== 方案"+ (++t) +" ====="); int step = 0, count = 0, lastSetA = 0; for(Integer setA : answer){ if(step == 1) System.out.printf("%"+(6-count)*5+"s", ""); count = 0; String setTStr = d2b(setA ^ lastSetA); lastSetA = setA; int len = setTStr.length(); System.out.print(codeName[len-1]); for(int i=1; i<len; ++i){ if(setTStr.charAt(i) == '1'){ ++count; System.out.print(" + "+codeName[len-i-1]); } } if(step == 1) System.out.println(); step ^= 1; } } } // 选择allrecords中的最优或最坏方案,switchBestWorst为true则选出最优,switchBestWorst为false则选择最坏 public void printSpecialAnswers(List<ArrayList<Integer>> answers, boolean switchBestWorst){ int shortestSize = answers.get(0).size(); List<ArrayList<Integer>> bestAnswers = new ArrayList<>(); for (ArrayList<Integer> answer : answers) { int size = answer.size(); if(size == shortestSize){ bestAnswers.add(answer); }else if(switchBestWorst && size < shortestSize || switchBestWorst && size > shortestSize){ bestAnswers.clear(); shortestSize = size; bestAnswers.add(answer); } } System.out.println( (switchBestWorst?"最佳":"最坏") + "过河方案需要过河"+shortestSize+"次"); printAnswers(bestAnswers); // 将选中的结果输出 } }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!