编程竞技:考拉兹猜想

86

http://xkcd.com/710/启发,这里提供一个代码高尔夫挑战。

挑战

给定一个大于0的正整数,输出它的Hailstone序列。

Hailstone序列

详见Wikipedia

  • 如果数是偶数,则将其除以2。
  • 如果数是奇数,则将其乘以3再加1。

重复以上步骤,直到产生的数等于1(如果继续进行下去,它将进入一个无限循环 1 -> 4 -> 2 -> 1...)。

有时候通过代码来解释可能是最好的方式,以下是来自Wikipedia的一些代码示例:

function collatz(n)
  show n
  if n > 1
    if n is odd
      call collatz(3n + 1)
    else
      call collatz(n / 2)

这段代码可以运行,但我想增加额外的挑战。程序不能容易地受到堆栈溢出的攻击。因此,它必须使用迭代或尾递归。

如果能够计算大数并且所使用的编程语言没有内置实现,那么将获得额外奖励分数。(或者如果您使用定长整数重新实现了大数支持)

测试用例

Number: 21
Results: 21 -> 64 -> 32 -> 16 -> 8 -> 4 -> 2 -> 1

Number: 3
Results: 3 -> 10 -> 5 -> 16 -> 8 -> 4 -> 2 -> 1

此外,代码高尔夫必须包括完整的用户输入和输出。


20
“must not be vulnerable to stack overflows”的意思是“不能容易受到堆栈溢出攻击”。而你说的“那你就不应该在这里发布了!”是一种玩笑话语,暗示讨论的内容可能存在安全漏洞。 - Felix Kling
51
我的朋友不再打电话给我了,这是否意味着我已经解决了问题? - Martin
18
你在SO上,但曾经有朋友吗?那是什么感觉? - Pops
5
汇编语言的回答很棒,但选择最长的答案有点反对代码高尔夫 - John La Rooy
显示剩余7条评论
70个回答

129

x86汇编,1337个字符

;
; To assemble and link this program, just run:
;
; >> $ nasm -f elf collatz.asm && gcc -o collatz collatz.o
;
; You can then enjoy its output by passing a number to it on the command line:
;
; >> $ ./collatz 123
; >> 123 --> 370 --> 185 --> 556 --> 278 --> 139 --> 418 --> 209 --> 628 --> 314
; >> --> 157 --> 472 --> 236 --> 118 --> 59 --> 178 --> 89 --> 268 --> 134 --> 67
; >> --> 202 --> 101 --> 304 --> 152 --> 76 --> 38 --> 19 --> 58 --> 29 --> 88
; >> --> 44 --> 22 --> 11 --> 34 --> 17 --> 52 --> 26 --> 13 --> 40 --> 20 --> 10
; >> --> 5 --> 16 --> 8 --> 4 --> 2 --> 1
; 
; There's even some error checking involved:
; >> $ ./collatz
; >> Usage: ./collatz NUMBER
;
section .text
global main
extern printf
extern atoi

main:

  cmp dword [esp+0x04], 2
  jne .usage

  mov ebx, [esp+0x08]
  push dword [ebx+0x04]
  call atoi
  add esp, 4

  cmp eax, 0
  je .usage

  mov ebx, eax
  push eax
  push msg

.loop:
  mov [esp+0x04], ebx
  call printf

  test ebx, 0x01
  jz .even

.odd:
  lea ebx, [1+ebx*2+ebx]
  jmp .loop

.even:

  shr ebx, 1
  cmp ebx, 1
  jne .loop

  push ebx
  push end
  call printf

  add esp, 16
  xor eax, eax
  ret

.usage:
  mov ebx, [esp+0x08]
  push dword [ebx+0x00]
  push usage
  call printf
  add esp, 8
  mov eax, 1
  ret

msg db "%d --> ", 0
end db "%d", 10, 0
usage db "Usage: %s NUMBER", 10, 0

27
x86汇编语言 1337字符。我因此感动得流泪。 - ZoogieZork
10
我喜欢在3n+1问题中滥用"lea"(汇编语言中的一种指令)。 - wowest
感谢您不使用 int 23h - Mike D.
我该如何在Linux中编译?迫不及待想尝试一下。 - hidroto

64

Befunge

&>:.:1-|
  >3*^ @
  |%2: <
 v>2/>+

2
你应该以二维方式阅读此内容。<>^v是改变“程序计数器”方向的箭头。|和_是条件语句,根据堆栈上的值是真还是假而上下或左右移动。整个“代码竞技场”通过顶部-底部和左侧-右侧进行环绕。 - SF.
只有35个字符,包括空格!一点也不错! - Potatoswatter
6
你确定它不是Perl语言吗? - ijw

