减治法——生成组合对象

生成组合对象

组合对象中最重要的类型就是排列、组合和给定集合的子集。一般来说,这种情况出现在要对不同的选择进行考虑的问题中

生成排列

为了简单起见,假设需要对元素进行排列的集合是从1到n的简单整数集合。为了使它更具有一般性,可以把它们解释为n个元素集合 { a 1 . . . a n } \{a_1...a_n\} {a1...an}中元素的下标。对于生成 { 1... n } \{1...n\} {1...n}的所有n!个排列的问题,减一技术有什么好建议呢?该问题的规模减一就是要生成所有(n一1)!个排列。假设这个较小的问题已经解决了,我们可以把n插入n一1个元素的每一种排列中的n个可能位置中去,来得到较大规模问题的一个解。按照这种方式生成的所有排列都是独一无二的,并且它们的总数量应该是n(n-1)!=n!。这样,
我们得到了 { 1... n } \{1...n\} {1...n}的所有排列。
我们既可以从左到右也可以从右到左把n插入前面生成的排列中。从实际情况来看,
下面这种做法是有好处的:一开始从右到左把n插入12…(n-1)的位置中,然后每处理一
{ 1... n − 1 } \{1...n-1\} {1...n1}的新排列时,再调换方向。下图给出了应用该方法从底向上对n=3的情况
进行处理的例子。
在这里插入图片描述
以这种次序来生成排列有什么好处呢?因为实际上它满足所谓的最小变化要求:因为仅仅需要交换直接前趋中的两个元素就能得到任何一个新的排列。这个最小变化要求不仅有利于提高该算法的速度,而且对使用这些排列的应用也有好处。例如,穷举查找解旅行推销
员问题时需要城市的一个排列。如果这个排列是由最小变化算法生成的,从它前趋的路线
长度中算出一条新路线的长度所需要的时间将是一个常量,而不是线性的。

Johnson-Trotter算法

对于n的较小值来说,不用生成排列的方式来得到n个元素的相同次序的排列是有可能的。为了做到这一点,我们给一个排列中的每个元素k赋予一个方向。我们在所讨论的每个元素上画一个小箭头来指出它的方向,例如:在这里插入图片描述

如果元素k的箭头指向一个相邻的较小元素,我们说它在这个以箭头标记的排列中是移动mobile)的。例如,对于排列3241来说,3和4是移动的,而2和1不是。通过使用移动元素这个概念,我们可以给出所谓的Johnson-Trotter算法的描述,它也是用来生成排列的。伪代码如下:
在这里插入图片描述

Java代码实现

package com.算法.减治法;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * @Author Lanh
 **/
public class 移动生成排列 {
    public static void main(String[] args) {
        List<String> list = johnsonTrotter(3);
        System.out.println(list);
    }

    public static List<String> johnsonTrotter(int n){
        List<String> list = new ArrayList<>();
        int[] direct = new int[n];
        int[] result = new int[n];
        for (int i=0;i<n;i++) direct[i] = -1;
        for (int i=0;i<n;i++) result[i] = i+1;
        int K_index = 0;
        list.add(Arrays.toString(result));
        while (K_index!=-1){
            K_index = -1;
            //求最大的移动元素k
            for (int j=0;j<n;j++){
                if (j+direct[j]>=0&&j+direct[j]<n&&result[j]>result[j+direct[j]]){
                    if (K_index==-1||result[j]>result[K_index]) K_index = j;
                }
            }

            if (K_index!=-1){
                //把k和它箭头指向的相邻元素互换
                int temp = result[K_index];
                int new_index = K_index+direct[K_index];
                result[K_index] = result[new_index];
                result[new_index] = temp;
                temp = direct[K_index];
                direct[K_index] = direct[new_index];
                direct[new_index] = temp;

                //调转所有大于k的元素的方向
                for (int j=0;j<n;j++){
                    if (result[j]>result[new_index]) direct[j] *= -1;
                }

                //将新排列添加到列表中
                list.add(Arrays.toString(result));
            }
        }
        return list;
    }
}

字典序生成排列

