搭船问题(父母兄妹一家四口和隔壁杰哥出去旅游...)
某日在扣扣群里见到的沙雕网友发的奇怪东西,稍微记录一下...
原问题
喂喂,这些牙白的组合真的是去旅游的吗?
另外提问
这玩意看着挺绕但确实有解
但问题来了,有多少个不同的解?
先规定一下嗷,一个解里面不能重复出现相同的状态,否则该解无效
这里的状态简单来说就是:某时刻船的位置(对岸还是本岸)+对岸人员组成
比如,一个有效解如下:
父+子 去,父 回
父+杰 去,杰 回
母+女 去,父 回
父+杰 去
但无效解如下:
父+子 去,父 回 <- (与这里的“父亲回来”后的状态重复)
父+杰 去,父+杰 回 <-“父+杰”回来之后,与之前状态重复,简单来说就是有做无用功的部分
父+杰 去,杰 回
母+女 去,父 回
父+杰 去
揭晓答案
估计很多人也只想知道答案,那么看完这里就可以啦~
实际上不同的解有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=\left\{a,b,c,d,e \right\}\)
- 禁止的组合:\(B=\left\{\left\{a,d\right\},\left\{a,e\right\},\left\{b,c\right\},\left\{b,d\right\},\left\{c,d\right\},\left\{c,e\right\}\right\}\)
- 船上的人:\(T=\left\{ \left\{x,y\right\}\;\;|\;\;x\in U\;\;and\;\;y\in U\;\;and\;\;\left\{x,y\right\}\notin B\;\;and\;\;\left\{x,y\right\}\notin \left\{\left\{e\right\},\left\{\phi\right\}\right\}\right\}\;\;\;\)
- 对岸的人:\(A_{1}=\phi\)
- 本岸的人:\(C_{1}=U\)
问:
是否存在有序序列\(D_{A}=\left ( A_{1},\;A_{2},\;...,\;A_{2n}\right )\;\;\;\left (A_{i}\notin B,\;\;i=1,2,3,...,2n\;\;\;and\;\;\;\left| A_{2k}\right| > \left| A_{2k-1}\right|,\;\;k=1,2,3,...,n\;\;\;and\;\;\;A_{2n}=U\right )\)
以及有序序列\(D_{C}=\left ( C_{1},\;C_{2},\;...,\;C_{2n}\right )\;\;\;\left (C_{i}\notin B,\;\;i=1,2,3,...,2n\;\;\;and\;\;\;\left| C_{2k}\right| < \left| C_{2k-1}\right|,\;\;k=1,2,3,...,n\;\;\;and\;\;\;C_{2n}=\phi\right )\)
使得\(A_{k+1}\bigoplus A_{k}=D_{k+1}\bigoplus D_{k}\;\;\;\left (A_{i+1}\bigoplus A_{i}\in T,\;\;i=1,2,3,...,2n\right )\)
这个问法可能很绕,但没关系这不是重点,可以先放一放
代码思路说明
集合的表达(编码和翻译)
因为就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); // 将选中的结果输出
}
}

浙公网安备 33010602011771号