如何获得N组合R中的第k个项

时间:2018-08-14 16:27:35

标签: java math combinations permutation combinatorics

如何在kth中获得NCR组合。而无需遍历所有可能的结果。例如说我有3C2个职位32个相同项目。我知道它是[011][101][110]。我如何获得例如第二项( k = 1 )是[101]使用的一种方法?

约束({R < N k >= 0k < P,其中P = NCR)。

  

NB: [101]是第二项(按升序/字典顺序),因为011 = 3 ,101 = 5,110 = 6   以十进制表示。因此,基本上目标是获得 NCR 中的 k 个数字,   因为 NCR 的每个 kth 输出都可以表示为一个数字。

3 个答案:

答案 0 :(得分:2)

是的,您说的没错:

  

因为从NCR输出的每kth都可以表示为一个数字。

从整数1 to # of combs/perms组到整个梳/烫发组有双射。查找特定梳/烫发的特定索引有时被称为获得排名。根据问题中的示例,这些是普通排列。此外,当您提到 升序 时,您指的是lexicographical order

在计数中获得给定集合的 n th 个普通排列是一项直接的练习。我们首先需要使用完善的公式来获得排列总数:

P(n, r) = n! / (n - r)!

下一部分是关键观察,它使我们能够快速获得目标排列的每个元素。

  

如果我们查看 n 组的所有排列,选择 r ,那么会有 n 个仅因排列而不同的组 n 个元素。

例如,如果我们查看[0 1 2 3] choose 3第一两组排列,我们有:

      [,0] [,1] [,2]
 [0,]    0    1    2
 [1,]    0    1    3
 [2,]    0    2    1
 [3,]    0    2    3
 [4,]    0    3    1
 [5,]    0    3    2
 [6,]    1    0    2
 [7,]    1    0    3
 [8,]    1    2    0
 [9,]    1    2    3
[10,]    1    3    0
[11,]    1    3    2

请注意,最后的排列只是集合[1 0 2 3]的前6个排列。也就是说,将0映射为1,将1映射为0,将最后2个元素映射为自身。 / p>

此模式将继续,因为我们只向右移动而不是 n 个相同的组,所以第二列 n将获得 n-1 个相似的组-2 表示第三,依此类推。

因此,要确定排列的第一个元素,我们需要确定 1 st 组。我们可以通过简单地将排列数量除以 n 来实现。对于上面的排列4的示例,选择3,如果我们要寻找 15 th 排列,则第一个元素具有以下内容:

Possible indices : [0 1 2 3]
P(4, 3) = 24
24 / 4 = 6 (elements per group)
15 / 6 = 2 (integer division) 2 means the 3rd element here (base zero)

现在我们已经使用了 3 rd 元素,我们需要将其从可能的索引数组中删除。我们如何获得下一个元素?

  

容易,我们通过从原始索引中减去刚找到的组的乘积和每个组中的元素的乘积来获得下一个子索引

Possible indices : [0 1 3]
Next index is 15 - 6 * 2 = 3

现在,我们只需重复一次,直到我们填满所有条目:

Possible indices : [0 1 3]
Second element
6 / 3 = 2 (elements per group)
3 / 2 = 1
Next index is 3 - 3 * 1 = 0

Possible indices : [0 3]
Third element
2 / 2 = 1
0 / 1 = 0

所以我们的 15 th 元素是:[2 1 0]

这里是一个C++的实现,应该很容易转换为Java

double NumPermsNoRep(int n, int k) {
    double result = 1;
    double i, m = n - k;

    for (i = n; i > m; --i)
        result *= i;

    return result;
}

std::vector<int> nthPermutation(int n, int r, double myIndex) {
    int j = 0, n1 = n;
    double temp, index1 = myIndex;
    std::vector<int> res(r);

    temp = NumPermsNoRep(n, r);
    std::vector<int> indexVec(n);
    std::iota(indexVec.begin(), indexVec.end(), 0);

    for (int k = 0; k < r; ++k, --n1) {
        temp /= n1;
        j = (int) std::trunc(index1 / temp);
        res[k] = indexVec[j];
        index1 -= (temp * (double) j);
        indexVec.erase(indexVec.begin() + j);
    }
}

这些概念扩展到其他类型的组合问题,例如找到 n th 组合或重复排列等。

答案 1 :(得分:1)

时间复杂度为O(kn),空间为O(n)

public static void main(String[] args) {
    //n = 4, r = 2, k = 3
    int[] ret1 = getKthPermutation(4, 2, 3);
    //ret1 is [1,0,0,1]

    //n = 3, r = 2, k = 1
    int[] ret2 = getKthPermutation(3, 2, 1);
    //ret2 is [1,0,1]
}

static int[] getKthPermutation(int n, int r, int k) {
    int[] array = new int[n];
    setLastN(array, r, 1);

    int lastIndex = n - 1;
    for(int count = 0; count < k; count++) {

        int indexOfLastOne = findIndexOfLast(array, lastIndex, 1);
        int indexOfLastZero = findIndexOfLast(array, indexOfLastOne, 0);
        array[indexOfLastOne] = 0;
        array[indexOfLastZero] = 1;

        //shortcut: swap the part after indexOfLastZero to keep them sorted
        int h = indexOfLastZero + 1;
        int e = lastIndex;
        while(h < e) {
            int temp = array[h];
            array[h] = array[e];
            array[e] = temp;
            h++;
            e--;
        }

    }

    return array;
}

//starting from `from`, and traveling the array forward, find the first `value` and return its index.
static int findIndexOfLast(int[] array, int from, int value) {
    for(int i = from; i > -1; i--)
        if(array[i] == value) return i;
    return -1;
}

//set the last n elements of an array to `value`
static void setLastN(int[] array, int n, int value){
    for(int i = 0, l = array.length - 1; i < n; i++)
        array[l - i] = value;
}

这是对非常典型的“查找第k个置换”算法的改编。

我将尝试解释总体思路(您的情况很特殊,因为只有两种类型的元素:0和1)。 可以说我有[2,1,6,4,7,5]。大于当前的下一个最小排列是什么?为什么我要关注比当前更大的下一个最小排列?因为如果您从最小排列[1,2,4,5,6,7]开始并重复动作(找到比当前值大的最小排列)k次,那么您将发现第k + 1个最小排列。

现在,由于我要查找的那个需要大于当前的那个,因此我需要增加当前的那个。为了使增量尽可能小,我将尝试修改5(最后一个)。现在,我不能只将5更改为随机值,只能将它替换为一个数字。

如果我在5之前交换一个较大的数字,比如说7,那么我将得到[2,1,6,4,5,7],它比当前数字小。现在显然我需要将5换成一个较小的数字,但是哪一个呢?如果将5与2交换,我得到[5,1,6,4,7,2],则此增量太大。我需要将“ 5”换成“低位数字”,以使增量尽可能小。那使我们找到了小于5的第一个(最低)数字(从右到左)。在这种情况下,我需要将5与4交换并得到[2,1,6,5,7,4]。这样,我可以减小“交换”的影响。现在,前缀确定为[2,1,6,5。没有更小的前缀。我们需要处理后缀7,4]。显然,如果我们对后缀进行排序并将其设为4,7],就可以完成。

