搭船问题(父母兄妹一家四口和隔壁杰哥出去旅游...)

某日在扣扣群里见到的沙雕网友发的奇怪东西,稍微记录一下...

原问题

喂喂,这些牙白的组合真的是去旅游的吗?

另外提问

这玩意看着挺绕但确实有解
但问题来了,有多少个不同的解?

先规定一下嗷,一个解里面不能重复出现相同的状态,否则该解无效
这里的状态简单来说就是:某时刻船的位置(对岸还是本岸)+对岸人员组成
比如,一个有效解如下:
父+子 去,父 回
父+杰 去,杰 回
母+女 去,父 回
父+杰 去
但无效解如下:
父+子 去,父 回       <- (与这里的“父亲回来”后的状态重复)
父+杰 去,父+杰 回 <-“父+杰”回来之后,与之前状态重复,简单来说就是有做无用功的部分
父+杰 去,杰 回
母+女 去,父 回
父+杰 去

揭晓答案

估计很多人也只想知道答案,那么看完这里就可以啦~

实际上不同的解有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的情况下让所有人过河?

符号化表述

令父亲为a,杰哥为b,哥哥为c,母亲为d,女儿为e,则有
已知:

  • 全集:U={a,b,c,d,e}
  • 禁止的组合:B={{a,d},{a,e},{b,c},{b,d},{c,d},{c,e}}
  • 船上的人:T={{x,y}|xUandyUand{x,y}Band{x,y}{{e},{ϕ}}}
  • 对岸的人:A1=ϕ
  • 本岸的人:C1=U

问:
是否存在有序序列DA=(A1,A2,...,A2n)(AiB,i=1,2,3,...,2nand|A2k|>|A2k1|,k=1,2,3,...,nandA2n=U)
以及有序序列DC=(C1,C2,...,C2n)(CiB,i=1,2,3,...,2nand|C2k|<|C2k1|,k=1,2,3,...,nandC2n=ϕ)
使得Ak+1Ak=Dk+1Dk(Ai+1AiT,i=1,2,3,...,2n)

这个问法可能很绕,但没关系这不是重点,可以先放一放

代码思路说明

集合的表达(编码和翻译)

因为就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); // 将选中的结果输出
}
}
posted @   kksk43  阅读(264)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
特效
黑夜
侧边栏隐藏
点击右上角即可分享
微信分享提示