每日随机高尔夫#3:整数分区


19

关于系列

首先,您可以像对待其他任何代码高尔夫挑战赛一样对待它,并回答它而不必担心系列赛。但是,在所有挑战中都有排行榜。您可以在第一篇文章中找到排行榜以及有关该系列的更多信息。

尽管我在本系列中有很多想法,但未来的挑战还没有定下来。如果您有任何建议,请在相关的沙箱帖子上让我知道。

漏洞3:整数分区

时间增加了一点难度。

分区正整数n被定义为正整数,其总和的多集n。例如,如果n = 5存在,则存在以下分区:

{1,1,1,1,1}
{2,1,1,1}
{2,2,1}
{3,1,1}
{3,2}
{4,1}
{5}

请注意,这些是多集,因此它们没有顺序{3,1,1}{1,3,1}并且{1,1,3}都被认为是相同的。

给您的任务是n生成的随机分区n。以下是详细规则:

  • 产生的隔板的分布必须均匀。也就是说,在上面的示例中,应该以1/7的概率返回每个分区。

    当然,由于PRNG的技术局限性,不可能实现完美的均匀性。为了评估您提交的文件的均匀性,以下操作将被视为产生完全均匀的分布:

    • 从PRNG(在任何范围内)获取一个数字,该数字被证明是(近似)统一的。
    • 通过取模或乘法(或一些其他将值均匀分配的运算)将较大的一组数字的均匀分布映射到较小的一组。较大的集合必须包含至少为较小集合的1024倍的可能值。
  • 由于分区是多集,因此您可以按任何顺序返回它们,并且此顺序不必一致。但是,出于随机分布的目的,顺序被忽略。也就是说,在上面的示例中{3,1,1}{1,3,1}{1,1,3} 一起返回的概率必须为1/7。

  • 您的算法必须具有确定性的运行时。特别是,您不能生成随机多集,如果它们的总和不等于,则拒绝它们n
  • 您算法的时间复杂度必须是中的多项式n。特别是,您不能简单地生成所有分区并选择一个随机分区(因为分区的数量与呈指数增长n)。您可能会假设您使用的PRNG可以返回每个值O(1)中的均匀分布值。
  • 您不得使用任何内置函数来解决此任务。

您可以编写完整的程序或函数,并通过STDIN或最接近的替代方案,命令行参数或函数自变量接受输入,并通过返回值或打印到STDOUT(或最接近的替代方案)产生输出。

您可以假定n ≤ 65(这样分区的数目小于2 21)。输出可以采用任何方便,明确的列表或字符串格式。

如果您提交功能,请考虑还提供一些测试程序,该程序会多次调用该功能并打印结果。如果必须在代码中调整参数,则可以。这样做是为了使人们可以检查解决方案至少近似均匀。

这是代码高尔夫球,因此最短的提交(以字节为单位)获胜。当然,每位用户最短的提交时间也将进入该系列的整体排行榜。

排行榜

该系列的第一篇文章将产生一个排行榜。

为确保您的答案显示出来,请使用以下Markdown模板以标题开头每个答案:

# Language Name, N bytes

N您提交的文件大小在哪里。如果您提高了分数,则可以通过打败旧分数保持标题。例如:

# Ruby, <s>104</s> <s>101</s> 96 bytes

(目前未显示该语言,但是该代码段确实需要并对其进行解析,并且将来我可能会添加一个语言排行榜。)

Answers:


8

Python 2,179字节

from random import*
m=r=input();i=q=r+1;h=[1]+[0]*q*q;exec"h[i]=h[i+~q]+h[i-i%q*q];i+=1;"*r*q
while r:
 x=random()*sum(h[r*q:r*q-~m]);m=0
 while x>0:m+=1;x-=h[r*q+m]
 print m;r-=m

我从此Knuth提取物中使用了公式(39),公式给出了n具有确切m部分的分区数。这恰好等于的分区数量nm作为最大的元素,这是我使用的解释。分区的元素从最大到最小生成。在每个阶段,公式都将与当前剩余n元素和最大允许元素一起重用。