51

Python - 95 64 51 46字符

明显不会产生堆栈溢出。

n=input()
while n>1:n=(n/2,n*3+1)[n%2];print n

4
你可能需要明确指定Python 2.x版本。如果我没记错的话,Python 3.x 的 input 不会执行 eval - Mike D.
5
这不符合要求 - 它没有打印第一个数字。 - Ben Lings
7
为什么这个被接受了?它不是最短的,也没有打印第一个数字。 - Claudiu
1
我猜input()会回显你输入的字符,所以这确实会打印出第一个数字 :) - Danko Durbić
1
@Danko:不过要换行。 - kennytm
17
你可以通过使用 n=input()*2 仅花费 2 字节的成本打印第一个数字。 - John La Rooy

51

LOLCODE:406个字符

HAI
BTW COLLATZ SOUNDZ JUS LULZ

CAN HAS STDIO?

I HAS A NUMBAR
BTW, I WANTS UR NUMBAR
GIMMEH NUMBAR

VISIBLE NUMBAR

IM IN YR SEQUENZ
  MOD OF NUMBAR AN 2
  BOTH SAEM IT AN 0, O RLY?
    YA RLY, NUMBAR R QUOSHUNT OF NUMBAR AN 2
    NO WAI, NUMBAR R SUM OF PRODUKT OF NUMBAR AN 3 AN 1
  OIC
  VISIBLE NUMBAR
  DIFFRINT 2 AN SMALLR OF 2 AN NUMBAR, O RLY?
    YA RLY, GTFO
  OIC
IM OUTTA YR SEQUENZ

KTHXBYE

测试通过 Justin J. Meza的解释器。拜拜了您嘞!


23

Haskell, 62个字符 63 76 83, 86, 97, 137

c 1=[1]
c n=n:c(div(n`mod`2*(5*n+2)+n)2)
main=readLn>>=print.c

用户输入,输出结果,使用常量内存和栈,适用于任意大的整数。

这段代码的一个样例运行,给出了一个全部为'1'的80位数作为输入,非常有趣。


原始、仅包含函数版本:

Haskell 51个字符

f n=n:[[],f([n`div`2,3*n+1]!!(n`mod`2))]!!(1`mod`n)

谁需要条件语句啊?

(编辑:我很聪明地使用了"fix"。没有它,代码长度降至54个字符。编辑2:通过将f()分解出来,降至51个字符。)


在完成了我的 Miranda 帖子之后(基本上只是旧版 Haskell),至少在 Miranda 中,您可以通过仅使用一个感叹号来缩短代码 - f n=n:[[],[f(n div 2),f(3*n+1)]!(n mod 2)]!(1 mod n) - 运行正常 :) - Elle H
@Martinho:我也是,但由于惰性求值,这些表格甚至比其他语言中的表格更加酷炫。 - Dario
1
使用jleedev的想法:c 1=[1];c n=n:(c$div(nmod2*(5*n+2)+n)2) - 这41个字符利用了这个事实,即这是k (3n + 1)+(1-k) n / 2,其中k = n mod 2。 - sdcvvc
我基于这段代码添加了一个新的Haskell答案,可以进行输入/输出。由于这将增加字符数达到97个,因此我没有编辑这个答案。大家更喜欢我将完整的答案编辑到这个里面(保留投票,但增加大小),并删除我的第二个Haskell条目吗?还是应该将它们分开保留? - MtnViewMark
2
我删除了我的另一个条目,并将我的代码移动到这里,并结合了这些评论中的更多想法。字符数增加到76,但包括输入和输出。 - MtnViewMark
显示剩余5条评论

23

Perl

我决定有点反竞争,展示通常如何在Perl中编写这样的问题。
最后还有一个46个字符的代码高尔夫入口。

这前三个例子都从这个标题开始。

#! /usr/bin/env perl
use Modern::Perl;
# which is the same as these three lines:
# use 5.10.0;
# use strict;
# use warnings;

while( <> ){
  chomp;
  last unless $_;
  Collatz( $_ );
}
  • 简单递归版本

