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

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

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

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

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

Ответы [ 31 ]

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

Предположение:

Просто неправильно, говоря о log (n) количестве верхних пределов единиц

EDIT:

Теперь я обнаружил, что, используя числа Кантора (если они верны), плотность на множестве равна (2/3) ^ Log_3 (n) (что за странная функция), и я согласен, плотность log (n) / n слишком сильная.

Если это верхний предел, существует алгоритм, который решает эту проблему как минимум за O (n * (3/2) ^ (log (n) / log (3))) сложность времени и O ((3/2 ) ^ (log (n) / log (3))) сложность пространства. (проверьте ответ юстиции на algorhitm)

Это все же намного лучше, чем O (n ^ 2)

Эта функция ((3/2) ^ (log (n) / log (3))) действительно выглядит как n * log (n) на первый взгляд.

Как я получил эту формулу?

Наложение номера Кантора на строку.
Предположим, что длина строки составляет 3 ^ p == n
На каждом этапе генерации строки Кантора вы сохраняете 2/3 от общего числа единиц. Примените это p раз.

Это значит (n * ((2/3) ^ p)) -> (((3 ^ p)) * ((2/3) ^ p)) оставшихся и после упрощения 2 ^ p. Это означает 2 ^ p единиц в 3 ^ p строке -> (3/2) ^ p единиц. Подставьте p = log (n) / log (3) и получите
((3/2) ^ (log (n) / log (3)))

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

Во время сканирования 1 с, добавьте их позиции в список. При добавлении второй и последующих 1, сравните их с каждой позицией в списке до сих пор. Интервал равен currentOne (в центре) - previousOne (слева). Бит правой стороны - currentOne + интервал. Если это 1, конец.

Список из них растет обратно пропорционально расстоянию между ними. Проще говоря, если у вас много нулей между 1 (как в худшем случае), ваш список известных 1 будет расти довольно медленно.

using System;
using System.Collections.Generic;

namespace spacedOnes
{
    class Program
    {
        static int[] _bits = new int[8] {128, 64, 32, 16, 8, 4, 2, 1};

        static void Main(string[] args)
        {
            var bytes = new byte[4];
            var r = new Random();
            r.NextBytes(bytes);
            foreach (var b in bytes) {
                Console.Write(getByteString(b));
            }
            Console.WriteLine();
            var bitCount = bytes.Length * 8;
            var done = false;
            var onePositions = new List<int>();
            for (var i = 0; i < bitCount; i++)
            {
                if (isOne(bytes, i)) {
                    if (onePositions.Count > 0) {
                        foreach (var knownOne in onePositions) {
                            var spacing = i - knownOne;
                            var k = i + spacing;
                            if (k < bitCount && isOne(bytes, k)) {
                                Console.WriteLine("^".PadLeft(knownOne + 1) + "^".PadLeft(spacing) + "^".PadLeft(spacing));
                                done = true;
                                break;
                            }
                        }
                    }
                    if (done) {
                        break;
                    }
                    onePositions.Add(i);
                }
            }
            Console.ReadKey();
        }

        static String getByteString(byte b) {
            var s = new char[8];
            for (var i=0; i<s.Length; i++) {
                s[i] = ((b & _bits[i]) > 0 ? '1' : '0');
            }
            return new String(s);
        }

        static bool isOne(byte[] bytes, int i)
        {
            var byteIndex = i / 8;
            var bitIndex = i % 8;
            return (bytes[byteIndex] & _bits[bitIndex]) > 0;
        }
    }
}
0 голосов
/ 14 октября 2009

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

При сдвиге вы сравниваете строку единиц и нулей со сдвинутой версией самого себя. Затем вы берете соответствующие. Возьмем этот пример, сдвинутый на два:

1010101010
  1010101010
------------
001010101000

Результирующие 1 (поразрядное AND) должны представлять все те 1, которые равномерно разделены двумя. Тот же пример сдвинут на три:

1010101010
   1010101010
-------------
0000000000000

В этом случае нет 1, которые равномерно распределены на три.

Так что это говорит вам? Хорошо, что вам нужно только проверить сдвиги, которые являются простыми числами. Например, скажем, у вас есть два 1, которые шесть друг от друга. Вам нужно будет только проверить «две» смены и «три» смены (поскольку они делят шесть). Например:

10000010 
  10000010 (Shift by two)
    10000010
      10000010 (We have a match)

10000010
   10000010 (Shift by three)
      10000010 (We have a match)

