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

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

原问题

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

另外提问

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

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

揭晓答案

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

实际上不同的解有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);  // 将选中的结果输出
    }
}
posted @ 2023-02-17 02:13  kksk43  阅读(286)  评论(0)    收藏  举报
特效
黑夜
侧边栏隐藏