как сделать время выполнения меньше (т.е. быстрее кода) для этой проблемы - PullRequest
2 голосов
/ 05 сентября 2010

этот вопрос от Codechef.com [если кто-то все еще решает этот вопрос, не заглядывайте дальше в пост, прежде чем пытаться самостоятельно], и хотя он работает правильно, но мне нужно сделать это намного быстрее. Я новичок в , C ++. (Я знаю вещи, вплоть до массивов, строк и указателей, но не обработки файлов и т. д.) Таким образом, есть способ заставить эту программу работать быстрее, не делая ее сложной (это нормально, если он сложен в алгоритме). Я тоже приму сложное кодирование, если вы упомянули, за какой книгой вы следовали, где все это дано :). В настоящее время я следую за Робертом Лафором. Вот программа: -

Есть N чисел a [0], a [1] .. a [N - 1]. Изначально все равны 0. Вы должны выполнить два типа операций:

1) Увеличить числа между индексами A и B на 1. Это представлено командой "0 A B"

2) Ответьте, сколько чисел между индексами A и B делится на 3. Это представляется командой "1 A B".

Ввод:

Первая строка содержит два целых числа, N и Q. Каждая из следующих Q строк имеет вид "0 A B" или "1 A B", как указано выше.

Выход:

Вывести 1 строку для каждого из запросов формы «1 A B», содержащую требуемый ответ для соответствующего запроса.

Пример ввода:

4 7
1 0 3
0 1 2
0 1 3
1 0 0
0 0 3
1 3 3
1 0 3

Пример вывода:

4
1
0
2

Ограничения:

1 <= N <= 100000
1 <= Q <= 100000
0 <= A <= B <= N - 1

ЗДЕСЬ МОЕ РЕШЕНИЕ: -

#include<stdio.h>

int main()
{
    unsigned int n; //amount of numbers taken
    scanf("%u",&n);

    unsigned int arr[n],q,a,b,count=0,j,i;
    short int c;
    scanf("%u",&q);//cases taken  
    for(i=0;i<n;++i)
    {
      arr[i]=0;
    }    


    for(i=0;i<q;++i)
    {
      scanf("%d%u%u",&c,&a,&b);//here a,b are A and B respectively and c is either 
                               //o or 1

      if(c==0)
      {
        for(j=a;j<=b;++j)
          arr[j]++;
      }


      if(c==1)
      {
        for(j=a;j<=b;++j)
        {
          if(arr[j]%3==0)
            count++;
        }
       printf("%u\n",count);
      }
      count=0;
    }  

}

Ответы [ 7 ]

2 голосов
/ 05 сентября 2010

Две очень простые оптимизации:

В массиве можно хранить только значение по модулю 3, а не фактическое значение.

Увеличение может быть выполнено с помощью простой таблицы поиска (чтобы избежать сравнений и ответвлений):

char increment[3] = { 1, 2 ,0 };
new_val = increment[old_val];

Тестирование на 3-делимость теперь сравнивается с 0 - что значительно быстрее, чем целочисленное деление.

2 голосов
/ 05 сентября 2010

1) Увеличить числа между индексами A и B на 1. Это представлено командой "0 A B"

2) Ответьте, сколько чисел между индексами A и B делится на 3. Это представляется командой "1 A B".

Первоначально числа равны 0 и, следовательно, делятся на 3. Увеличение на единицу делает число неразделимым. Следующий шаг - число, которое еще не делится. Третий шаг делает число снова делимым.

Первая оптимизация, которую можно попробовать, состоит в том, чтобы не допустить, чтобы число росло выше 2: если во время приращения число увеличивается от 2 до 3, установите его обратно в ноль. Теперь поиск диапазона становится простым сравнением с 0. (Таким образом, массив будет содержать вместо номера его модуль 3).

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

struct extent {
   int start; /* 0 .. N-1; extent should have at least one number */
   int end;   /* 0 .. N   */
   int n;     /* 0, 1, 2; we are only interested in the result of % */
};