在我们的例子中,有两个区别: 1.我们需要交换最后一个1,因为您不能通过将零与前面的任何数字交换来使排列变大。 2.我们总是可以使用代码中所示的快捷方式对后缀进行排序。我会把它留给你:)

答案 2 :(得分:1)

$ yarn config set proxy http://my_company_proxy_url:port
$ yarn config set https-proxy http://localhost:3128

example $ yarn config set https-proxy http://proxy.abc.com:8080

您可以将public static String lexicographicPermutation(String str, long n) { final long[] factorials = { 1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800, 39916800, 479001600 }; n--; char[] arr = str.toCharArray(); for (int i = 0; i < arr.length - 1; i++) { long fact = factorials[arr.length - i - 2]; long p = i + n / fact; n %= fact; for (int j = i + 1; j <= p; j++) swap(arr, i, j); } return new String(arr); } private static void swap(char[] arr, int i, int j) { char tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; } 替换为所需的字符串。在给定的示例中, 1st 排列是STR(这是一个包含13个字符的字符串), 13!st 排列是反向字符串"abcdefghijklm"第100 个排列是"mlkjihgfedcba"

要实现此解决方案,只需使用Google Factorial number system。这是解决此问题的。这是Project Euler: Problem 24

演示:

"abcfklgmeihjd"