Хруст числа в Ruby (необходима оптимизация) - PullRequest
2 голосов
/ 15 апреля 2011

Ruby, возможно, не является оптимальным языком для этого, но мне удобно работать с этим в моем терминале, поэтому я и собираюсь.

Мне нужно обработать числа от 1 до 666666, поэтому я распечатываю все числа, которые содержат 6, но не содержат 7, 8 или 9. Первое число будет 6, следующее 16, затем 26 и пр. Затем мне нужно было напечатать его как (6=6) (16=6) (26=6), а когда у меня есть диапазоны от 60 до 66, мне нужно, чтобы он напечатался как (60 THRU 66=6) (синтаксис SPSS).

У меня есть этот код, и он работает, но он не красив и не очень эффективен, так как я могу его оптимизировать?

(может следовать глупый код)

class Array
  def to_ranges
    array = self.compact.uniq.sort
    ranges = []
    if !array.empty?
      # Initialize the left and right endpoints of the range
      left, right = array.first, nil
      array.each do |obj|
        # If the right endpoint is set and obj is not equal to right's successor 
        # then we need to create a range.
        if right && obj != right.succ
          ranges << Range.new(left,right)
          left = obj
        end
        right = obj
      end
      ranges << Range.new(left,right) unless left == right
    end
    ranges
  end
end

write = ""
numbers = (1..666666).to_a

# split each number in an array containing it's ciphers
numbers = numbers.map { |i| i.to_s.split(//) }

# delete the arrays that doesn't contain 6 and the ones that contains 6 but also 8, 7 and 9
numbers = numbers.delete_if { |i| !i.include?('6') }
numbers = numbers.delete_if { |i| i.include?('7') }
numbers = numbers.delete_if { |i| i.include?('8') }
numbers = numbers.delete_if { |i| i.include?('9') }

# join the ciphers back into the original numbers
numbers = numbers.map { |i| i.join }

numbers = numbers.map { |i| i = Integer(i) }

# rangify consecutive numbers
numbers = numbers.to_ranges

# edit the ranges that go from 1..1 into just 1
numbers = numbers.map do |i|
  if i.first == i.last
    i = i.first
  else
    i = i
  end
end

# string stuff
numbers = numbers.map { |i| i.to_s.gsub(".."," thru ") }

numbers = numbers.map { |i| "(" + i.to_s + "=6)"}

numbers.each { |i| write << " " + i }

File.open('numbers.txt','w') { |f| f.write(write) }

Как я уже сказал, это работает для чисел даже в миллионах - но я хотел бы получить несколько советов о том, как сделать красивее и эффективнее.

Ответы [ 10 ]

4 голосов
/ 16 апреля 2011

Я удалил свою предыдущую попытку на parlez-vous-ruby? и восполнил это. Я знаю, что есть оптимизированная версия превосходного примера x3ro .

$,="\n"
puts ["(0=6)", "(6=6)", *(1.."66666".to_i(7)).collect {|i| i.to_s 7}.collect do |s|
    s.include?('6')? "(#{s}0 THRU #{s}6=6)" : "(#{s}6=6)"
end ]

По сравнению с версией x3ro

... Это до трех строк

... 204,2 х быстрее (до 66666666)

... имеет байт-идентичный вывод

Он использует все мои идеи для оптимизации

  • номера поколений, основанные на модулях 7 цифр (поэтому цифры base-7)
  • генерирует последнюю цифру «умный»: это то, что сжимает диапазоны

Итак ... каковы сроки? Это было тестирование с 8 цифрами (до 66666666 или 823544 строк вывода):

$ time ./x3ro.rb >  /dev/null

real    8m37.749s
user    8m36.700s
sys 0m0.976s

$ time ./my.rb >  /dev/null

real    0m2.535s
user    0m2.460s
sys 0m0.072s

Несмотря на то, что производительность на самом деле хорошая, она даже не близка к оптимизированной версии C Я писал ранее: я не смог запустить my.rb до 6666666666 (6x10 ) из-за OutOfMemory . При запуске до 9 цифр это сравнительный результат:

sehe@meerkat:/tmp$ time ./my.rb >  /dev/null

real    0m21.764s
user    0m21.289s
sys 0m0.476s

sehe@meerkat:/tmp$ time ./t2 > /dev/null

real    0m1.424s
user    0m1.408s
sys 0m0.012s

Версия C все еще примерно в 15 раз быстрее ... что справедливо, если учесть, что она работает на голом металле.

Надеюсь, вам понравилось, и могу ли я получить ваши голоса, если только за изучение Ruby для этой цели:)

( Можете ли вы сказать, что я горжусь? Это мое первое знакомство с рубином; я начал заниматься рубиновым коаном 2 часа назад ... )

Редактировать @ johndouthat :

Очень мило! Использование base7 очень умно, и это отличная работа для вашей первой пробной версии ruby:)