5

Dyalog APL,67 59 51字节

p←{⍵,⊂1,⍨+/¨⌽⍵↑¨⍨⌽⍳⍴⍵}⍣⎕⊢⍬⋄f←{⍵=0:⍬⋄a,a∇⍵-a←{1++/(?+/⍵)>+\⍵}⍺↑⍵⊃p}⍨ (67字节)

p是矢量的矢量,其中p[n][k]是的分区的数量nk被加数,或者等同地:分区具有最大被加数的数目k。我们p从空向量开始,进行读取n读取输入)并重复应用以下内容:

{⍵,⊂1,⍨+/¨⌽⍵↑¨⍨⌽⍳⍴⍵}
                 ⍴⍵   ⍝ the current length, initially 0
                ⍳⍴⍵   ⍝ 1 2 ... length
               ⌽⍳⍴⍵   ⍝ length ... 2 1
           ⍵↑¨⍨       ⍝ take length elements from p[1], length-1 from p[2], etc
                      ⍝ padded with 0-s, e.g. if p was (,1)(1 1)(1 1 1)(1 2 1 1)(1 2 2 1 1):
                      ⍝ we get:     (1 0 0 0 0)(1 1 0 0)(1 1 1)(1 2)(,1)
          ⌽           ⍝ reverse it: (,1)(1 2)(1 1 1)(1 1 0 0)(1 0 0 0 0)
       +/¨            ⍝ sum each:   1 3 3 2 1
    1,⍨               ⍝ append 1:   1 3 3 2 1 1
 ⍵,⊂                  ⍝ append the above to the vector of vectors

之后n应用程序(⍣⎕),我们已经建立p

f选择一个随机分区。 n f k最多 k求数的随机分区。 f nn f n

{⍵=0:⍬⋄a,a∇⍵-a←{1++/(?+/⍵)>+\⍵}⍺↑⍵⊃p}⍨
                                     ⍨ ⍝ "selfie" -- use n as k if no k is provided
 ⍵=0:⍬                                 ⍝ if n=0 return empty
                                 ⍵⊃p   ⍝ pick the n-th element of p
                               ⍺↑      ⍝ take k elements from that
               {1++/(?+/⍵)>+\⍵}        ⍝ use them as weights to pick a random number 1...k
               {           +\⍵}        ⍝   partial sums of weights
               {    (?+/⍵)    }        ⍝   a random number 1...sum of weights
               {    (?+/⍵)>+\⍵}        ⍝   which partial sums is it greater than?
               {  +/          }        ⍝   count how many "greater than"-s
               {1+            }        ⍝   we're off by one
             a←                        ⍝ this will be the greatest number in our partition
         a∇⍵-a                         ⍝ recur with n1=n-a and k1=a
       a,                              ⍝ prepend a

一些改进:

  • 内联p的代价是性能稍差(但仍然足够好)

  • p重新排列1,保存字符的计算中

  • 变成前面{1++/(?+/⍵)>+\⍵}的火车1+1+(+/(?+/)>+\)

  • f一个匿名函数并提供(评估输入)作为参数以获得完整的程序

{⍵=0:⍬⋄a,a∇⍵-a←1+(+/(?+/)>+\)⍺↑⍵⊃{⍵,⊂⌽1,+/¨⍵↑¨⍨⌽⍳⍴⍵}⍣⍵⊢⍬}⍨⎕ (59字节)

用n = 5进行测试

用n = 65进行测试

以下链接运行n = 5 000次,并收集有关每个分区频率的统计信息: ⎕rl←0 ⋄ {⍺,⍴⍵}⌸ {⍵=0:⍬⋄a,a∇⍵-a←1+(+/(?+/)>+\)⍺↑⍵⊃{⍵,⊂⌽1,+/¨⍵↑¨⍨⌽⍳⍴⍵}⍣⍵⊢⍬}⍨ ¨10000⍴5


