Search
Write a publication
Pull to refresh

Comments 12

Чтобы подсчитывать быстро сумму на отрезке за O(log N) нужен простой советский дерево отрезков.

В функции Cheer используется поиск в std::map, который выполняется за логарифмическое время. Плюс не более 99 итераций для поиска в users_by_pages , это константа и при расчете сложности она не учитывается. Получаем тот же O(log N).

Ага, а N у нас - тысяча, ура, получили O(1)

Вы правы, мой алгоритм работает дольше, чем дерево отрезков. @xxxphilinxxx подробно пояснил, в чем была моя ошибка при подсчетах.

А можно было бы просто из "неограниченного числа читателей" вычесть число пользователей, прочитавших столько же страниц, сколько и заданный, но тогда бы не родилась статья :)

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

Промежуточные суммы вижу, а где хеширование из заголовка? В вычислении ключа этих сумм?

Тут выше упомянули дерево отрезков - вы его фактически и реализовали, просто зафиксировали кол-во уровней на трех (книга/сотни/страницы) и зафиксировали кол-во потомков на третьем уровне на сотне элементов.

А можно было бы просто из "неограниченного числа читателей" вычесть число пользователей, прочитавших столько же страниц, сколько и заданный, но тогда бы не родилась статья :)
По условиям задачи мы не знаем, сколько пользователей прочитало столько же страниц, что и искомый пользователь.

Если хранить счетчик пользователей, не читающих страницу, а уже прочитавших ее, то все как раз становится тривиально: total_book_readers - pages_read_count[current_pages[user_id]-1]. Даже Read станет проще, не нужно будет декрементить предыдущую страницу.

Плюс не более 99 итераций для поиска в users_by_pages , это константа и при расчете сложности она не учитывается.

99 итераций помимо остальных действий для вас несущественны, а в статье 1000 называете уже сильно замедляющими программу, хотя тут всего один порядок разницы.

Получаем тот же O(log N).

Логарифм у вас там в поиске по пользователям, а не по страницам; его, кстати, еще приплюсовать надо. К тому же у меня немного другая математика: если N - кол-во страниц, то кол-во чтений будет (N/100 + 99). Константы убираем и получаем вполне линейную из-за фиксации только на сотнях сложность O(N) безо всяких логарифмов. Вместе с пользователями O(N) + O(log U).

Еще в глаза бросилось, что в функции Read чтение из map "pages_by_users[user_id]" пять раз фигурирует в пределах одной функции, причем дважды по дважды в пределах одной строки. И сотни почему-то int[10], а не вектор, хотя книги ооочень длинными бывают - не встречали, например, "Червя" Джона Маккрея?

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

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

#include <iostream>
#include <iomanip>
#include <string>
#include <cstdlib>

struct node {
    node * nodes[ 10 ] = { nullptr, };
    unsigned users = { 0U };
    void addUser() { users++; };
    void removeUser() { if( users > 0U ) users--; };
};


struct book {
    node nodes[ 10 ];
    int l{ 0 };
    void usersPages( int page, node * n ) {
        if( n == nullptr )
            return;
        std::cout << std::setw(l++) << page << ":" << n->users << std::endl;
        for( int i = 0; i < 10; i++ ) {
            usersPages( i, n->nodes[ i ] );
        }
        l--;
    }

    void printUsersPages() {
        for( int i = 0; i < 10; i++ ) {
            usersPages( i, &nodes[ i ] );
        }
    }

    node * findPage( int page ) {
        std::string strPage = std::to_string( page );

        if( strPage.empty() )
            return nullptr;

        auto it = strPage.rbegin();
        int num = *it - '0';
        node * nodePage{ &nodes[ num ] };

        for( ; it != strPage.rend(); ++it ) {
            num = *it - '0';
            if( nodePage->nodes[ num ] == nullptr )
                nodePage->nodes[ num ] = new node;
            nodePage = nodePage->nodes[ num ];
        }

        return nodePage;
    }

    void addUser( int page ) {
        if( auto nodePage = findPage( page ) ) {
            nodePage->addUser();
        }
    }

    void removeUser( int page ) {
        if( auto nodePage = findPage( page ) ) {
            nodePage->removeUser();
        }
    }

};

int main() {

    book foo;

    int page = 7641063;

    if( auto nodePage = foo.findPage( page ) ) {
        nodePage->addUser();
        nodePage->addUser();
        nodePage->removeUser();
        nodePage->addUser();
        std::cout << page << ":" << nodePage->users << std::endl << std::endl;
    }

    page = 1001;

    if( auto nodePage = foo.findPage( page ) ) {
        nodePage->addUser();
        nodePage->removeUser();
        nodePage->addUser();
        std::cout << page << ":" << nodePage->users << std::endl << std::endl;
    }

    page = 1111;

    if( auto nodePage = foo.findPage( page ) ) {
        nodePage->addUser();
        nodePage->removeUser();
        nodePage->addUser();
        std::cout << page << ":" << nodePage->users << std::endl << std::endl;
    }

    foo.printUsersPages();

    return 0;
}

*извиняюсь за размер картинки

Наивным решением было бы хранить в std::map<int,int> в качестве ключа номера страниц, в качестве значения- количество прочитавших их пользователей

Если все равно есть актуальная информация о том, какие страницы прочитал каждый пользователь, то наивное решение — multiset<int> — хранить, сколько страниц прочитал каждый пользователь и просто считать, сколько там чисел не больше заданного. Хоть бы и через std::distance. Будет за линию от количества пользователей, но зато очень просто в реализации и концептуально.


Стандартная реализация балансированых деревьев в set'ах в С++, к сожалению, не умеет считать количество ключей меньше заданного за логарифм, но в балансированых деревьях поиска это можно сделать. Можно или написать свое дерево или воспользоваться чуть другой структурой — дервевом отрезков.


И еще, очень глаз режет вот этот код:


((pages_by_users[user_id] - (pages_by_users[user_id] % 100)) + 100)/100

его можно заменить на


pages_by_users[user_id] / 100 + 1

И еще неясно, зачем там +100 (+1 в сокращенном коде) вообще нужно.
У вас там несоклько раз такие излишне сложные конструкции встречаются.


А еще, где у вас там хеширование из заголовка? Вот такое вот разделение данных на группы (по сотням) — это скорее шардирование или аггрегирование. Или что-то типа Sqrt-декомпозиции (по уму, надо делить не на сотни, а на куски длиной sqrt(n)). Хешированием тут и не пахнет.

Деление на группы по сотням - скорее смахивает на корневую оптимизацию.

корневую оптимизацию.

По-моему, мы об одном и том же говорим. Это может быть то, что я назвал "sqrt-декомпозицией"? Разбиваем N элементов на K=sqrt(N) групп. При запросе обходим группы за O(k) и возможно элементы в одной или двух группах за O(N/k)?

Sign up to leave a comment.

Articles