Вот небольшая модификация вашего сниппета, которая позволит вам тестировать 10+ цифр без ошибки OutOfMemory:

puts ["(0=6)", "(6=6)"]
(1.."66666666".to_i(7)).each do |i| 
  s = i.to_s(7)
  puts s.include?('6') ? "(#{s}0 THRU #{s}6=6)" : "(#{s}6=6)"
end

# before:
real    0m26.714s
user    0m23.368s
sys 0m2.865s
# after
real    0m15.894s
user    0m13.258s
sys 0m1.724s
3 голосов
/ 15 апреля 2011

Используя шаблоны чисел, вы можете замкнуть множество петель, например:

Если вы определите prefix как место 100 и все перед ним, и определим suffix как все в 10-м и 1-м месте, а затем, цикл через каждый возможный префикс:

  • Если префикс пуст (то есть вы тестируете 0-99), тогда возможны 13 совпадений
  • Если префикс содержит 7, 8 или 9, возможных совпадений нет.
  • Если префикс содержит 6, есть 49 возможных совпадений (сетка 7x7)
  • иначе, есть 13 возможных совпадений. (см. изображение ниже)

enter image description here

(код еще не исключает числа, которых конкретно нет в диапазоне, но он довольно близок)

number_range = (1..666_666)
prefix_range = ((number_range.first / 100)..(number_range.last / 100))

for p in prefix_range
  ps = p.to_s

  # TODO: if p == prefix_range.last or p == prefix_range.first,
  # TODO: test to see if number_range.include?("#{ps}6".to_i), etc...

  if ps == '0'
    puts "(6=6) (16=6) (26=6) (36=6) (46=6) (56=6) (60 thru 66) "

  elsif ps =~ /7|8|9/
    # there are no candidate suffixes if the prefix contains 7, 8, or 9.

  elsif ps =~ /6/
    # If the prefix contains a 6, then there are 49 candidate suffixes
    for i in (0..6)
      print "(#{ps}#{i}0 thru #{ps}#{i}6) "
    end
    puts

  else
    # If the prefix doesn't contain 6, 7, 8, or 9, then there are only 13 candidate suffixes.
    puts "(#{ps}06=6) (#{ps}16=6) (#{ps}26=6) (#{ps}36=6) (#{ps}46=6) (#{ps}56=6) (#{ps}60 thru #{ps}66) "

  end
end

Что выводит следующее:

(6=6) (16=6) (26=6) (36=6) (46=6) (56=6) (60 thru 66) 
(106=6) (116=6) (126=6) (136=6) (146=6) (156=6) (160 thru 166) 
(206=6) (216=6) (226=6) (236=6) (246=6) (256=6) (260 thru 266) 
(306=6) (316=6) (326=6) (336=6) (346=6) (356=6) (360 thru 366) 
(406=6) (416=6) (426=6) (436=6) (446=6) (456=6) (460 thru 466) 
(506=6) (516=6) (526=6) (536=6) (546=6) (556=6) (560 thru 566) 
(600 thru 606) (610 thru 616) (620 thru 626) (630 thru 636) (640 thru 646) (650 thru 656) (660 thru 666) 
(1006=6) (1016=6) (1026=6) (1036=6) (1046=6) (1056=6) (1060 thru 1066) 
(1106=6) (1116=6) (1126=6) (1136=6) (1146=6) (1156=6) (1160 thru 1166) 
(1206=6) (1216=6) (1226=6) (1236=6) (1246=6) (1256=6) (1260 thru 1266) 
(1306=6) (1316=6) (1326=6) (1336=6) (1346=6) (1356=6) (1360 thru 1366) 
(1406=6) (1416=6) (1426=6) (1436=6) (1446=6) (1456=6) (1460 thru 1466) 
(1506=6) (1516=6) (1526=6) (1536=6) (1546=6) (1556=6) (1560 thru 1566) 
(1600 thru 1606) (1610 thru 1616) (1620 thru 1626) (1630 thru 1636) (1640 thru 1646) (1650 thru 1656) (1660 thru 1666) 

и т.д ...

2 голосов
/ 16 апреля 2011

Примечание Я не говорю на рубине, но я намереваюсь сделать позже сделал версию ruby ​​ только для сравнения скорости:)