如何按照字典序生成 a 1 a 2 . . . a n a_1a_2...a_n a1a2...an后面的排列呢?如果 a n − 1 < a n a_{n-1}<a_n an1<an(肯定有一半序列是这样的情况),可以简单地调换最后这两个元素的位置。例如,123后面应该是132。如果 a n − 1 > a n a_{n-1}>a_n an1>an,则需要找到序列的最长递减后缀 a i + 1 > a i + 2 > . . . > a n ( 但 a i > a i + 1 ) a_{i+1}>a_{i+2}>...>a_n(但a_i>a_{i+1}) ai+1>ai+2>...>an(ai>ai+1);然后把 a i a_i ai与后缀中大于它的最小元素进行交换,以使 a i a_i ai增大;再将新后缀颠倒,使其变为递增排列。例如,经过这样的变换,362541后面会跟着364125。

伪代码

LexicographicPermute(n)
	//以字典序产生排列
	//输入:一个正整数n
	//输出:在字典序下{1,...,n}所有排列的列表
	初始化第一个排列123...n
	while 最后一个排列有两个连续升序的元素 do
		找出使得ai<a_i+1的最大的i  //a_i+1>a_i+2>...>an
		找到使得ai<aj的最大索引j   //j>=i+1,因为ai<a_i+1
		交换ai和aj   //a_i+1 a_i+2 ... an仍保持降序
		将a_i+1到an的元素反序
		将这个新排列添加到列表中

Java代码实现

package com.算法.减治法;

import java.util.*;

/**
 * @Author Lanh
 **/
public class 字典序生成排列 {
    public static void main(String[] args) {
        List<String> list = lexicographicPermute(5);

        System.out.println(list);
    }

    public static List<String> lexicographicPermute(int n){
        List<String> list = new ArrayList<>();
        int[] result = new int[n];
        for (int i=0;i<n;i++) result[i] = i+1;
        list.add(Arrays.toString(result));

        //标志最后一个排列有两个连续升序的元素
        boolean flat = true;
        while (flat){
            flat = false;
            int i = -1;
            int j = -1;

            //找出使得ai<a_i+1的最大的i  //a_i+1>a_i+2>...>an
            for (int k=0;k<n-1;k++) {
                if (result[k]<result[k+1]) {
                    i = k;
                    j = k+1;
                    flat = true;
                }
            }
            if (flat) {
                //找到使得ai<aj的最大索引j   //j>=i+1,因为ai<a_i+1
                for (int k=n-1;k>j;k--) {
                    if (result[i]<result[k]) {
                        j = k;
                        //break;  本来想break的,但是我循环条件设置了k>j,但是j=k了,注定不会再循环,所以我就不写break了
                    }
                }
                //交换ai和aj   //a_i+1 a_i+2 ... an仍保持降序
                int temp = result[i];
                result[i] = result[j];
                result[j] = temp;

                //将a_i+1到an的元素反序
                for (int k=1;k<(n-i+1)/2;k++) {
                    temp = result[i+k];
                    result[i+k] = result[n-k];
                    result[n-k] = temp;
                }
                //将这个新排列添加到列表中
                list.add(Arrays.toString(result));
            }
        }
        return list;
    }
}

测试结果

在这里插入图片描述

生成子集

我们研究过背包问题,它要求找出物品中最有价值的一个子集,并且能够装入一个承重量有限的背包中。当时讨论了求解该问题的穷举查找方法,它是以生成给定物品集合的所有子集为基础的。在本节中,我们来讨论一个算法,该算法能够生成一个抽象集合A= { a 1 . . . a n } \{a_1...a_n\} {a1...an}的所有 2 n 2^n 2n个子集。