Таким образом, единственные сдвиги, которые вам когда-либо нужно проверять, это 2,3,5,7,11,13 и т. Д. До простого, ближайшего к квадратному корню, размера строки цифр.

Почти решен?

Я думаю, что я ближе к решению. В основном:

  1. Сканирование строки на 1. Для каждой 1 ноты это остаток после взятия модуля его положения. Модуль колеблется от 1 до половины размера струны. Это потому, что максимально возможный размер разделения составляет половину строки. Это сделано в O (n ^ 2). НО. Необходимо проверять только простые модули, поэтому O (n ^ 2 / log (n))
  2. Сначала отсортируйте список модулей / остатков в порядке наибольшего модуля, это можно сделать за время O (n * log (n)).
  3. Найдите три последовательных модуля / остатка, которые являются одинаковыми.
  4. Каким-то образом восстановите положение тех!

Я думаю, что самый большой ключ к ответу - это то, что самые быстрые алгоритмы сортировки - это O (n * log (n)).

НЕВЕРНО

Шаг 1 неверен, как указал коллега. Если бы у нас были единицы в позициях 2, 12 и 102. Тогда, взяв модуль 10, они все имели бы одинаковые остатки, и все же не были бы одинаково разнесены! К сожалению.

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

Это можно решить за линейное время O (n)

  1. начать с начала и ждать первого 1
  2. Начните считать нули.
  3. Когда вы нажмете на 1 магазин количество подсчитанных нулей (действительное число также 0) NumberOfZeros -> PrevZeros
  4. Начните считать нули.
  5. Когда вы нажимаете 1, отметьте NumberOfZeros == PrevZeros

    Если true счетчик возврата

    else NumberOfZeros -> prev_zeros и goto 4

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

Я предполагаю, что причина этого nlog (n) заключается в следующем:

  • Чтобы найти 1, который является началом триплета, вам нужно проверить (n-2) символов. Если вы не нашли его к этому моменту, вы не сможете (символы n-1 и n не могут запустить триплет) (O (n))
  • Чтобы найти вторую 1, являющуюся частью триплета (начиная с первой), вам нужно проверить m / 2 (m = n-x, где x - смещение первых 1) символов. Это потому, что если вы не нашли вторую 1 к тому времени, когда вы на полпути от первой к концу, вы не будете ... так как третья 1 должна быть точно на том же расстоянии, что и вторая. (O (журнал (п)))
  • Это O (1), чтобы найти последние 1, так как вы знаете индекс, по которому они должны быть к моменту, когда вы найдете первое и второе.

Итак, у вас есть n, log (n) и 1 ... O (nlogn)

Редактировать: Упс, мой плохой. В моем мозгу было установлено, что n / 2 было logn ... чего, очевидно, нет (удвоение числа элементов по-прежнему удваивает число итераций во внутреннем цикле). Это все еще на п ^ 2, не решая проблему. Ну, по крайней мере, я должен написать код:)


Реализация в Tcl

proc get-triplet {input} {
    for {set first 0} {$first < [string length $input]-2} {incr first} {
        if {[string index $input $first] != 1} {
            continue
        }
        set start [expr {$first + 1}]
        set end [expr {1+ $first + (([string length $input] - $first) /2)}]
        for {set second $start} {$second < $end} {incr second} {
            if {[string index $input $second] != 1} {
                continue
            }
            set last [expr {($second - $first) + $second}]
            if {[string index $input $last] == 1} {
                return [list $first $second $last]
            }
        }
    }
    return {}
}

get-triplet 10101      ;# 0 2 4
get-triplet 10111      ;# 0 2 4
get-triplet 11100000   ;# 0 1 2
get-triplet 0100100100 ;# 1 4 7
0 голосов
/ 17 октября 2009

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

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

-

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

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

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

-

Причина, по которой я думаю, что алгоритм будет работать, заключается в том, что последовательности без действительных триплетов, кажется, чередуются между группами 1 и партиями 0. Он эффективно разделяет близлежащее пространство поиска, и дерево эмулирует это разбиение.

Время выполнения алгоритма совсем не очевидно. Он опирается на нетривиальные свойства последовательности. Если 1 действительно редки, то наивный алгоритм будет работать в рамках бюджета. Если 1 плотные, то совпадение должно быть найдено сразу. Но если плотность «просто правильная» (например, около ~ n ^ 0,63, чего можно добиться, установив все биты в позиции без цифры «2» в базе 3), я не знаю, будет ли это работать. Вы должны доказать, что эффект расщепления достаточно силен.

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