Roger Roger的帮助下,还有更多改进:

  • 替换{⍵=0:A⋄B}{×⍵:B⋄A}。Signum(×⍵)的true返回⍵>0false ⍵=0

  • 替换(+/(?+/)>+\)+/b<?⊃⌽b←+\,它保存一个字符

  • 使用一个矩阵,而不是矢量的矢量来计算p:更换⍵⊃{⍵,⊂⌽1,+/¨⍵↑¨⍨⌽⍳⍴⍵}⍣⍵⊢⍬⊃↓(0,⍨⊢⍪⍨1 1⍉+\)⍣⍵⍪1

{×⍵:a,a∇⍵-a←1++/b<?⊃⌽b←+\⍺↑⊃↓(0,⍨⊢⍪⍨1 1⍉+\)⍣⍵⍪1⋄⍬}⍨ (51字节)

测试n = 5 ; 测试n = 65 ; 频率统计


2
一个人如何获得许冠杰的帮助?
FUZxxl 2015年

5
编写一个玩具APL口译员,让自己和他一起被录用。摆出上述表情作为挑战,承诺为他带出的每个角色都喝一品脱啤酒。然后获利:更少的角色和更多的饮酒,因为他不喝啤酒。
NGN

1
我知道了。那是一个很巧妙的策略,让我们看看我是否可以重现一下……您能问他Dyalog APL是否很快就会得到像J一样的东西u/\. y
FUZxxl 2015年


谢谢你问他。现在,我想知道是否也可以在线性时间内进行。
FUZxxl 2015年

4

GolfScript,90个字节