use Sub::Call::Recur;
sub Collatz{
  my( $n ) = @_;
  $n += 0; # ensure that it is numeric
  die 'invalid value' unless $n > 0;
  die 'Integer values only' unless $n == int $n;
  say $n;
  given( $n ){
    when( 1 ){}
    when( $_ % 2 != 0 ){ # odd
      recur( 3 * $n + 1 );
    }
    default{ # even
      recur( $n / 2 );
    }
  }
}
  • 简单的迭代版本

  • sub Collatz{
      my( $n ) = @_;
      $n += 0; # ensure that it is numeric
      die 'invalid value' unless $n > 0;
      die 'Integer values only' unless $n == int $n;
      say $n;
      while( $n > 1 ){
        if( $n % 2 ){ # odd
          $n = 3 * $n + 1;
        } else { #even
          $n = $n / 2;
        }
        say $n;
      }
    }
    
  • 优化后的迭代版本

  • sub Collatz{
      my( $n ) = @_;
      $n += 0; # ensure that it is numeric
      die 'invalid value' unless $n > 0;
      die 'Integer values only' unless $n == int $n;
      #
      state @next;
      $next[1] //= 0; # sets $next[1] to 0 if it is undefined
      #
      # fill out @next until we get to a value we've already worked on
      until( defined $next[$n] ){
        say $n;
        #
        if( $n % 2 ){ # odd
          $next[$n] = 3 * $n + 1;
        } else { # even
          $next[$n] = $n / 2;
        }
        #
        $n = $next[$n];
      }
      say $n;
      # finish running until we get to 1
      say $n while $n = $next[$n];
    }
    
    现在我将展示如何使用 Perl v5.10.0 之前的版本完成上一个示例。
    #! /usr/bin/env perl
    use strict;
    use warnings;
    
    while( <> ){
      chomp;
      last unless $_;
      Collatz( $_ );
    }
    {
      my @next = (0,0); # essentially the same as a state variable
      sub Collatz{
        my( $n ) = @_;
        $n += 0; # ensure that it is numeric
        die 'invalid value' unless $n > 0;
    
        # fill out @next until we get to a value we've already worked on
        until( $n == 1 or defined $next[$n] ){
          print $n, "\n";
    
          if( $n % 2 ){ # odd
            $next[$n] = 3 * $n + 1;
          } else { # even
            $next[$n] = $n / 2;
          }
          $n = $next[$n];
        }
        print $n, "\n";
    
        # finish running until we get to 1
        print $n, "\n" while $n = $next[$n];
      }
    }
    

    性能测试

    首先,IO(输入输出)始终是速度较慢的部分。因此,如果您按原样对它们进行基准测试,则应该获得每个函数相同的速度。

    为了测试这些函数,我打开了一个与/dev/null ($null)有关的文件句柄,并将每个say $n编辑为say {$null} $n。这是为了减少对IO的依赖。

    #! /usr/bin/env perl
    use Modern::Perl;
    use autodie;
    
    open our $null, '>', '/dev/null';
    
    use Benchmark qw':all';
    
    cmpthese( -10,
    {
      Recursive => sub{ Collatz_r( 31 ) },
      Iterative => sub{ Collatz_i( 31 ) },
      Optimized => sub{ Collatz_o( 31 ) },
    });
    
    sub Collatz_r{
      ...
      say {$null} $n;
      ...
    }
    sub Collatz_i{
      ...
      say {$null} $n;
      ...
    }
    sub Collatz_o{
      ...
      say {$null} $n;
      ...
    }
    

    运行了10次后,这是代表性的输出示例:

                Rate Recursive Iterative Optimized
    Recursive 1715/s        --      -27%      -46%
    Iterative 2336/s       36%        --      -27%
    Optimized 3187/s       86%       36%        --
    

    最后,一个真正的代码高尔夫入门:

    perl -nlE'say;say$_=$_%2?3*$_+1:$_/2while$_>1'
    

    总共46个字符

    如果您不需要打印起始值,您可以再删减5个字符。

    perl -nE'say$_=$_%2?3*$_+1:$_/2while$_>1'
    

    总共41个字符
    实际代码部分为31个字符,但是没有 -n 开关代码将无法运行。因此我在计数时包含了整个示例。


    @Brad,当你在一个数字上运行Collatz时,优化是一种恶化,因为除非猜想是错误的,否则不应该出现重复数字。你看到改进的原因是你正在运行许多数字(就像欧拉问题一样),事实上,我最近写了一篇关于这个的博客文章http://lanzkron.wordpress.com/2010/01/18/yet-another-meaningless-javascript-benchmark/。 - Motti
    2
    @Motti,这就是我所说的优化。此外,在Perl中,$i + 1始终是加法(回应博客文章)。另外,使用Sub::Call::Recur也是一种优化。否则,我会使用@_=$n;goto &Collatz。(如果将state @next更改为my @next,速度会慢10-20%) - Brad Gilbert
    3
    我认为 Perl Golf 计数标准不计算调用解释器时所需的强制击键或引号,但对于除 E 之外的每个旗标都会计算一个击键。按照这些规则,您最后的输入分别计算了37个字符和32个字符。 - R. Martinho Fernandes
    @Brad,关于$i + 1,没错,我在这个例子中混淆了JavaScript和Perl(忘记了.运算符)。 - Motti
    为什么你不直接将stdout重定向到/dev/null并将时间打印到stderr呢?例如perl foo > /dev/null,而不是修改你的代码。 - Alex Budovski
    显示剩余3条评论

    22

    Golfscript:20个字符

      ~{(}{3*).1&5*)/}/1+`
    # 
    # Usage: echo 21 | ruby golfscript.rb collatz.gs
    

    这与

    stack<int> s;
    s.push(21);
    while (s.top() - 1) {
      int x = s.top();
      int numerator = x*3+1;
      int denominator = (numerator&1) * 5 + 1;
      s.push(numerator/denominator);
    }
    s.push(1);
    return s;
    

    2
    必须包含完整的用户输入和输出。 - F'x
    2
    @FX,将“21”替换为“~”将导致程序使用来自stdin的数字。 - John La Rooy
    @gnibbler:Golfscript.rb有更新吗?我在将“21”替换为“~”时出现了“(eval):1:in initialize': undefined method leftparen' for nil:NilClass (NoMethodError)”错误。 - kennytm
    @KennyTM,遗憾的是GolfScript无法交互式地读取stdin,您必须将某些内容通过管道传递到stdin中,例如 echo 21 | ruby golfscript.rb collatz.gs - John La Rooy

    19

    bc 41个字符

    我猜这种问题正是为了发明bc的。

    for(n=read();n>1;){if(n%2)n=n*6+2;n/=2;n}
    

    测试:

    bc1 -q collatz.bc
    21
    64
    32
    16
    8
    4
    2
    1
    

    正确的代码:

    for(n=read();n>1;){if(n%2)n=n*3+1else n/=2;print n,"\n"}
    

    bc 可以处理最多有INT_MAX个数字的数

    编辑:维基百科文章提到该猜想已被检查过,对于所有小于20x258(约为5.76e18)的值。此程序:

    c=0;for(n=2^20000+1;n>1;){if(n%2)n=n*6+2;n/=2;c+=1};n;c
    

    测试在68秒内完成,220,000+1(约为3.98e6,020)次,144,404周期。


    将“n!=1”更改为“n>1”,共54个字符。 - Jerry Coffin
    4
    以下是生成任意长度数字的命令行(此处为10000位数):cat /dev/urandom | tr -dc '0-9' | head -c 10000 | bc collatz-conjecture.bc - indiv
    3
    @indiv - 我不得不测试它 :), 处理这个有10,000位数字的数花了3分12秒的时间。我已将输出保存到文件中,文件大约1.2GB,但是它确实在1秒内正确完成。bc得分。 - Carlos Gutiérrez

    16

    Perl:31个字符

    perl -nE 'say$_=$_%2?$_*3+1:$_/2while$_>1'
    #         123456789 123456789 123456789 1234567
    

    编辑以去除2个不必要的空格。

    编辑以去除1个不必要的空格。


    你可以删除两个不必要的空格(在say后面和while后面)。 - sorpigal
    尝试perl -E '当$_>1时,say$_=$_%2?$_*3+1:$_/2;说$_=10;' - sorpigal
    我想用户应该知道序列的起始数字 ;-). - a'r
    41
    有时当我遇到 base64 编码的文本时,有时会错误地将其误认为是 Perl 源代码。 - Martin
    21
    @Martin:我无法想象你会如何做到这一点。Base64更易读。 - Jerry Coffin
    显示剩余2条评论

    15

    微软 Excel,35 个字符

    =IF(A1/2=ROUND(A1/2,0),A1/2,A1*3+1)
    

    以下内容直接来自维基百科:

    In cell A1, place the starting number.
    In cell A2 enter this formula =IF(A1/2=ROUND(A1/2,0),A1/2,A1*3+1) 
    Drag and copy the formula down until 4, 2, 1
    

    只需要将公式复制/粘贴111次,就能得到从1000开始的结果。 ;)


    16
    我猜现在指出这就是填充手柄的作用,可能有点晚了,不是吗?http://www.ehow.com/how_2284668_use-fill-handle-microsoft-excel.html :) - Jordan Running
    这是一个非常方便的功能,我甚至不知道它的存在。我只是复制了第一个单元格,然后突出显示所有其他单元格并粘贴一次。 - Lance McNearney
    我在发现填充手柄大约十年后才了解到区域粘贴。 - Jimmy

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接