Я думаю, что нашел способ решения проблемы, но я не могу построить формальное доказательство. Решение, которое я принял, написано на Java и использует счетчик 'n' для подсчета количества обращений к списку / массиву. Так что n должно быть меньше или равно stringLength * log (stringLength), если это правильно. Я пробовал это для чисел от 0 до 2 ^ 22, и это работает.

Он начинается с перебора входной строки и составления списка всех индексов, которые содержат единицу. Это просто O (n).

Затем из списка индексов он выбирает firstIndex и secondIndex, который больше первого. Эти два индекса должны содержать индексы, потому что они находятся в списке индексов. Оттуда может быть рассчитан третий индекс. Если inputString [thirdIndex] равен 1, то он останавливается.

public static int testString(String input){
//n is the number of array/list accesses in the algorithm
int n=0;

//Put the indices of all the ones into a list, O(n)
ArrayList<Integer> ones = new ArrayList<Integer>();
for(int i=0;i<input.length();i++){
    if(input.charAt(i)=='1'){
        ones.add(i);
    }
}

//If less than three ones in list, just stop
if(ones.size()<3){
    return n;
}

int firstIndex, secondIndex, thirdIndex;
for(int x=0;x<ones.size()-2;x++){
    n++;
    firstIndex = ones.get(x);

    for(int y=x+1; y<ones.size()-1; y++){
        n++;
        secondIndex = ones.get(y);
        thirdIndex = secondIndex*2 - firstIndex;

        if(thirdIndex >= input.length()){
            break;
        }

        n++;
        if(input.charAt(thirdIndex) == '1'){
            //This case is satisfied if it has found three evenly spaced ones
            //System.out.println("This one => " + input);
            return n;
        }
    }
}

return n;

}

дополнительное примечание: счетчик n не увеличивается, когда он перебирает входную строку для построения списка индексов. Эта операция O (n), поэтому она не повлияет на сложность алгоритма.

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

Здесь нет теоретического ответа, но я написал быструю Java-программу для изучения поведения во время выполнения как функции k и n, где n - общая длина бита, а k - число единиц. Я с несколькими из ответчиков, которые говорят, что «обычный» алгоритм, который проверяет все пары позиций битов и ищет 3-й бит, хотя для этого потребуется O (k ^ 2) в худшем случае, в реальность, потому что в худшем случае нужны разреженные цепочки битов, это O (n ln n).

В любом случае, вот программа ниже. Это программа в стиле Монте-Карло, которая запускает большое количество испытаний NTRIALS для константы n и случайным образом генерирует наборы битов для диапазона значений k, используя процессы Бернулли с ограничениями плотности единиц, которые могут быть заданы, и записывает время выполнения. найти или не найти триплет из равномерно распределенных, время измеряется в шагах, а не во времени ЦП. Я запустил его для n = 64, 256, 1024, 4096, 16384 * (все еще выполняется), сначала тестовый запуск с 500000 испытаний, чтобы увидеть, какие значения k занимают самое продолжительное время работы, затем другой тест с 5000000 испытаниями с суженными сосредоточиться на плотности, чтобы увидеть, как эти значения выглядят. Самое длинное время работы происходит при очень разреженной плотности (например, для n = 4096 пики времени работы находятся в диапазоне k = 16-64, с небольшим пиком для среднего времени работы при 4212 шагах при k = 31, максимальное время работы достигло максимума при 5101 шаги @ k = 58). Похоже, что для шага O (k ^ 2) в худшем случае потребовалось бы чрезвычайно большое значение N, чтобы стать больше шага O (n), когда вы сканируете цепочку битов, чтобы найти индексы позиции 1.

package com.example.math;

import java.io.PrintStream;
import java.util.BitSet;
import java.util.Random;

public class EvenlySpacedOnesTest {
    static public class StatisticalSummary
    {
        private int n=0;
        private double min=Double.POSITIVE_INFINITY;
        private double max=Double.NEGATIVE_INFINITY;
        private double mean=0;
        private double S=0;