减一思想也可以应用到这个问题中来。集合A= { a 1 . . . a n } \{a_1...a_n\} {a1...an}的所有子集可以分为两组:不包含 a n a_n an的子集和包含 a n a_n an的子集。前一组其实就是 { a 1 . . . a n − 1 } \{a_1...a_{n-1}\} {a1...an1}的所有子集,而后一组中的每一个元素都可以通过把 a n a_n an添加到 { a 1 . . . a n − 1 } \{a_1...a_{n-1}\} {a1...an1}的一个子集中来获得。因此,一旦我们得到了 { a 1 . . . a n − 1 } \{a_1...a_{n-1}\} {a1...an1}所有子集的列表,我们可以把列表中的每一个元素加上an,再把它们添加到列表中,以得到 { a 1 . . . a n } \{a_1...a_n\} {a1...an}的所有子集。下面给出了应用该算法来生成 { a 1 , a 2 , a 3 } \{a_1,a_2,a_3\} {a1,a2,a3}的所有子集的示例。
在这里插入图片描述
和生成排列相同,我们不必对较小集合生成幂集。有一个直接解决该问题的简便方法,它是基于这样一种关系:n个元素集合A= { a 1 . . . a n } \{a_1...a_n\} {a1...an}的所有 2 n 2^n 2n个子集和长度为n的所有 2 n 2^n 2n个位串之间的一一对应关系。建立这样一种对应关系的最简单方法是为每一个子集指定一个位串。如果 a i a_i ai属于该子集, b i b_i bi=1;如果 a i a_i ai不属于该子集, b i b_i bi=0。(这就是位向量的思想。)例如,位串000将对应于一个三元素集合的空子集,111将对应于该集合本身,也就是 { a 1 , a 2 , a 3 } \{a_1,a_2,a_3\} {a1,a2,a3},而110将表示 { a 1 , a 2 } \{a_1,a_2\} {a1,a2}。如果利用这种对应关系,我们可以产生从0到 2 n 2^n 2n-1的二进制数来生成长度为n的所有位串。顺便说一句,如果有必要,应在数字前添加相应个数的0。例如,对于n=3的情况,我们得到:
在这里插入图片描述
请注意,当该算法以字典序(在两个符号0和1构成的字母表中)生成位串时,子集的排列次序绝对是很不自然的。因此,我们有可能希望得到所谓的挤压序(squashed order),其中,所有包含a的子集必须紧排在所有包含 a 1 , . . . , a j − 1 ( j = 1 , . . . , n − 1 ) a_1,...,a_{j-1}(j=1,...,n-1) a1,...,aj1(j=1,...,n1)的子集后面,就像图4.10给出的三元素集合的子集列表。很容易就能对基于位串的算法进行一个调整,让它生成相关子集的挤压序
一个更有挑战性的问题是,是否存在一种生成位串的最小变化算法,使得每一个位串和它的直接前趋之间仅仅相差一位。(就子集来讲,我们希望每一个子集和它的直接前趋之间的区别,要么是增加了一个元素,要么是删除了一个元素,但两者不能同时发生。)该问题的答案为“是”。这是二进制反射格雷码的一个例子。对于n=3时,有:
在这里插入图片描述

伪代码

BRGC(n)
	//递归生成n位的二进制反射格雷码
	//输入:一个正整数n
	//输出:所有长度为n的格雷码位串列表
	if n=1,L包含位串0和位串1
	else 调用BRGC(n-l)生成长度为n-1的位串列表L1
		 把表L1倒序后复制给表L2
		 把0加到表L1中的每个位串前面
		 把1加到表L2中的每个位串前面
		 把表L2添加到表L1后面得到表L
	return L

Java代码实现

package com.算法.减治法;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * @Author Lanh
 **/
public class 二进制反射格雷码生成子集 {
    public static void main(String[] args) {
        List<String> list = BRGC(3);
        System.out.println(list);
    }

    public static List<String> BRGC(int n){
        if (n == 1) {
            List<String> list = new ArrayList<>();
            list.add("0");list.add("1");
            return list;
        }else {
            List<String> L1 = BRGC(n-1);
            List<String> L2 = new ArrayList<>(L1);
            Collections.reverse(L2);
            for (int i=0;i<L1.size();i++){
                L1.set(i,"0"+L1.get(i));
            }
            for (int i=0;i<L2.size();i++){
                L2.set(i,"1"+L2.get(i));
            }
            L1.addAll(L2);
            return L1;
        }
    }
}

结果

在这里插入图片描述

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

绿豆蛙给生活加点甜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值