Первоначально массив будет содержать один экстент, охватывающий все числа {0, N, 0}. На шаге увеличения диапазон может быть разделен или объединен с соседним. Это представление ускорит подсчет чисел, так как вы будете проходить по массиву не один за другим, а по частям. (Это все равно ухудшит линейный поиск, если все диапазоны содержат только один элемент.)


Другим подходом может быть использование вместо массива трех множеств с индексами. Набор № 0 будет содержать все числа чисел, чей модуль 3 равен 0, набор № 1 - 1, набор № 2 - 2. Поскольку во время операции приращения нам нужно выполнить поиск, вместо std::set лучше использовать например std::bitset с каждым битом, отмечающим индекс числа, принадлежащего набору.

N.B. Таким образом, мы не сохраняем исходные цифры вообще. Мы неявно храним только результат по модулю 3.

Во время приращения нам нужно найти, к какому набору принадлежит индекс, например, установите #n и переместите указатель на следующий набор (мод 3): установите бит в ноль в наборе n, установите бит в 1 в наборе n + 1 (mod 3). Подсчет числа, делимого на 3, теперь так же прост, как и подсчет ненулевых битов в наборе # 0. Этого можно достичь, создав временную маску std::bitset в виде маски с битами в диапазоне [A,B], установленными в единицу, маскируя с помощью временной установки значение 0 и вызвав std::bitset::count() для полученного набора битов.

1 голос
/ 05 сентября 2010

Одно улучшение, которое вы можете сделать, это заменить

if (c == 0) {
    //code here
}

if (c == 1) {
   // code here
}

с:

if (c == 0) {
   //...
} else if (c == 1) {
  //...
}

если вы точно знаете, что c всегда будет 0 или 1, вы также можете заменить else if на простой else.

Что действительно замедляет вас, так это ввод / вывод. Если это достаточно большой список, он может заплатить malloc достаточно памяти для хранения ввода и вывода. Затем соберите все входные данные перед входом в цикл и отобразите выходные данные в конце.

0 голосов
/ 05 сентября 2010

Это довольно сложно, но оставайся со мной.Я не могу привести ни одного конкретного места, откуда я получил это, кроме «27 лет опыта кодирования».

В исходной задаче числовая строка установлена ​​в натуральные целые числа 0,1,2,3,4,5,6 ... Однако мы заботимся только о числах, которые делятся на 3, поэтому давайте переопределим нашу числовую строку, чтобы в ней было только три значения: {2,3,4} и переназначим числовую строку так, чтобы:

0 => 4
1 => 2
2 => 3
3 => 4
4 => 2
5 => 3
6 => 4
.. и т. д.

Вы заметите, что числа, которые делятся на 3, отображаются в 4 в нашей последовательности.Зачем использовать {2,3,4}?Значение 4 равно 100 в двоичном виде, что означает, что любой элемент с его третьим установленным битом будет делиться на 3. Это легко проверить с помощью битовых операций.

Поскольку мы используем 2,3,4 в качестве триной последовательностимы можем уменьшить размер нашего элемента массива до 4 бит.Мы определим массив как 8-битные значения, но вдвое меньше необходимого нам размера в байтах (плюс 1 в случае, если это массив нечетного размера), и сохраним в массиве по два элемента.Операции добавления и сравнения можно выполнять как операции SIMD (одиночная инструкция, несколько данных), увеличивая или проверяя до 16 элементов на одну итерацию цикла, используя некоторые умные битовые операции.

В этом и заключается концепция.Теперь перейдем к коду.

Сначала нам нужно выделить и инициализировать наш массив.

unsigned char *arr = malloc(n/2 + 1);

// Init all element values to 4:
memset(&arr, 0x44, n/2 + 1);

Мы собираемся увеличивать 16 элементов за один раз, приведя блок из 8 массивов.байтов к uint_64, добавьте 0x1111111111111111 и затем перейдите к следующему блоку.Повторите с 32-битной, 16-битной, 8-битной и 4-битной математикой, чтобы до 8, 4, 2 или 1 осталось до конца операции.

Перед каждым шагом все, что имеетзначение 4 должно быть уменьшено на 3 перед приращением, чтобы числа оставались в правильном положении.

Вот код (не проверенный) для команды приращения:

