Алгоритм O (nlogn) - Найти три равномерно расположенных в двоичной строке - PullRequest
173 голосов
/ 13 октября 2009

У меня вчера был этот вопрос на тесте Алгоритмов, и я не могу найти ответ. Это сводит меня с ума, потому что это стоило около 40 баллов. Я полагаю, что большинство класса не решило это правильно, потому что я не придумал решение за последние 24 часа.

Учитывая произвольную двоичную строку длины n, найдите три равномерно распределенные строки внутри строки, если они существуют. Напишите алгоритм, который решает это за время O (n * log (n)).

Таким образом, у подобных строк есть три "равномерно распределенных": 11100000, 0100100100

edit: это случайное число, поэтому оно должно работать на любое число. Примеры, которые я привел, должны были проиллюстрировать «равномерно распределенное» свойство. Таким образом, 1001011 является действительным числом. 1, 4 и 7 равны друг другу.

Ответы [ 31 ]

1 голос
/ 15 октября 2009

Здесь я приведу приблизительное предположение и позволю тем, кто лучше разбирается в сложности, помочь мне понять, как мой алгоритм работает в O-нотации

  1. заданная двоичная строка 0000010101000100 (как пример)
  2. обрезка головы и хвоста нулей -> 00000 101010001 00
  3. мы получаем 101010001 из предыдущего расчета
  4. проверка, является ли средний бит единицей, если истина, найдены правильные три равномерно распределенных единицы (только если число битов нечетно)
  5. соответственно, если оставшееся количество битов даже пронумеровано, голова и хвост «один» не могут быть частью равномерно расположенного «одного»,
  6. мы используем 1010100001 в качестве примера (с дополнительным «нулем», чтобы стать четным урожаем), в этом случае нам нужно снова обрезать, затем становится -> 10101 00001
  7. мы получаем 10101 из предыдущего расчета и проверяем средний бит, и мы снова нашли равномерно распределенный бит

Понятия не имею, как рассчитать сложность для этого, кто-нибудь может помочь?

edit: добавить код, чтобы проиллюстрировать мою идею

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

char *binaryStr = "0000010101000100";

int main() {
   int head, tail, pos;
   head = 0;
   tail = strlen(binaryStr)-1;
   if( (pos = find3even(head, tail)) >=0 )
      printf("found it at position %d\n", pos);
   return 0;
}

int find3even(int head, int tail) {
   int pos = 0;
   if(head >= tail) return -1;
   while(binaryStr[head] == '0') 
      if(head<tail) head++;
   while(binaryStr[tail] == '0') 
      if(head<tail) tail--;
   if(head >= tail) return -1;
   if( (tail-head)%2 == 0 && //true if odd numbered
       (binaryStr[head + (tail-head)/2] == '1') ) { 
         return head;
   }else {
      if( (pos = find3even(head, tail-1)) >=0 )
         return pos;
      if( (pos = find3even(head+1, tail)) >=0 )
         return pos;
   }
   return -1;
}
1 голос
/ 15 октября 2009

Я придумал что-то вроде этого:

def IsSymetric(number):
    number = number.strip('0')

    if len(number) < 3:
        return False
    if len(number) % 2 == 0:
        return IsSymetric(number[1:]) or IsSymetric(number[0:len(number)-2])
    else:
        if number[len(number)//2] == '1':
            return True
        return IsSymetric(number[:(len(number)//2)]) or IsSymetric(number[len(number)//2+1:])
    return False

Это вдохновлено andycjw.

  1. Обрезать нули.
  2. Если даже тогда проверить две подстроки 0 - (len-2) (пропустить последний символ) и из 1 - (len-1) (пропустить первый символ)
  3. Если нет, даже если средний символ равен единице, мы добьемся успеха. Еще разделите строку в середине без элемента середины и проверьте обе части.

Что касается сложности, то это может быть O (nlogn), так как в каждой рекурсии мы делим на два.

Надеюсь, это поможет.

1 голос
/ 16 октября 2009

Хорошо, я собираюсь сделать еще один удар по проблеме. Я думаю, что могу доказать алгоритм O (n log (n)), который похож на те, которые уже обсуждались, используя сбалансированное двоичное дерево для хранения расстояний между единицами. Этот подход был вдохновлен наблюдением Джастиса о сокращении проблемы до списка расстояний между единицами.

Можем ли мы отсканировать входную строку, чтобы построить сбалансированное двоичное дерево вокруг позиции 1, чтобы каждый узел сохранял позицию 1, а каждое ребро помечено расстоянием до смежной 1 для каждого дочернего узла. Например:

10010001 gives the following tree

      3
     / \
  2 /   \ 3
   /     \
  0       7

Это может быть сделано в O (n log (n)), поскольку для строки размера n каждая вставка принимает O (log (n)) в худшем случае.

Тогда проблема состоит в том, чтобы найти дерево, чтобы определить, есть ли в каком-либо узле путь от этого узла через левого потомка, который имеет то же расстояние, что и путь через правый потомок. Это можно сделать рекурсивно на каждом поддереве. При объединении двух поддеревьев в поиске мы должны сравнить расстояния от путей в левом поддереве с расстояниями от путей в правом. Поскольку число путей в поддереве будет пропорционально log (n), а количество узлов равно n, я считаю, что это можно сделать за O (n log (n)).

Я что-то пропустил?

0 голосов
/ 17 октября 2009

Как насчет простого решения O (n) с пробелом O (n ^ 2)? (Используется предположение, что все побитовые операторы работают в O (1).)

Алгоритм в основном работает в четыре этапа:

Этап 1: для каждого бита в вашем исходном числе выясните, как далеко они находятся, но примите во внимание только одно направление. (Я рассмотрел все биты в направлении младшего значащего бита.)

Этап 2: обратный порядок битов на входе;

Этап 3: повторно выполнить шаг 1 для обратного входа.

Стадия 4: Сравните результаты Стадии 1 и Стадии 3. Если какие-либо биты находятся на одинаковом расстоянии выше И ниже, мы должны получить попадание.

Имейте в виду, что ни один шаг в приведенном выше алгоритме не занимает больше времени, чем O (n). ^ _ ^

В качестве дополнительного преимущества этот алгоритм найдет ВСЕ равные интервалы от КАЖДОГО числа. Так, например, если вы получите результат «0x0005», то на равных 1 и 3 единицах будут одинаковые интервалы

Я действительно не пытался оптимизировать код ниже, но это скомпилированный код C #, который, кажется, работает.

using System;

namespace ThreeNumbers
{
    class Program
    {
        const int uint32Length = 32;

        static void Main(string[] args)
        {
            Console.Write("Please enter your integer: ");
            uint input = UInt32.Parse(Console.ReadLine());

            uint[] distancesLower = Distances(input);
            uint[] distancesHigher = Distances(Reverse(input));

            PrintHits(input, distancesLower, distancesHigher);
        }

        /// <summary>
        /// Returns an array showing how far the ones away from each bit in the input.  Only 
        /// considers ones at lower signifcant bits.  Index 0 represents the least significant bit 
        /// in the input.  Index 1 represents the second least significant bit in the input and so 
        /// on.  If a one is 3 away from the bit in question, then the third least significant bit 
        /// of the value will be sit.
        /// 
        /// As programed this algorithm needs: O(n) time, and O(n*log(n)) space.  
        /// (Where n is the number of bits in the input.)
        /// </summary>
        public static uint[] Distances(uint input)
        {
            uint[] distanceToOnes = new uint[uint32Length];
            uint result = 0;

            //Sets how far each bit is from other ones. Going in the direction of LSB to MSB
            for (uint bitIndex = 1, arrayIndex = 0; bitIndex != 0; bitIndex <<= 1, ++arrayIndex)
            {
                distanceToOnes[arrayIndex] = result;
                result <<= 1;

                if ((input & bitIndex) != 0)
                {
                    result |= 1;
                }
            }

            return distanceToOnes;
        }

        /// <summary>
        /// Reverses the bits in the input.
        /// 
        /// As programmed this algorithm needs O(n) time and O(n) space.  
        /// (Where n is the number of bits in the input.)
        /// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        public static uint Reverse(uint input)
        {
            uint reversedInput = 0;
            for (uint bitIndex = 1; bitIndex != 0; bitIndex <<= 1)
            {
                reversedInput <<= 1;
                reversedInput |= (uint)((input & bitIndex) != 0 ? 1 : 0);
            }

            return reversedInput;
        }

        /// <summary>
        /// Goes through each bit in the input, to check if there are any bits equally far away in 
        /// the distancesLower and distancesHigher
        /// </summary>
        public static void PrintHits(uint input, uint[] distancesLower, uint[] distancesHigher)
        {
            const int offset = uint32Length - 1;

            for (uint bitIndex = 1, arrayIndex = 0; bitIndex != 0; bitIndex <<= 1, ++arrayIndex)
            {
                //hits checks if any bits are equally spaced away from our current value
                bool isBitSet = (input & bitIndex) != 0;
                uint hits = distancesLower[arrayIndex] & distancesHigher[offset - arrayIndex];

                if (isBitSet && (hits != 0))
                {
                    Console.WriteLine(String.Format("The {0}-th LSB has hits 0x{1:x4} away", arrayIndex + 1, hits));
                }
            }
        }
    }
}

Кто-то, вероятно, прокомментирует, что для любого достаточно большого числа битовые операции не могут быть выполнены в O (1). Ты был бы прав. Однако я бы предположил, что каждое решение, использующее сложение, вычитание, умножение или деление (что не может быть выполнено путем сдвига), также будет иметь эту проблему.

0 голосов
/ 17 октября 2009

Ниже приведено решение. Там и там могут быть небольшие ошибки, но идея здравая.

Редактировать: Это не n * log (n)

КОД PSEUDO:

foreach character in the string
  if the character equals 1 {         
     if length cache > 0 { //we can skip the first one
        foreach location in the cache { //last in first out kind of order
           if ((currentlocation + (currentlocation - location)) < length string)
              if (string[(currentlocation + (currentlocation - location))] equals 1)
                 return found evenly spaced string
           else
              break;
        }
     }
     remember the location of this character in a some sort of cache.
  }

return didn't find evenly spaced string

C # код:

public static Boolean FindThreeEvenlySpacedOnes(String str) {
    List<int> cache = new List<int>();

    for (var x = 0; x < str.Length; x++) {
        if (str[x] == '1') {
            if (cache.Count > 0) {
                for (var i = cache.Count - 1; i > 0; i--) {
                    if ((x + (x - cache[i])) >= str.Length)
                        break;

                    if (str[(x + (x - cache[i]))] == '1')
                        return true;                            
                }
            }
            cache.Add(x);                    
        }
    }

    return false;
}

Как это работает:

iteration 1:
x
|
101101001
// the location of this 1 is stored in the cache

iteration 2:
 x
 | 
101101001

iteration 3:
a x b 
| | | 
101101001
//we retrieve location a out of the cache and then based on a 
//we calculate b and check if te string contains a 1 on location b

//and of course we store x in the cache because it's a 1

iteration 4:
  axb  
  |||  
101101001

a  x  b  
|  |  |  
101101001


iteration 5:
    x  
    |  
101101001

iteration 6:
   a x b 
   | | | 
101101001

  a  x  b 
  |  |  | 
101101001
//return found evenly spaced string
0 голосов
/ 17 октября 2009

Я постараюсь представить математический подход. Это скорее начало, чем конец, поэтому любая помощь, комментарии или даже противоречия будут высоко оценены. Однако, если этот подход доказан - алгоритм представляет собой простой поиск в строке.

  1. При фиксированном количестве пробелов k и строке S поиск k-интервального триплета занимает O(n) - мы просто проверяем каждый 0<=i<=(n-2k), если S[i]==S[i+k]==S[i+2k]. Тест занимает O(1), и мы делаем это n-k раз, где k является константой, поэтому требуется O(n-k)=O(n).

  2. Предположим, что существует обратная пропорция между числом 1 и максимальными пробелами, которые мы должны искать. То есть, если есть много 1, должен быть триплет, и он должен быть достаточно плотным; Если есть только несколько 1, триплет (если есть) может быть довольно разреженным. Другими словами, я могу доказать, что если у меня достаточно 1, такой триплет должен существовать - и чем больше у меня 1, тем более плотный триплет должен быть найден. Это может быть объяснено принципом Pigeonhole - Надеюсь уточнить это позже.

  3. Скажем, есть верхняя граница k на возможном количестве пробелов, которые мне нужно искать. Теперь для каждого 1, расположенного в S[i], нам нужно проверить 1 в S[i-1] и S[i+1], S[i-2] и S[i+2], ... S[i-k] и S[i+k]. Это требует O((k^2-k)/2)=O(k^2) для каждого 1 в S - из-за формулы суммирования серии Гаусса . Обратите внимание, что это отличается от раздела 1 - у меня k в качестве верхней границы для числа пробелов, а не в качестве постоянного пробела.

Нам нужно доказать O(n*log(n)). То есть нам нужно показать, что k*(number of 1's) пропорционально log(n).

Если мы сможем это сделать, алгоритм будет тривиальным - для каждого 1 в S, индекс которого равен i, просто ищите 1 с каждой стороны до расстояния k. Если два были найдены на одном и том же расстоянии, верните i и k. Опять же, хитрая часть будет искать k и доказывать правильность.

Буду очень признателен за ваши комментарии. Я пытался найти связь между k и числом 1 на моей доске, но пока безуспешно.

0 голосов
/ 17 октября 2009

Я решил добавить один комментарий, прежде чем публиковать 22-е наивное решение проблемы. Для наивного решения нам не нужно показывать, что число 1 в строке не более O (log (n)), а скорее всего O (sqrt (n * log (n))).

Solver:

def solve(Str):
    indexes=[]
    #O(n) setup
    for i in range(len(Str)):
        if Str[i]=='1':
            indexes.append(i)

    #O((number of 1's)^2) processing
    for i in range(len(indexes)):
        for j in range(i+1, len(indexes)):
                            indexDiff = indexes[j] - indexes[i]
            k=indexes[j] + indexDiff
            if k<len(Str) and Str[k]=='1':
                return True
    return False

По сути, это немного похоже на идею и реализацию flybywire, хотя и смотрит вперед, а не назад.

Жадный струнный строитель:

#assumes final char hasn't been added, and would be a 1 
def lastCharMakesSolvable(Str):
    endIndex=len(Str)
    j=endIndex-1
    while j-(endIndex-j) >= 0:
        k=j-(endIndex-j)
        if k >= 0 and Str[k]=='1' and Str[j]=='1':
            return True
        j=j-1
    return False



def expandString(StartString=''):
    if lastCharMakesSolvable(StartString):
        return StartString + '0'
    return StartString + '1'

n=1
BaseStr=""
lastCount=0
while n<1000000:
    BaseStr=expandString(BaseStr)
    count=BaseStr.count('1')
    if count != lastCount:
        print(len(BaseStr), count)
    lastCount=count
    n=n+1

(В свою защиту, я все еще нахожусь в стадии понимания "учить питона")

Кроме того, потенциально полезный вывод из жадного строения строк, есть довольно последовательный скачок после удара степени 2 в числе 1 ... которого я не хотел ждать, чтобы засвидетельствовать попадание 2096.

strlength   # of 1's
    1    1
    2    2
    4    3
    5    4
   10    5
   14    8
   28    9
   41    16
   82    17
  122    32
  244    33
  365    64
  730    65
 1094    128
 2188    129
 3281    256
 6562    257
 9842    512
19684    513
29525    1024
0 голосов
/ 14 октября 2009

Это казалось забавной проблемой, поэтому я решил попробовать свои силы.

Я предполагаю, что 111000001 найдет первые 3 и будет успешным. По сути, число нулей, следующих за 1, является важным, поскольку, согласно вашему определению, 0111000 соответствует 111000 Как только вы найдете два случая 1, следующий найденный 1 завершит трилогию.

Вот оно в Python:

def find_three(bstring):
    print bstring
    dict = {}
    lastone = -1
    zerocount = 0
    for i in range(len(bstring)):
        if bstring[i] == '1':
            print i, ': 1'
            if lastone != -1:
                if(zerocount in dict):
                    dict[zerocount].append(lastone)
                    if len(dict[zerocount]) == 2:
                        dict[zerocount].append(i)
                        return True, dict
                else:
                    dict[zerocount] = [lastone]
            lastone = i
            zerocount = 0
        else:
            zerocount = zerocount + 1
    #this is really just book keeping, as we have failed at this point
    if lastone != -1:
        if(zerocount in dict):
            dict[zerocount].append(lastone)
        else:
            dict[zerocount] = [lastone]
    return False, dict

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

0 голосов
/ 16 октября 2009

Вот некоторые мысли, которые, несмотря на все мои старания, не обернутся в поклон. Тем не менее, они могут быть полезной отправной точкой для чьего-либо анализа.

Рассмотрим предложенное решение следующим образом. Это тот подход, который предложили несколько человек, включая меня в предыдущей версии этого ответа. :)

  1. Обрезать начальные и конечные нули.
  2. Сканирование строки в поисках 1.
  3. Когда 1 найден:
    1. Предположим, что это середина 1 решения.
    2. Для каждого предыдущего 1 используйте сохраненную позицию, чтобы вычислить ожидаемую позицию финала 1.
    3. Если вычисленная позиция находится после конца строки, она не может быть частью решения, поэтому исключите эту позицию из списка кандидатов.
    4. Проверьте решение.
  4. Если решение не найдено, добавьте текущий 1 в список кандидатов.
  5. Повторяйте, пока не будет найдено больше 1.

Теперь рассмотрим строки входных строк, подобные приведенным ниже, которые не будут иметь решения:

101
101001
1010010001
101001000100001
101001000100001000001

В общем, это конкатенация k строк в форме j 0, за которыми следует 1 для j от нуля до k-1.

k=2  101
k=3  101001
k=4  1010010001
k=5  101001000100001
k=6  101001000100001000001

Обратите внимание, что длины подстрок равны 1, 2, 3 и т. Д. Итак, размер задачи n имеет подстроки длиной от 1 до k, такие что n = k (k + 1) /2.

k=2  n= 3  101
k=3  n= 6  101001
k=4  n=10  1010010001
k=5  n=15  101001000100001
k=6  n=21  101001000100001000001

Обратите внимание, что k также отслеживает количество единиц, которые мы должны рассмотреть. Помните, что каждый раз, когда мы видим 1, нам нужно учитывать все 1, увиденные до сих пор. Поэтому, когда мы видим вторую 1, мы рассматриваем только первую, когда мы видим третью 1, мы пересматриваем первые две, когда мы видим четвертую 1, нам нужно пересматривать первые три, и так далее. К концу алгоритма мы рассмотрели k (k-1) / 2 пары единиц. Назовите это p.

k=2  n= 3  p= 1  101
k=3  n= 6  p= 3  101001
k=4  n=10  p= 6  1010010001
k=5  n=15  p=10  101001000100001
k=6  n=21  p=15  101001000100001000001

Соотношение между n и p таково, что n = p + k.

Процесс прохождения строки занимает O (n) времени. Каждый раз, когда встречается 1, выполняется максимум (k-1) сравнений. Поскольку n = k (k + 1) / 2, n> k ** 2, то sqrt (n)> k. Это дает нам O (n sqrt (n)) или O (n ** 3/2). Тем не менее, обратите внимание, что это не может быть очень жесткой границей, потому что количество сравнений идет от 1 до максимум k, это не k за все время. Но я не уверен, как объяснить это в математике.

Это все еще не O (n log (n)). Кроме того, я не могу доказать, что эти данные являются худшими, хотя я подозреваю, что это так. Я думаю, что более плотная упаковка 1 в передней части приводит к еще более редкой упаковке в конце.

Так как кто-то может все еще найти это полезным, вот мой код для этого решения на Perl:

#!/usr/bin/perl

# read input as first argument
my $s = $ARGV[0];

# validate the input
$s =~ /^[01]+$/ or die "invalid input string\n";

# strip leading and trailing 0's
$s =~ s/^0+//;
$s =~ s/0+$//;

# prime the position list with the first '1' at position 0
my @p = (0);

# start at position 1, which is the second character
my $i = 1;

print "the string is $s\n\n";

while ($i < length($s)) {
   if (substr($s, $i, 1) eq '1') {
      print "found '1' at position $i\n";
      my @t = ();
      # assuming this is the middle '1', go through the positions
      # of all the prior '1's and check whether there's another '1'
      # in the correct position after this '1' to make a solution
      while (scalar @p) {
         # $p is the position of the prior '1'
         my $p = shift @p;
         # $j is the corresponding position for the following '1'
         my $j = 2 * $i - $p;
         # if $j is off the end of the string then we don't need to
         # check $p anymore
         next if ($j >= length($s));
         print "checking positions $p, $i, $j\n";
         if (substr($s, $j, 1) eq '1') {
            print "\nsolution found at positions $p, $i, $j\n";
            exit 0;
         }
         # if $j isn't off the end of the string, keep $p for next time
         push @t, $p;
      }
      @p = @t;
      # add this '1' to the list of '1' positions
      push @p, $i;
   }
   $i++;
}

print "\nno solution found\n";
0 голосов
/ 14 октября 2009

Может ли это быть решением?Я не уверен, что это O (nlogn), но, на мой взгляд, лучше, чем O (n²), потому что единственный способ не найти тройку - это распределение простых чисел.

Есть место для улучшения,второй найденный 1 может быть следующим первым 1. Также нет проверки ошибок.

#include <iostream>

#include <string>

int findIt(std::string toCheck) {
    for (int i=0; i<toCheck.length(); i++) {
        if (toCheck[i]=='1') {
            std::cout << i << ": " << toCheck[i];
            for (int j = i+1; j<toCheck.length(); j++) {
                if (toCheck[j]=='1' && toCheck[(i+2*(j-i))] == '1') {
                    std::cout << ", " << j << ":" << toCheck[j] << ", " << (i+2*(j-i)) << ":" << toCheck[(i+2*(j-i))] << "    found" << std::endl;
                    return 0;
                }
            }
        }
    }
    return -1;
}

int main (int agrc, char* args[]) {
    std::string toCheck("1001011");
    findIt(toCheck);
    std::cin.get();
    return 0;
}
...