Если вы просто итерируете все числа от 0 до 117648 (ruby <<< 'print "666666".to_i(7)') и напечатаете их в формате base-7, вы по крайней мере отбросите любые числа, содержащие 7,8,9.Это включает в себя предложение по оптимизации, предложенное MrE, помимо поднятия задачи на простую int-арифметику вместо манипуляций с последовательностью символов.

Все, что остается, - это проверить наличие хотя бы одного 6. Это сделаеталгоритм пропускает не более 6 элементов подряд, поэтому я считаю его менее важным (среднее количество пропускаемых элементов в общем диапазоне составляет 40%).

Простой тест для 6666666666

(обратите внимание, что это означает вывод 222 009 073 (222M) строк 6-ти чисел)

Оставаясь близко к этой идее, я написал этот весьма оптимизированный код C (я нене говори рубина) чтобы продемонстрировать идею.Я запустил его до 282475248 (соответствует 6666666666 (мод 7)), так что это было больше для измерения: 0m26.5s

#include <stdio.h>

static char buf[11]; 
char* const bufend = buf+10;

char* genbase7(int n)
{
    char* it = bufend; int has6 = 0;
    do
    { 
        has6 |= 6 == (*--it = n%7); 
        n/=7; 
    } while(n);

    return has6? it : 0;
}

void asciify(char* rawdigits)
{
    do { *rawdigits += '0'; } 
    while (++rawdigits != bufend);
}

int main()
{
    *bufend = 0; // init

    long i;
    for (i=6; i<=282475248; i++)
    {
        char* b7 = genbase7(i);
        if (b7)
        {
            asciify(b7);
            puts(b7);
        }
    }
}

Я также протестировал другой подход, который неудивительноработает менее чем в два раза быстрее, потому что

  • эта версия напрямую манипулирует результатами в виде строки ascii, готовой к отображению
  • эта версия сокращает флаг has6 для более глубоких уровней рекурсии
  • эта версия также оптимизирует «переворот» последней цифры, когда требуется «6»
  • код просто короче ...

Выполняетсявремя: 0m12.8s

#include <stdio.h>
#include <string.h>

inline void recursive_permute2(char* const b, char* const m, char* const e, int has6)
{
    if (m<e)
        for (*m = '0'; *m<'7'; (*m)++)
            recursive_permute2(b, m+1, e, has6 || (*m=='6'));
    else
        if (has6)
            for (*e = '0'; *e<'7'; (*e)++)
                puts(b);
        else /* optimize for last digit must be 6 */
            puts((*e='6', b));
}

inline void recursive_permute(char* const b, char* const e)
{
    recursive_permute2(b, b, e-1, 0);
}

int main()
{
    char buf[] = "0000000000"; 
    recursive_permute(buf, buf+sizeof(buf)/sizeof(*buf)-1);
}

Контрольные показатели измеряются с помощью:

gcc -O4 t6.c -o t6
time ./t6 > /dev/null
1 голос
/ 16 апреля 2011

Мой первый ответ был слишком умным.Вот гораздо более простая версия

class MutablePrintingCandidateRange < Struct.new(:first, :last)
  def to_s
    if self.first == nil and self.last == nil
      ''
    elsif self.first == self.last
      "(#{self.first}=6)"
    else
      "(#{self.first} thru #{self.last})"
    end
  end

  def <<(x)
    if self.first == nil and self.last == nil
      self.first = self.last = x
    elsif self.last == x - 1
      self.last = x
    else
      puts(self) # print the candidates
      self.first = self.last = x # reset the range
    end
  end

end

и как ее использовать:

numer_range = (1..666_666)
current_range = MutablePrintingCandidateRange.new

for i in numer_range
  candidate = i.to_s

  if candidate =~ /6/ and candidate !~ /7|8|9/
    # number contains a 6, but not a 7, 8, or 9
    current_range << i
  end
end
puts current_range
1 голос
/ 16 апреля 2011

Я придумал этот фрагмент кода, который старался более или менее сохранить в стиле FP. Вероятно, не намного эффективнее (как уже было сказано, с помощью базовой числовой логики вы сможете повысить производительность, например, перейдя непосредственно с 19xx до 2000, но я оставлю это на ваше усмотрение:)

def check(n)
    n = n.to_s
    n.include?('6') and
    not n.include?('7') and
    not n.include?('8') and
    not n.include?('9')
end

def spss(ranges)
  ranges.each do |range|
    if range.first === range.last
      puts "(" + range.first.to_s + "=6)"
    else
      puts "(" + range.first.to_s + " THRU " + range.last.to_s + "=6)"
    end
  end