/**
   @param p
      p is the address of the byte with the first aligned element to be incremented, &arr[A/2] when A is even, &arr[A/2]+1 when A is odd.
   @param j
      j is the number of elements to increment.  (B-A) when A is even, (B-A-1) when A is odd.
 */ 
void increment_aligned_block(unsigned char *p, int j)
    uint64_t fours;

    while (j>16) {
       // Find the ones that are value 4
       fours = *p & 0x4444444444444444;
       // Decrement each that matches by 3
       *p -= (fours >> 1 | fours >> 2);

       // Add 1 to each of the 16 array elements in the block.
       (uint64_t)(*p) += 0x1111111111111111;
       p += 8; j -= 16;
    }
    if (j >= 8) {
        // repeat the above for 32-bits (8 elements)
        // left as an exercise for the reader.
        p += 4; j -= 8;
   }
    if (j >= 4) {
        // repeat the above for 16-bits (4 elements)
        // left as an exercise for the reader.
        p += 2; j -= 4;
    }
    if (j >= 2) {
        // repeat the above for 8-bits (2 elements)
        // left as an exercise for the reader.
        p += 1; j -= 2;
    }
    if (j == 1) {
        // repeat the above for 8-bits (1 elements)
        // left as an exercise for the reader.
    }
}

Для сравненияuse:

/**
   @param p
      p is the address of the byte with the first aligned element to be counted, &arr[A/2] when A is even, &arr[A/2]+1 when A is odd.
   @param j
      j is the number of elements to count.  (B-A) when A is even, (B-A-1) when A is odd.
 */ 
int count_aligned_block(unsigned char *p, int j)
    int count = 0;
    uint64_t divisible_map;

    while (j > 16) {
        // Find the values of 4 in the block
        divisible_map = (uint64_t)(*p) & 0x4444444444444444;

        // Count the number of 4s in the block,
        // 8-bits at a time
        while (divisible_map) {
          switch (divisible_map & 0x44) {
            case 0x04:
            case 0x40:
                count++;
                break;
            case 0x44:
                count += 2;
                break;
            default:
                break;
          }
          divisible_map >>= 8;
        }
    }
    // Repeat as above with 32, 16, 8 and 4-bit math.
    // Left as an exercise to the reader

    return count;
}

Возможно, вы заметили, что функции называются foo_aligned_block, а p должен быть байтом первого выровненного элемента.Что это такое?Поскольку мы упаковываем два элемента на байт, индекс начального элемента должен быть выровнен по четному числу.Если команда в файле 0 0 30, то мы можем вызвать increment_algined_block(&arr[A/2], 30), без проблем.Однако, если команда в файле - 0 1 30, нам нужен дополнительный код для обработки первого элемента без выравнивания по индексу 1, а затем вызов increment_aligned_block(&arr[A/2 + 1], 29).Опять же, оставлено в качестве упражнения для читателя.


Хочу отметить, что это не самый оптимальный вариант.

Нераспределенный доступ обычно довольно дорогой.То есть чтение 8-байтового значения из 8-байтового выровненного адреса происходит быстрее, чем из не выровненного адреса.Мы можем добавить дополнительные оптимизации только к вызову foo_aligned_block(), так что все обращения будут гарантированно выровнены.

0 голосов
/ 05 сентября 2010

Это выглядит довольно продуктивно для меня. Одна вещь, которую я вижу, это то, что вы используете массивы переменной длины , это нормально для C (AFAIK), но недопустимо в C ++, для C ++ вам нужно будет использовать std::vector или new up массив.

Единственное место, где я могу видеть, как вы улучшаете свои показатели, - это использование устройства Даффа для частичного раскручивания петли, которое я бы не рекомендовал для образца игрушки.

0 голосов
/ 05 сентября 2010

Я думаю, что ваше решение отлично, за исключением отсутствия проверки границ.Возможно, используйте 'else' при проверке 'c' или переключателя, но это сэкономит вам тривиальное количество времени.Я не думаю, что вы найдете что-то столь бесполезное, как это, в любой книге.

0 голосов
/ 05 сентября 2010

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

-Alex

...