~[[[1.]]]\({..[[{{(\{)}%+}%1$,1$,-=}%[1,]@0=+{1+}%]zip{{(\.,/*~}%.,.rand@=+}:^%]\+}*0=^(;`

在线演示

这是我(更简单)的分区计数代码的改编它不仅跟踪计数,而且还跟踪计数和一个均匀选择的计数元素。

两者的并排比较:

~[[[1.]]]\({..[[{{(\{)}%+}%1$,1$,-=}%[1,]@0=+{1+}%]zip{{(\.,/*~}%.,.rand@=+}:^%]\+}*0=^(;`
 [[ 1  ]]\({..[[{          1$,1$,-=}%  0 @0=+     ]zip{{+}*                }:^%]\+}*0=^

差异:

  • 最初~是因为这是程序而不是代码段。
  • [1.]更换1对应于什么样的变化被跟踪。
  • 所述附加{(\{)}%+}%增量在分区中的每个元件,并且{1+}%增加了1到分区中。
  • 0成为跟踪的更改的一部分[0](但是1,),但是由于在添加到另一个数组之前需要保留一个数组,因此需要额外的数组[ ]
  • 简单的总和{+}*将成为分区的加权选择,再加上其计数的总和。
  • (;`去除输出并提出分割成一个漂亮的格式计数。

测试框架

;7000,{;
  '5'

  ~[[[1.]]]\({..[[{{(\{)}%+}%1$,1$,-=}%[1,]@0=+{1+}%]zip{{(\.,/*~}%.,.rand@=+}:^%]\+}*0=^(;`

}%
:RESULTS
.&${
  RESULTS.[2$]--,' '\n
}/

如果要运行其他数量的试验,请调整初始7000。请注意,这对于在线演示来说太慢了。


3

Java中,285个 267字节

int[][]p;void p(int n){p=new int[n+1][n+1];int a=n,b=k(n,a),c,d;for(b*=Math.random();n>0;System.out.print(c+" "),n-=a=c)for(c=0;c++<(a<n?a:n)&b>=(d=k(n-c,c));b-=d);}int k(int n,int k){if(p[n][k]<1)for(int a=0,b=0;b<k&b++<n;p[n][k]=a)a+=k(n-b,b);return n>0?p[n][k]:1;}

这是与TheBestOne的答案相同的方法,但是它使用简单的数组而不是地图。此外,List与其将随机分区作为返回,还不如将其打印到控制台。

下面是一个运行100000次的测试程序。对于该示例n=5,所有组合在我上次运行的完美1/7的0.64%之内。

public class Partition {
    public static void main(String[] args) {
        Partition p = new Partition();
        for(int i=0;i<100000;i++){
            p.p(5);
            System.out.println();
        }
    }

    int[][]p;

    void p(int n){
        p=new int[n+1][n+1];
        int a=n,b=k(n,a),c,d;
        for(b*=Math.random();n>0;System.out.print(c+" "),n-=a=c)
            for(c=0;c++<(a<n?a:n)&b>=(d=k(n-c,c));b-=d);
    }

    int k(int n,int k){
        if(p[n][k]<1)
            for(int a=0,b=0;b<k&b++<n;p[n][k]=a)
                a+=k(n-b,b);
        return n>0?p[n][k]:1;
    }

}

3
尽管您已将Math.min电话打到了(k<n?k:n),但您可以将其完全抛弃并进行两次检查,以走得更远b<k&b++<n。还可以方便地沟n>0循环条件的一部分(因为n>0&b<n减少了b<nb保证非负)。
彼得·泰勒

@PeterTaylor谢谢。换个角度看,让我摆脱多余的return语句和单独的int声明。
Geobits,2015年

3

CJam,64 56字节

ri_L{_0>{\,f{)_@1$-j+}{)@)2$+:Umr@<@@?U+}*}{!a\;}?}2j);p

您可以使用以下脚本对其进行测试:

ria100*{_L{_0>{\,f{)_@1$-j+}{)@)2$+:Umr@<@@?U+}*}{!a\;}?}2j);}%__|\f{_,\2$a-,-}2/p

说明

ri_                  " Read an integer and duplicate. ";
L{                   " Create a memoized function of the maximum and the sum, which returns
                       a random partition, and the total number of partitions as the last item. ";
    _0>              " If sum > 0: ";
    {
        \,f{         " For I in 0..max-1: ";
            )_@1$-   " Stack: I+1 I+1 sum-I-1 ";
            j+       " Recursively call with the two parameters, and prepend I+1. ";
        }
        {            " Reduce on the results: ";
            )@)2$+   " Stack: partition1 total1 partition2 total1+total2 ";
            :Umr     " U = total1+total2, then generate a random number smaller than that. ";
            @<@@?    " If it is <total1, choose partition1, else choose partition2. ";
            U+       " Append the total back to the array. ";
        }*
    }
    {!a\;}?          " Else return [0] if negative, or [1] if zero. ";
}2j
);p                  " Discard the total and print. ";

2
您应该删除答案中不正确的“打得不好”部分;)
anatolyg

@anatolyg已删除。但是我相信仍然可以删除一些字节。我实在太懒了。
jimmy23013 2015年

3

Pyth,64个字节

使用/programming//a/2163753/4230423,除了a)由于Pyth自动记忆,没有缓存,b)打印每个而不是追加到列表,并且c)转换为Pyth。

M?smg-Gddr1hhS,GHG1Akd,QOgQQWQFNr1hhS,QkKg-QNNI<dKB-=dK)N=kN-=QN

有空的时候,我会发布对此的解释,但这是相应的python代码:

g=lambda G,H: sum(map(lambda d:g(G-d, d), range(1, (H if H<G else G) + 1))) if G else 1
Q=input()
k,d = Q,random.randrange(g(Q, Q))
while Q:
    for N in range(1, min(k, Q) + 1):
        K = g(Q-N, N)
        if d < K:
            break
        d -= K
    print N
    k=N
    Q -= N

编辑:我终于绕过去做解释:

M                Lambda g(G,H)
 ?         G     If G truthy
  s              Sum
   m             Map
    g            Recursive call
     -Gdd        G-d,d
    r            Range
     1           1 to
     h           +1
      hS         First element of sorted (does min)
       ,GH       From G and H
   1             Else 1
A                Double assign
 kd              Vars k and d
 ,               To vals
  Q              Q (evaled input)
  O              Randrange 0 till val
   gQQ           Call g(Q, Q)
WQ               While Q is truthy
 FN              For N in
  r              Range
   1             From one
   h             Till +1
    hS,QK        Min(Q,K)
  Kg             K=g(
   -QN           Q-N
   N             N
  I<dK           If d<k
   B             Break (implicit close paren)
  -=dk           Subtracts d-=k
 )               Close out for loop
 N               Prints N
 =kN             Set k=N
 -=QN            Subtracts Q-=N

2

八度,200

function r=c(m)r=[];a=eye(m);a(:,1)=1;for(i=3:m)for(j=2:i-1)a(i,j)=a(i-1,j-1)+a(i-j,j);end;end;p=randi(sum(a(m,:)));while(m>0)b=a(m,:);c=cumsum(b);x=min(find(c>=p));r=[r x];p=p-c(x)+b(x);m=m-x;end;end

取消高尔夫:

function r=c(m)
  r=[];
  a=eye(m);
  a(:,1)=1;
  for(i=3:m)
    for(j=2:i-1)
      a(i,j)=a(i-1,j-1)+a(i-j,j);
    end;
  end;
  p=randi(sum(a(m,:)));
  while(m>0)
    b=a(m,:);
    c=cumsum(b);
    x=min(find(cumsum(b)>=p));
    r=[r x];
    p=p-c(x)+b(x);
    m=m-x;
  end
end

根据这样引用的Knuth提取@feersum,构造一个方阵,其中每个单元格(m,n)反映m最大数目为的分区数n。例如,5,2给我们2是因为有两个有效的分区2,2,12,1,1,16,3给我们3个3,1,1,13,2,13,3

现在,我们可以确定地找到第p个分区。在这里,我们生成的p是一个随机数,但是您可以稍微更改脚本,因此p可以使用一个参数:

function r=c(m,p)
  r=[];
  a=eye(m);
  a(:,1)=1;
  for(i=3:m)
    for(j=2:i-1)
      a(i,j)=a(i-1,j-1)+a(i-j,j);
    end;
  end;
  while(m>0)
    b=a(m,1:m);
    c=cumsum(b);
    x=min(find(c>=p));
    r=[r x];
    p=p-c(x)+b(x);
    m=m-x;
  end
end

现在,我们可以确定性地表明每个结果仅取决于p:

octave:99> for(i=1:7)
> c(5,i)
> end
ans =

   1   1   1   1   1

ans =

   2   1   1   1

ans =

   2   2   1

ans =

   3   1   1

ans =

   3   2

ans =

   4   1

ans =  5

因此,回到随机产生p的原始位置,我们可以确保每个结果均具有相同的可能性。


我不确定您的5,2示例。两个分区都不应该是(2,2,1)(2,1,1,1,1)(因为您列出的两个分区的数字都大于2)。
马丁·恩德

你是对的,我搞砸了。有两个具有两个组件的分区,以及两个以开头的分区2。我的意思是后者。
dcsohl

2

R,198个字节

function(m){r=c();a=diag(m);a[,1]=1;for(i in 3:m)for(j in 2:(i-1))a[i,j]=a[i-1,j-1]+a[i-j,j];p=sample(sum(a[m,]),1);while(m>0){b=a[m,];c=cumsum(b);x=min(which(c>=p));r=c(r,x);p=p-c[x]+b[x];m=m-x};r}

取消高尔夫:

f <- function(m) {
    r <- c()
    a <- diag(m)
    a[, 1] <- 1
    for (i in 3:m)
        for (j in 2:(i-1))
            a[i, j] <- a[i-1, j-1] + a[i-j, j]
    p <- sample(sum(a[m, ]), 1)
    while (m > 0) {
        b <- a[m, ]
        c <- cumsum(b)
        x <- min(which(c >= p))
        r <- c(r, x)
        p <- p - c[x] + b[x]
        m <- m - x
    }
    return(r)
}

它遵循与@dcsohl 在Octave中出色解决方案相同的结构,因此也基于 Knuth提取 @feersum发布。

如果可以在R中提出更具创造性的解决方案,我将在稍后进行编辑。与此同时,当然欢迎任何输入。


1

Java,392字节

import java.util.*;Map a=new HashMap();List a(int b){List c=new ArrayList();int d=b,e=b(b,d),f=(int)(Math.random()*e),g,i;while(b>0){for(g=0;g++<Math.min(d, b);f-=i){i=b(b-g,g);if(f<i)break;}c.add(g);d=g;b-=g;}return c;}int b(int b,int c){if(b<1)return 1;List d=Arrays.asList(b,c);if(a.containsKey(d))return(int)a.get(d);int e,f;for(e=f=0;f++<Math.min(c, b);)e+=b(b-f,f);a.put(d,e);return e;}

致电 a(n)。返回一个ListInteger小号

缩进:

import java.util.*;

Map a=new HashMap();

List a(int b){
    List c=new ArrayList();
    int d=b,e=b(b,d),f=(int)(Math.random()*e),g,i;
    while(b>0){
        for(g=0;g++<Math.min(d, b);f-=i){
            i=b(b-g,g);
            if(f<i)
                break;
        }
        c.add(g);
        d=g;
        b-=g;
    }
    return c;
}

int b(int b,int c){
    if(b<1)
        return 1;
    List d=Arrays.asList(b,c);
    if(a.containsKey(d))
        return(int)a.get(d);
    int e,f;
    for(e=f=0;f++<Math.min(c, b);)
        e+=b(b-f,f);
    a.put(d,e);
    return e;
}

改编自 /programming//a/2163753/4230423并打高尔夫球

工作原理:我们可以计算O(n 2)时间中整数n的分区数。副作用是,这将生成一个大小为O(n 2)的表,然后我们可以使用该表为任意整数k生成n的k个分区,在O( Ñ)时间内。

因此,让total =分区数。从0到总数中选择一个随机数k -1.生成k个分区。

和往常一样,欢迎提出建议:)


1

Python 2,173字节

from random import*
N,M=input__
R=67;d=[(0,[])]*R*R
for k in range(R*R):p,P=d[k+~R];q,Q=d[k-k%R*R];d[k]=p+q+0**k,[[x+1 for x in Q],[1]+P][random()*(p+q)<p]
print d[N*R+M][1]

递归地创建一个字典d,其中的键通过(使用保证)k代表一对。条目是划分成多个部分的元组,以及一个随机的这样的划分。计数是通过递归公式计算的(感谢feersum指出了这一点)(n,m)k=67*n+mn<=65nm

f(n,m) = f(n-1,m-1) + f(n,n-m)

随机分区通过选择两个分支之一来更新,其概率与计数成正比。更新通过添加完成,通过添加1在第一个分支上,在第二个分支上增加每个元素。

我很难获得0的越界值mn给出零计数。最初,我使用了默认为0且空列表的字典。在这里,我使用一个列表,并使用此默认条目填充它。负索引会导致从列表的末尾读取列表,这给出了一个默认条目,该字符串几乎没有到达末尾,而环绕仅触及以下区域m>n


1

80386机器代码,105字节

代码的十六进制转储:

60 8b fa 81 ec 00 41 00 00 33 c0 8b f4 33 d2 42
89 14 06 42 33 ed 8b d8 03 2c 1e 2a fa 73 f9 83
c6 04 89 2c 06 42 3b d1 76 ea fe c4 3a e1 76 db
33 d2 0f c7 f0 f7 f5 86 e9 85 d2 74 1b 33 c0 8d
34 0c 39 14 86 77 03 40 eb f8 2b 54 86 fc 40 89
07 83 c7 04 2a e8 77 e1 42 89 17 83 c7 04 fe cd
7f f7 4a b6 41 03 e2 61 c3

作为C函数: void random_partition(int n, int result[]);。它以提供的缓冲区中的数字列表形式返回结果;它不会以任何方式标记列表的结尾,但是用户可以通过累积数字来发现结尾-当总和等于时,列表结束n

使用方法(在Visual Studio中):

#include <stdio.h>

__declspec(naked) void __fastcall random_partiton(int n, int result[])
{
#define a(byte) __asm _emit 0x ## byte
a(60) a(8b) a(fa) a(81) a(ec) a(00) a(41) a(00) a(00) a(33) a(c0) a(8b) a(f4) a(33) a(d2) a(42)
a(89) a(14) a(06) a(42) a(33) a(ed) a(8b) a(d8) a(03) a(2c) a(1e) a(2a) a(fa) a(73) a(f9) a(83)
a(c6) a(04) a(89) a(2c) a(06) a(42) a(3b) a(d1) a(76) a(ea) a(fe) a(c4) a(3a) a(e1) a(76) a(db)
a(33) a(d2) a(0f) a(c7) a(f0) a(f7) a(f5) a(86) a(e9) a(85) a(d2) a(74) a(1b) a(33) a(c0) a(8d)
a(34) a(0c) a(39) a(14) a(86) a(77) a(03) a(40) a(eb) a(f8) a(2b) a(54) a(86) a(fc) a(40) a(89)
a(07) a(83) a(c7) a(04) a(2a) a(e8) a(77) a(e1) a(42) a(89) a(17) a(83) a(c7) a(04) a(fe) a(cd)
a(7f) a(f7) a(4a) a(b6) a(41) a(03) a(e2) a(61) a(c3)
}

void make_stack() // see explanations about stack below
{
    volatile int temp[65 * 64];
    temp[0] = 999;
}

int main()
{
    int result[100], j = 0, n = 64, counter = n;
    make_stack(); // see explanations about stack below

    random_partiton(n, result);

    while (counter > 0)
    {
        printf("%d ", result[j]);
        counter -= result[j];
        ++j;
    }
    putchar('\n');
}

输出示例(n = 64):

21 7 4 4 3 3 3 3 2 2 2 2 2 1 1 1 1 1 1 1

这需要很多解释...

当然,我使用了其他所有人也都使用过的算法;对于复杂性没有任何选择。因此,我不必过多解释该算法。无论如何:

我用不大于f(n, m)n部分来表示元素的划分数量m。我将它们存储在一个二维数组(在C中声明为f[65][64])中,第一个索引为n,第二个为m-1。我决定支持n=65太麻烦了,于是放弃了……

这是计算此表的C代码:

#define MAX_M 64
int f[(MAX_M + 1) * MAX_M];
int* f2;
int c; // accumulates the numbers needed to calculate f(n, m)
int m;
int k; // f(k, m), for various values of k, are accumulated
int n1;

for (n1 = 0; n1 <= n; ++n1)
{
    f2 = f;
    f2[n1 * MAX_M] = 1;
    for (m = 2; m <= n; ++m)
    {
        c = 0;
        k = n1;
        while (k >= 0)
        {
            c += f2[k * MAX_M];
            k -= m;
        }
        ++f2;
        f2[n1 * MAX_M] = c;
    }
}

该代码具有一些混淆的样式,因此可以轻松地将其转换为汇编语言。它最多计算元素f(n, n),即n元素的分区数。该代码完成后,临时变量将c包含所需的数字,该数字可用于选择随机分区:

int index = rand() % c;

稍后,index使用生成的表将其转换为所需的格式(数字列表)。

do {
    if (index == 0)
        break;

    m = 0;
    f2 = &f[n * MAX_M];
    while (f2[m] <= index)
    {
        ++m;
    }

    index -= f2[m-1];
    ++m;
    *result++ = m;
    n -= m;
} while (n > 0);

do {
    *result++ = 1;
    --n;
} while (n > 0);

该代码也经过了优化,可转换为汇编语言。有一个小“ bug”:如果分区1末尾不包含任何数字,则最后一个循环遇到n = 0,并输出不需要的1元素。但这没有什么坏处,因为打印代码会跟踪该数字的总和,并且不会打印此无关的数字。

当转换为内联汇编时,此代码如下所示:

__declspec(naked) void _fastcall random_partition_asm(int n, int result[])
{
    _asm {
        pushad;

        // ecx = n
        // edx = m
        // bh = k; ebx = k * MAX_M * sizeof(int)
        // ah = n1; eax = n1 * MAX_M * sizeof(int)
        // esp = f
        // ebp = c
        // esi = f2
        // edi = result

        mov edi, edx;
        sub esp, (MAX_M + 1) * MAX_M * 4; // allocate space for table
        xor eax, eax;
    row_loop:
        mov esi, esp;
        xor edx, edx;
        inc edx;
        mov dword ptr [esi + eax], edx;
        inc edx;

    col_loop:
        xor ebp, ebp;
        mov ebx, eax;

    sum_loop:
        add ebp, [esi + ebx];
        sub bh, dl;
        jae sum_loop;

        add esi, 4;
        mov [esi + eax], ebp;
        inc edx;
        cmp edx, ecx;
        jbe col_loop;

        inc ah;
        cmp ah, cl;
        jbe row_loop;

        // Done calculating the table

        // ch = n; ecx = n * MAX_M * sizeof(int)
        // eax = m
        // ebx = 
        // edx = index
        // esp = f
        // esi = f2
        // ebp = c
        // edi = result

        xor edx, edx;
        rdrand eax; // generate a random number
        div ebp; // generate a random index in the needed range
        xchg ch, cl; // multiply by 256

    n_loop:
        test edx, edx;
        jz out_trailing;
        xor eax, eax;
        lea esi, [esp + ecx];

    m_loop:
        cmp [esi + eax * 4], edx;
        ja m_loop_done;
        inc eax;
        jmp m_loop;
    m_loop_done:

        sub edx, [esi + eax * 4 - 4];
        inc eax;
        mov [edi], eax;
        add edi, 4;
        sub ch, al;
        ja n_loop;

    out_trailing:
        inc edx;
    out_trailing_loop:
        mov dword ptr [edi], edx;
        add edi, 4;
        dec ch;
        jg out_trailing_loop;

        dec edx;
        mov dh, (MAX_M + 1) * MAX_M * 4 / 256;
        add esp, edx;
        popad;
        ret;
    }
}

一些有趣的事情要注意:

  • 生成随机数仅需3个字节的机器代码(rdrand指令)
  • 碰巧的是,表的大小是64,所以一行的大小是256个字节。我用它在“高字节”寄存器(如)中保存行索引ah,这使我可以自动乘以256。为了利用这一点,我牺牲了对n = 65。我希望我可以为这种罪过感到宽恕...
  • 通过从堆栈指针寄存器中减去0x4100来执行堆栈上的分配空间esp。这是一个6字节的指令!将这个数字加回去时,我设法用5个字节来做:

        dec edx; // here edx = 1 from earlier calculations
        mov dh, (MAX_M + 1) * MAX_M * 4 / 256; // now edx = 0x4100
        add esp, edx; // this deallocates space on stack
    
  • 在MS Visual Studio中调试此功能时,我发现将数据写入其在堆栈上分配的空间时会崩溃!深入研究后,我发现了某种堆栈溢出保护:OS似乎只为堆栈分配了非常有限的虚拟地址范围;如果某个函数访问的地址太远,则OS会认为它超出了限制并杀死了该程序。但是,如果函数具有许多局部变量,则OS会做一些额外的“魔术”操作。因此,我必须调用一个在堆栈上分配了一个大数组的空函数。此函数返回后,将分配并使用额外的堆栈VM页面。

        void make_stack()
        {
            volatile int temp[65 * 64];
            temp[0] = 999; // have to "use" the array to prevent optimizing it out
        }
    
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.