end

range = (1..666666)

range = range.select { |n| check(n) }

range = range.inject([0..0]) do |ranges, n|
  temp = ranges.last
  if temp.last + 1 === n
    ranges.pop
    ranges.push(temp.first..n)
  else
    ranges.push(n..n)
  end
end

spss(range)
1 голос
/ 15 апреля 2011
$range_start = -1
$range_end = -1
$f = File.open('numbers.txt','w')

def output_number(i)
  if $range_end ==  i-1
    $range_end = i
  elsif $range_start < $range_end
    $f.puts "(#{$range_start} thru #{$range_end})"
    $range_start = $range_end = i
  else
    $f.puts "(#{$range_start}=6)" if $range_start > 0 # no range, print out previous number
    $range_start = $range_end = i
  end
end

'1'.upto('666') do |n|
  next unless n =~ /6/ # keep only numbers that contain 6
  next if n =~ /[789]/ # remove nubmers that contain 7, 8 or 9
  output_number n.to_i
end
if $range_start < $range_end
  $f.puts "(#{$range_start} thru #{$range_end})"
end
$f.close

puts "Ruby is beautiful :)"
0 голосов
/ 16 апреля 2011

Убийца здесь

numbers = (1..666666).to_a

Диапазон поддерживает итерации, поэтому вам будет лучше, если вы пройдете по всему диапазону и накопите числа, включающие ваши сегменты в блоки.Когда один блок закончен и заменен другим, вы можете записать его.

0 голосов
/ 16 апреля 2011

( Я не удосужился обновить свое решение C для форматирования. Вместо этого я выбрал x3ro отличную версию ruby ​​ и оптимизировал это )

Восстановлено:

Я до сих пор не уверен, что измененное поведение обозначения диапазона на самом деле не то, что хочет OP: эта версия изменяет поведение разделения диапазонов, которые на самом деле являются смежными по модулю 6; Я не удивлюсь, что ОП действительно ожидал .

....
(555536=6)
(555546=6)
(555556 THRU 666666=6)

вместо

....
(666640 THRU 666646=6)
(666650 THRU 666656=6)
(666660 THRU 666666=6)

Я позволю ОП решить, и вот модифицированная версия, которая работает в 18% времени как версия x3ro (3.2 с вместо 17.0 при генерации до 6666666 (7x6) ).

def check(n)
    n.to_s(7).include?('6')
end

def spss(ranges)
  ranges.each do |range|
    if range.first === range.last
      puts "(" + range.first.to_s(7) + "=6)"
    else
      puts "(" + range.first.to_s(7) + " THRU " + range.last.to_s(7) + "=6)"
    end
  end
end

range = (1..117648)

range = range.select { |n| check(n) }

range = range.inject([0..0]) do |ranges, n|
  temp = ranges.last
  if temp.last + 1 === n
    ranges.pop
    ranges.push(temp.first..n)
  else
    ranges.push(n..n)
  end
end

spss(range)
0 голосов
/ 16 апреля 2011

Мой ответ, приведенный ниже, не является полным, но просто для того, чтобы показать путь (я мог бы вернуться и продолжить ответ):

Есть только два случая:

1) Все цифрыкроме того, самая нижняя либо отсутствует, либо отсутствует 6

6, 16, ...

2) По крайней мере одна цифра, кроме самой нижней, включает 6

60--66, 160--166, 600--606, ...

Случаи в (1) не содержат непрерывных чисел, поскольку все они имеют 6 всамая низкая цифра, и отличаются друг от друга.Все случаи в (2) отображаются в виде непрерывных диапазонов, где самая низкая цифра продолжается от 0 до 6. Любое одно продолжение в (2) не является непрерывным с другим в (2) или с чем-либо из (1), потому что число на единицу меньшеxxxxx0 будет xxxxy9, а число больше xxxxxx6 будет xxxxxx7 и, следовательно, будет исключено.

Поэтому вопрос сводится к следующему:

3)

Получить все строки между "" и "66666", которые не содержат "6"
Для каждого из них ("xxx") выведите строку "(xxx6 = 6)"

4)

Получить все строки от "" до "66666", которые содержат хотя бы одну "6"
Для каждого из них ("xxx"), выведите строку "(xxx0THRU xxx6 = 6) "

0 голосов
/ 15 апреля 2011

Базовое наблюдение: если текущее число (скажем) 1900, вы знаете, что можете безопасно перейти как минимум до 2000 ...

...