        public StatisticalSummary() {}
        public void add(double x) {
            min = Math.min(min, x);
            max = Math.max(max, x);
            ++n;
            double newMean = mean + (x-mean)/n;
            S += (x-newMean)*(x-mean);
            // this algorithm for mean,std dev based on Knuth TAOCP vol 2
            mean = newMean;
        }
        public double getMax() { return (n>0)?max:Double.NaN; }
        public double getMin() { return (n>0)?min:Double.NaN; }
        public int getCount() { return n; }
        public double getMean() { return (n>0)?mean:Double.NaN; }
        public double getStdDev() { return (n>0)?Math.sqrt(S/n):Double.NaN; } 
        // some may quibble and use n-1 for sample std dev vs population std dev    
        public static void printOut(PrintStream ps, StatisticalSummary[] statistics) {
            for (int i = 0; i < statistics.length; ++i)
            {
                StatisticalSummary summary = statistics[i];
                ps.printf("%d\t%d\t%.0f\t%.0f\t%.5f\t%.5f\n",
                        i,
                        summary.getCount(),
                        summary.getMin(),
                        summary.getMax(),
                        summary.getMean(),
                        summary.getStdDev());
            }
        }
    }

    public interface RandomBernoulliProcess // see http://en.wikipedia.org/wiki/Bernoulli_process
    {
        public void setProbability(double d);
        public boolean getNextBoolean();
    }

    static public class Bernoulli implements RandomBernoulliProcess
    {
        final private Random r = new Random();
        private double p = 0.5;
        public boolean getNextBoolean() { return r.nextDouble() < p; }
        public void setProbability(double d) { p = d; }
    }   
    static public class TestResult {
        final public int k;
        final public int nsteps;
        public TestResult(int k, int nsteps) { this.k=k; this.nsteps=nsteps; } 
    }

    ////////////
    final private int n;
    final private int ntrials;
    final private double pmin;
    final private double pmax;
    final private Random random = new Random();
    final private Bernoulli bernoulli = new Bernoulli();
    final private BitSet bits;
    public EvenlySpacedOnesTest(int n, int ntrials, double pmin, double pmax) {
        this.n=n; this.ntrials=ntrials; this.pmin=pmin; this.pmax=pmax;
        this.bits = new BitSet(n);
    }

    /*
     * generate random bit string
     */
    private int generateBits()
    {
        int k = 0; // # of 1's
        for (int i = 0; i < n; ++i)
        {
            boolean b = bernoulli.getNextBoolean();
            this.bits.set(i, b);
            if (b) ++k;
        }
        return k;
    }

    private int findEvenlySpacedOnes(int k, int[] pos) 
    {
        int[] bitPosition = new int[k];
        for (int i = 0, j = 0; i < n; ++i)
        {
            if (this.bits.get(i))
            {
                bitPosition[j++] = i;
            }
        }
        int nsteps = n; // first, it takes N operations to find the bit positions.
        boolean found = false;
        if (k >= 3) // don't bother doing anything if there are less than 3 ones. :(
        {       
            int lastBitSetPosition = bitPosition[k-1];
            for (int j1 = 0; !found && j1 < k; ++j1)
            {
                pos[0] = bitPosition[j1];
                for (int j2 = j1+1; !found && j2 < k; ++j2)
                {
                    pos[1] = bitPosition[j2];

                    ++nsteps;
                    pos[2] = 2*pos[1]-pos[0];
                    // calculate 3rd bit index that might be set;
                    // the other two indices point to bits that are set
                    if (pos[2] > lastBitSetPosition)
                        break;
                    // loop inner loop until we go out of bounds

                    found = this.bits.get(pos[2]);
                    // we're done if we find a third 1!
                }
            }
        }
        if (!found)
            pos[0]=-1;
        return nsteps;
    }

    /*
     * run an algorithm that finds evenly spaced ones and returns # of steps.
     */
    public TestResult run()
    {
        bernoulli.setProbability(pmin + (pmax-pmin)*random.nextDouble());
        // probability of bernoulli process is randomly distributed between pmin and pmax

        // generate bit string.
        int k = generateBits();
        int[] pos = new int[3];
        int nsteps = findEvenlySpacedOnes(k, pos);
        return new TestResult(k, nsteps); 
    }

    public static void main(String[] args)
    {
        int n;
        int ntrials;
        double pmin = 0, pmax = 1;
        try {
            n = Integer.parseInt(args[0]);
            ntrials = Integer.parseInt(args[1]);
            if (args.length >= 3)
                pmin = Double.parseDouble(args[2]);
            if (args.length >= 4)
                pmax = Double.parseDouble(args[3]);
        }
        catch (Exception e)
        {
            System.out.println("usage: EvenlySpacedOnesTest N NTRIALS [pmin [pmax]]");
            System.exit(0);
            return; // make the compiler happy
        }

        final StatisticalSummary[] statistics;
        statistics=new StatisticalSummary[n+1];
        for (int i = 0; i <= n; ++i)
        {
            statistics[i] = new StatisticalSummary();
        }

        EvenlySpacedOnesTest test = new EvenlySpacedOnesTest(n, ntrials, pmin, pmax);
        int printInterval=100000;
        int nextPrint = printInterval;
        for (int i = 0; i < ntrials; ++i)
        {
            TestResult result = test.run();
            statistics[result.k].add(result.nsteps);
            if (i == nextPrint)
            {
                System.err.println(i);
                nextPrint += printInterval;
            }
        }
        StatisticalSummary.printOut(System.out, statistics);
    }
}
0 голосов
/ 15 октября 2009

Я думаю, что этот алгоритм имеет O (n log n) сложность (C ++, DevStudio 2k5). Теперь я не знаю деталей того, как анализировать алгоритм, чтобы определить его сложность, поэтому я добавил в код некоторую информацию для сбора метрик. Код подсчитывает количество тестов, выполненных на последовательности 1 и 0 для любого заданного ввода (надеюсь, я не сделал шарики алгоритма). Мы можем сравнить фактическое количество тестов со значением O и посмотреть, есть ли корреляция.

#include <iostream>
using namespace std;

bool HasEvenBits (string &sequence, int &num_compares)
{
  bool
    has_even_bits = false;

  num_compares = 0;

  for (unsigned i = 1 ; i <= (sequence.length () - 1) / 2 ; ++i)
  {
    for (unsigned j = 0 ; j < sequence.length () - 2 * i ; ++j)
    {
      ++num_compares;
      if (sequence [j] == '1' && sequence [j + i] == '1' && sequence [j + i * 2] == '1')
      {
        has_even_bits = true;
        // we could 'break' here, but I want to know the worst case scenario so keep going to the end
      }
    }
  }

  return has_even_bits;
}

int main ()
{
  int
    count;

  string
    input = "111";

  for (int i = 3 ; i < 32 ; ++i)
  {
    HasEvenBits (input, count);
    cout << i << ", " << count << endl;
    input += "0";
  }
}

Эта программа выводит количество тестов для каждой длины строки до 32 символов. Вот результаты:

 n  Tests  n log (n)
=====================
 3     1     1.43
 4     2     2.41
 5     4     3.49
 6     6     4.67
 7     9     5.92
 8    12     7.22
 9    16     8.59
10    20    10.00
11    25    11.46
12    30    12.95
13    36    14.48
14    42    16.05
15    49    17.64
16    56    19.27
17    64    20.92
18    72    22.59
19    81    24.30
20    90    26.02
21   100    27.77
22   110    29.53
23   121    31.32
24   132    33.13
25   144    34.95
26   156    36.79
27   169    38.65
28   182    40.52
29   196    42.41
30   210    44.31
31   225    46.23

Я также добавил значения 'n log n'. Нанесите их на график с помощью выбранного графического инструмента, чтобы увидеть корреляцию между двумя результатами. Распространяется ли этот анализ на все значения n? Я не знаю.

0 голосов
/ 18 октября 2009
# <algorithm>
def contains_evenly_spaced?(input)
  return false if input.size < 3
  one_indices = []
  input.each_with_index do |digit, index|
    next if digit == 0
    one_indices << index
  end
  return false if one_indices.size < 3
  previous_indexes = []
  one_indices.each do |index|
    if !previous_indexes.empty?
      previous_indexes.each do |previous_index|
        multiple = index - previous_index
        success_index = index + multiple
        return true if input[success_index] == 1
      end
    end
    previous_indexes << index
  end
  return false
end
# </algorithm>

def parse_input(input)
  input.chars.map { |c| c.to_i }
end

У меня проблемы с наихудшими сценариями с миллионами цифр. Fuzzing от /dev/urandom по существу дает вам O (n), но я знаю, что худший случай хуже этого. Я просто не могу сказать, насколько хуже. Для малых n тривиально найти входные данные на уровне 3*n*log(n), но на удивление трудно отличить их от некоторого другого порядка роста для этой конкретной проблемы.

Может ли кто-нибудь, кто работал с входными данными для наихудшего случая, сгенерировать строку длиной, скажем, сто тысяч?

...