Блог пользователя oversolver

Автор oversolver, 3 месяца назад, По-английски

I am using -Werror=uninitialized locally in g++ args. It prevents me from bugs like this

int x;
++x;

But today I realized that my typical code of dfs on tree passes this check

vector<int> tin(n);
int tn;
function<void(int, int)> go = [&](int v, int p) {
    tin[v] = tn++;
    ...
};

So after playing with lines of code I came to this successfully compiled code

int n = 100;
vector<vector<int>> g(n);
int tn;
++tn; //HELLO WHERE IS MY CE???
function<void()> f = [&] {
    cout << tn << endl;
};
f();

What interesting is that commenting vector g will result a compilation error. Replacing std::function to lambda will result a compilation error. Moving g under ++tn will result a compilation error...

I am very tilted now. Mike please add g++14 with "deducing this" support...

Полный текст и комментарии »

  • Проголосовать: нравится
  • +30
  • Проголосовать: не нравится

Автор oversolver, 10 месяцев назад, По-английски

Considering all the color revolutions my last blue point was after Codeforces Beta Round 62

Полный текст и комментарии »

  • Проголосовать: нравится
  • +88
  • Проголосовать: не нравится

Автор oversolver, 20 месяцев назад, По-английски

I like to build my cp library. I am not aiming to fill it by each algo I have met, so content looks poor. Instead I am focusing on frequently used features or implement convenient api for powerful methods.

Also I like to rate cp libraries of others and currently I noticed that my lib have enough implementations which I never seen before, so I want to share it with community.

Hope it will inspire you to improve or rewrite your library and eventually rethink about some algorithms (which happens to me couple times).

#5 Centroid decomposition

graphs/trees/centriod_decomposition.cpp

This one is the perfect example of what I am calling convenient api. I've seen a lot of centroid decomposition snippets and almost all of them is template code or unfinished data structure with piece of code where you need to add your solution. I have snippet that doesn't need changes at all and I need to just call it.

centriod_decomposition(tree, [&](auto &g, size_t centroid, size_t sizeof_subtree) {
	//g - is current subtree and I can freely traverse it from centroid by dfs
});

Once I've seen almost similar implementation but author passed to lambda original tree and vector of blocked vertices. I feel this uncomfortable to code: you still need to take in mind that vertex can be inaccessible.

It has little overhead from coping graph but I have never stubbed on this because overall running time is more crucial.

#4 Parallel binary search

misc/parallel_binary_search.cpp

Well, it is actually not general (can't solve this) but I am working on this. This one about "find first version of data where query is satisfied".

It rarely used but api is really sweet. Here I need to describe scope where I set predicate, implement evolution of data and just commit all changes — internally all queries will be executed in necessary moments.

auto res_bs = parallel_binary_search(qn, m, [&](auto vc) {
	//usually create init data
	vc.pred = [&](size_t qi) {
		//check query in data and return true/false
	};
	//do changes of data and call vc.commit() on each
});

Some examples:

First time united by dsu
Asking range queries

As bonus this implementation has very low time and memory overhead. It not uses map or vector of vectors: you can store all unfinished ranges sorted by left and very easy to split it to new groups in place.

#3 ilist<T>

structs/ilist.cpp

Implicit splay tree with iterators (stl style). It has api close to std::vector and not invalidating iterators like in std::set.

ilist<int> a;
a.push_back(0);
a[0] = 10; //O(logn) access
a.assign(begin(vec), end(vec));
for(int &x : a) ++x; //traverse from begin to end

while(std::size(a) < 10) { //calls a.size()
	a.insert(begin(a), 1); //O(logn) insertion
}

ilist<int> b = a; //makes copy
a.insert(begin(a), b.extract(begin(b) + 3, begin(b) + 6)); //O(logn) cut
a.insert(end(a), std::move(b)); //moving without coping

auto it = a.at(5); //iterator to 5th item
assert(begin(a) + 5 == it); //iterator advance in O(logn)
assert(it - begin(a) == 5); //iterators distance in O(logn)
assert(std::next(it) - begin(a) == 6); //also has ++it
assert(std::prev(it) - begin(a) == 4); //and --it

This snippet is biggest one but is not in final state. As TODO I want to implement reversible option and build link-cut trees that use it as black box.

#2 NTT (with modulos calculated in compile time)

numeric/ntt.cpp

My ntt is very default and maybe slow but it have no hardcoded ntt-frendly modulos. Actually I can understand why you add 3-5 modulos in ntt snippet but what I do not is hardcoding attendant stuff for it like roots. Seriously all this can be calculated in runtime easily. But with power of c++ all modulos can be calculated too!

As result convolution receives parameters and compiler will generate modulos that satisfy them.

convolution<modint<998244353>>(a, b); //will recognise as ntt-mod
convolution<modint<786433>>(a, b); //same! check codeforces.com/gym/100341/problem/C
convolution<modint<1000000007>>(a, b); //will generate 3 modulos and combine result by CRT
convolution<uint64_t, 1<<20 ,5>(a, b); //will generate 5 modulos good for length at most 2**20

#1 Hashing

And finally my favorite... strings/hasher.cpp

My hashing snippet uses famous rolling hash with $$$mod = 2^{61}-1$$$ with magic bit multiplication which works really fast.

Lets define core usual primitives about hashing and just implement it:

hash_t — wrapper of uint64_t containing raw hash (special case of modint)

hashed — what remains from string after it... hashed: hash_t and its length

hashed h = str; //from std::string
h.length() // length of hashed string
if(h1 == h2) // equality check in O(1)
h = h1 + h2; // concatenations in O(1)
h += 'a';

hasher — hash array of string/vector, allows get hash on range in O(1)

hash_span — reference to some range in hasher

hasher a(str), arev(rbegin(str), rend(str));
a.substr(start, length); // hashed
a.subhash(start, length); // hash_t
hash_span s = a(l, r); 
assert(s.length() == r - l);
assert(a[s.start()] == s[0]);

hash_span also have overloaded operator< (in $$$O(\log len)$$$) which can be used in std::sort or std::lower_bound.

Полный текст и комментарии »

  • Проголосовать: нравится
  • +116
  • Проголосовать: не нравится

Автор oversolver, история, 2 года назад, По-английски

Looking at language statistics in last rounds I have wondered why so many Java and so extremely low Kotlin coders?

Codeforces Round #797 (Div. 3)
CodeCraft-22 and Codeforces Round #795 (Div. 2)
Educational Codeforces Round 129 (Rated for Div. 2)

Kotlin, in a rough sense, is modern replace for Java, so...

If you are Java-coder then I am curious why you not change Java to Kotlin?

  • I'm too accustomed to Java/other, my brain think in Java, my hands do in Java
  • My (future) job is coding in Java
  • I doesn't like/hate Kotlin for some reasons
  • I don't want to note basic advantages of Kotlin which described everywhere. But it is language with modern syntax, and I think it is cool for cp almost as Python. I have more than one year experience of coding in Kotlin (but not cp!!), and I can describe my feelings as "I will never write even single line again in Java".

    Maybe you remembered Kotlin as not so cool language when it was 1.4 but now (1.7 in moment of writing this blog) both syntax and std lib have improvements. For example, instead of readLine()!! now it is readln(). Or modulo function

    val MOD = (1e9 + 7).toInt()
    println((-1).mod(MOD)) //1000000006
    println(((MOD-1L)*(MOD-2L)).mod(MOD)) //2
    

    Usage of lambdas in Kotlin is awesome. Just look at this custom comparator:

    data class Segment(val l: Int, val r: Int, val id: Int)
    ...
    segments.sortWith(compareBy<Segment> { it.l }
        .thenByDescending { it.r }
        .thenBy { a[it.id] }
    )
    

    Last what I really like is extensions, great for templates/libraries

    fun IntRange.firstFalse(pred: (Int) -> Boolean): Int {
        var l = first
        var r = last + 1
        while (l < r) {
            val m = (l + r) / 2
            if (pred(m)) l = m + 1 else r = m
        }
        return r
    }
    
    //usage:
    val i = (0 .. 2e9.toInt()).firstFalse {
        1L * it * it <= s
    }
    

    Полный текст и комментарии »

    • Проголосовать: нравится
    • +63
    • Проголосовать: не нравится

    Автор oversolver, 3 года назад, По-английски

    TL;DR: range_sum_range_add with only two BITs.

    Suppose we have (sub)task to proceed following queries:

    • range_sum(l, r)
    • range_add(l, r, x)

    Most familiar solution is segment tree with lazy propagation. And maybe you didn't knew, but such segment tree does not needs pushes!

    code of segment tree without pushes

    Every time I see segment tree with pushes in this problem, my heart is bleeding... But! This is unnecessary, because this problem does not needs segment tree!

    Reducing range_sum_range_add to range_sum_position_add

    All following is described in 1-index numeration, and by range I mean half-interval $$$[L, R)$$$.

    Let me remind you that sum on range can be reduced to sum on prefix (or suffix). And in the same way — adding on range can be reduced to adding on prefix (or suffix).

    How?

    OK, suppose we have some changes of array (adding on prefix). We have for each $$$i$$$ value $$$a_i$$$ means this value is added on $$$i$$$-th prefix. How to calculate sum on particular prefix $$$k$$$? All added values inside prefix, i.e. $$$i \leq k$$$, must be added fully as $$$a_i \times i$$$. Values outside prefix, i.e. $$$k < i$$$, must be added as $$$a_i \times k$$$.

    Lets keep two classic range_sum_position_add data structures. First, call it f, takes $$$a$$$ as is. Second, call it t, as $$$a_i \times i$$$. It means, if we need to proceed adding $$$x$$$ on prefix $$$i$$$, we call f.position_add(i, x) and t.position_add(i, x*i).

    To answer prefix sum query we need:

    • all values inside: it will be t.range_sum(1, i+1),

    • all values outside: it will be f.range_sum(i+1, n+1) * i.

    That's all! With help of Binary Indexed Tree, as most popular rsq, we can achieve fast, non recursive and short way to implement required data structure.

    We can change prefix/prefix to other three combinations and get similar formulas. As example, my code library have prefix/suffix version to achieve only prefix summation and suffix addition in both nested BITs:

    code

    Bonus: sqrt versions

    It is well-known that with classic sqrt-decomposition queries can be proceeded in $$$O(\sqrt n)$$$ each. But with described reducing to range_sum_position_add we can achieve $$$O(1)$$$ / $$$O(\sqrt n)$$$ or $$$O(\sqrt n)$$$ / $$$O(1)$$$ versions for range_add / range_sum.

    First is simple, adding to one element followed with adding to block contain this element, and summation is sum of $$$O(\sqrt n)$$$ blocks and sum of $$$O(\sqrt n)$$$ corner elements.

    Second needs reducing range_sum_position_add to position_sum_range_add: lets support suffix sums (which gives sum on range as difference of two suffixes in $$$O(1)$$$) and changing one element $$$i$$$ affecting only suffixes sums from $$$0$$$ to $$$i$$$ (this part takes $$$O(\sqrt n)$$$).

    Полный текст и комментарии »

    • Проголосовать: нравится
    • +53
    • Проголосовать: не нравится

    Автор oversolver, 5 лет назад, По-английски

    Given a rooted tree.

    Suppose query (v,d) is set of vertices in subtree of v in depth d from v.

    Let's take all possible queries and leave only distinct sets.

    Sum of sizes of these sets is $$$O(n\sqrt{n})$$$.

    Actually, idk how to prove it. If you know proof, please post it here.

    I met it in one Gerald's lecture with proof like "try to construct worst case and you see it".

    I can construct such family of trees with $$$f(k)\approx \frac{n \sqrt n}{2}$$$ where $$$n(k) = \frac{k(k+1)}{2}$$$

    Here tree with k=4:

    Only two times I tried to apply this as base of my solutions. But it was 1) useless — there are was easiest ones 2) painfull.

    If you have some problems for this feature, post it please.

    Полный текст и комментарии »

    • Проголосовать: нравится
    • +52
    • Проголосовать: не нравится

    Автор oversolver, история, 5 лет назад, перевод, По-русски

    Задача

    Есть довольно известная задача для заданного массива чисел и числа $$$K$$$ найти минимумы для всех отрезков длины $$$K$$$.

    Более общий вариант: дан массив чисел длины $$$n$$$ и $$$q$$$ запросов о поиске минимума на отрезках $$$[l_i,r_i]$$$, причём $$$l_i \leq l_{i+1}$$$ и $$$r_i \leq r_{i+1}$$$. Решение должно работать за $$$O(n+q)$$$.

    Решением задачи является поддержка скользящего окна, в котором хранится отрезок чисел. Нужна структура данных, умеющая в push_back (добавить элемент в конец окна), pop_front (удалить первый элемент окна) и get_min — текущий минимум.

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

    Решение #1

    Идея в том, чтобы хранить последовательность возрастающих минимумов. Первый элемент последовательности равен минимуму всего текущего окна, следом идёт минимум на суффиксе после этого элемента, и так далее. Например, для значений в окне [2,3,1,5,4,8,7,6,9] это будут минимумы [1,4,6,9].

    Когда происходит добавление элемента справа (incR, push_back), последовательность меняется следующим образом: все большие элементы убираются, сам элемент добавляется в конец.

    Несколько примеров, для [1,4,6,9]:

    Добавление 5: [1,4,6,9] -> [1,4] -> [1,4,5]

    Добавление 100: [1,4,6,9] -> [1,4,6,9,100]

    Добавление 0: [1,4,6,9] -> [] -> [0]

    При удалении элемента слева (incL, pop_front) нужно знать, был ли главный минимум первым элементом окна. По значению этого не понять, поэтому в последовательности помимо самих значений нужно хранить в паре с ними позиции этих значении. Пример выше превратится в последовательность [(1,2), (4,4), (6,7), (9,8)], если элементы нумеруются с нуля. Границы текущего окна нужно хранить явно в виде пары чисел $$$L$$$ и $$$R$$$. Итак, если первый элемент последовательности является первым элементом окна, то есть его позиция равна $$$L$$$, то его необходимо просто удалить, более ничего в последовательности не изменится.

    Для реализации потребуется структура данных, позволяющая эффективно делать операции удаления слева и справа, а также добавления справа. Для этого можно использовать дек (std::deque в c++)

    реализация #1

    Решение #2

    Представим окно в виде двух стеков: префикс окна находится в левом стеке $$$L$$$ так, что первый элемент окна вверху $$$L$$$, а суффикс окна в правом стеке $$$R$$$ так, что последний элемент окна вверху $$$R$$$.

    [2,3,1,5,4,8,7,6,9]
    <-------|--------->

    L = [5,1,3,2]
    R = [4,8,7,6,9]

    Когда происходит добавление элемента справа (incR, push_back), элемент попадает в вершину правого стека

    При удалении элемента слева (incL, pop_front), элемент уходит с вершины левого стека. Исключение составляет случай, когда в этот момент левый стек пуст. Тогда перед удалением происходит операция перебрасывания правого стека в левый: пока правый стек не закончится, его верхний элемент перемещается в левый стек. Например,

    [4,3,1,2]
    |------->

    L = []
    R = [4,3,1,2]

    L = [2]
    R = [4,3,1]

    L = [2,1]
    R = [4,3]

    L = [2,1,3]
    R = [4]

    L = [2,1,3,4]
    R = []

    [4,3,1,2]
    <-------|

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

    Чтобы отвечать в любой момент о текущем минимуме в окне нужно знать минимумы в стеках. Так как правый стек только увеличивается или очищается полностью, его минимум можно поддерживать в отдельной переменной $$$Rmin$$$.

    В левом стеке вместо самих значений нужно хранить минимум среди значений от этого элемента до дна стека. Например для стека [5,7,3,4,2,1,8,6] это будет стек [5,5,3,3,2,1,1,1]

    Итого, минимум в окне будет равен либо верхнему элементу левого стека, либо $$$Rmin$$$.

    реализация #2

    Примечание

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

    Также, оба решения можно проапгрейдить до операции push_front, хотя кажется, такая операция нигде не нужна.

    Полный текст и комментарии »

    • Проголосовать: нравится
    • +35
    • Проголосовать: не нравится

    Автор oversolver, история, 6 лет назад, По-английски

    We have n points (xi, yi). Let both x and y be different and from [1, n].

    Initially, all points have zero value.

    Two types of queries:

    1) add X — adding +1 to all points with xi ≤ X

    2) sum Y — calculate sum of values of points with yi ≤ Y

    What is best known solution? (Problem not from running contest).

    Полный текст и комментарии »

    • Проголосовать: нравится
    • +49
    • Проголосовать: не нравится

    Автор oversolver, история, 7 лет назад, По-английски

    I've heard more about how vector<bool> is slow, and we need to dodge him or use vector<char>.

    But today I ran some tests in "Codeforces custom test" with GNU G++.

    First simple erato sieve

    //N = 3e7
    for(int i=2;i<N;++i) if(!u[i]) for(int j=i*2;j<N;j+=i) u[j] = 1;
    int cnt = 0;
    for(int i=0;i<N;++i) if(u[i]) ++cnt;
    cout<<cnt<<endl;
    

    Depends on u:

    bool u[N] : 420 ms, 31204 KB

    vector<bool> u(N): 218 ms, 5484 KB

    vector<char> u(N): 451 ms, 31164 KB

    You can say like "memory constant speed up this code"

    Second.

    //N = 1e4
    double total = 0;
    for(int it=0;it<N;++it){
    	for(int i=0;i<N;++i){
    		int x = rand()%N;
    		u[x] = 1;
    	}
    	for(int i=0;i<N;++i){
    		int x = rand()%N;
    		u[x] = 0;
    	}
    	for(int i=0;i<N;++i){
    		total+=u[i];
    		u[i] = 0;
    	}
    }
    cout<<total/N<<endl;
    

    bool u[N] : 2683 ms, 1860 KB

    vector<bool> u(N): 2667 ms, 1832 KB

    vector<char> u(N): 2620 ms, 1868 KB

    We see its equal!

    So maybe its not so bad? Or you have examples where vector<bool> is really slower than alternatives?

    Полный текст и комментарии »

    • Проголосовать: нравится
    • +57
    • Проголосовать: не нравится

    Автор oversolver, история, 7 лет назад, По-английски

    Your given a simple task:

    For given number 2 ≤ n ≤ 1018 check prime or not it is.

    Limits are: 1 second TL, 16Mb ML.

    And we have solution:

    bool isprime(LL n){
    	if(n<2) return false;
    	for(LL i=2;i*i*i<=n;++i) if(n%i==0) return false;
    	for(int it=0;it<1e5;++it){
    		LL i = rand()%(n-1)+1;
    		if(__gcd(i,n)!=1) return false;
    		if(mpow(i,n-1,n)!=1) return false;
    	}
    	return true;
    }
    

    where rand() returns 64-bit random integer and mpow(a,b,m) is ab modulo m.

    Can you challenge this solution?

    Полный текст и комментарии »

    • Проголосовать: нравится
    • +42
    • Проголосовать: не нравится

    Автор oversolver, история, 9 лет назад, По-английски

    Hi people! I interested in problems with template name "Count of subrectangles" or "Best subrectangle" in rectangle. For example some well-known problems:

    In given matrix of zeros and ones find subrectangle that contains only zeros and has largest area. It can be solved in O(n2).

    In given matrix of integers find subrectangle with largest sum. It can be solved in O(n3).

    Can you give me links or just statements on similar problems?

    Here problems that i remembered:

    Please, gimme other problems, any links in contests or archives:) I will add it here later.

    Полный текст и комментарии »

    • Проголосовать: нравится
    • +38
    • Проголосовать: не нравится

    Автор oversolver, 9 лет назад, По-русски

    Хотелось бы узнать, как проводится подготовка и проведение четвертей в разных регионах. Точно интересуют следующие вопросы:

    • Как готовятся задачи (полигон, папка на компе, папка в облаке и т.д.);

    • В чём проводится тур (известная система, или нет, название);

    • Кем прорешиваются задачи (коллектив авторов, или есть вообще деление на авторов — подготовителей — прорешивающих);

    • Условия задач на английском или на русском?

    Интересна любая информация с разных регионов.

    Полный текст и комментарии »

    • Проголосовать: нравится
    • +83
    • Проголосовать: не нравится

    Автор oversolver, 10 лет назад, По-русски

    Всем привет!
    Многим известны соревнования Google Code Jam, Facebook Hackercup, IPSC, ch24, deadline24 и т.п. Их объединяет то, что в задаче даётся инпут с мультитестом внутри, на который нужно посчитать ответ у себя локально. Мне стало интересно, пользуетесь ли вы распараллеливанием? Современные машины позволяют дать ощутимый прирост, например, чтобы избежать таких ситуаций.

    А прошедший deadline24 мы писали так


    (само описание событий)

    Я решил написать свой вариант шаблона, и вот что у меня получилось: code.
    Вот ещё другой подход от kormyshov: code.

    Кто может предложить другие варианты, желательно на с++? А может на других языках это делается проще? А может у кого-то есть инструменты, позволяющие параллелить инпут на несколько машин (кроме рук и флешки:)? Интересны любые ответы и замечания по теме.

    Полный текст и комментарии »

    • Проголосовать: нравится
    • +41
    • Проголосовать: не нравится

    Автор oversolver, 10 лет назад, По-русски

    485A - Фабрика

    Производство остановится только в том случае, если существует такое целое K ≥ 0, что a·2K делится на m. Из этого следует, что K может быть максимум порядка log2(m). Так как K — это по сути сколько пройдёт дней до этого момента, то можно промоделировать описанные в условии задачи действия, например, в течение 20-ти дней (не забыв при этом про 64-битный тип данных). Если в какой-то момент производство остановилось, то ответ "Yes". Если не остановилось в течение этих дней, то оно не остановится никогда, а значит ответ "No".

    485B - Ценные ресурсы

    Найдём минимальную длину, необходимую чтобы покрыть все точки по оси Ox — это будет в точности Maximum(xi) - Minumum(xi). Аналогично и для оси Oy — это Maximum(yi) - Minumum(yi). Так как нам необходим квадрат, то следует взять максимальную из этих двух величин в качестве длины его стороны.

    484A - Биты

    Опишем функцию f(L, R), которая будет ответом на задачу. Она ведёт себя следующим образом:

    • если L = R, то f(L, R) = L;

    • иначе, если 2b ≤ L, где b — максимальное целое число такое, что 2b ≤ R, то f(L, R) = f(L - 2b, R - 2b) + 2b;

    • иначе, если 2b + 1 - 1 ≤ R, то f(L, R) = 2b + 1 - 1;

    • иначе f(L, R) = 2b - 1.

    Итоговая сложность — O(logR) на один запрос.

    484B - Максимальное значение

    Переберём все различные числа aj исходной последовательности. Так как нужно максимизировать , то переберём в цикле по x все числа, кратные aj в диапазоне от 2aj до M, где M — удвоенное максимальное значение элемента последовательности. Для каждого такого числа нужно найти максимальное ai, такое, что ai < x. Ограничения на числа позволяют сделать это за O(1) при помощи массива. Обновим ответ величиной . Так как перебираются только различные aj, то итоговая сложность составит O(nlogn + MlogM).

    484C - Странная сортировка

    Заметим, что d-сортировка не зависит от того, какие символы находятся в строке, а значит является перестановкой (назовём её P). Посмотрим на операцию перемешивания по другому: вместо того, чтобы переходить к очередной следующей подстроке и сортировать её, сделаем циклический сдвиг всей строки на один символ влево. Таким образом, сортироваться будет только префикс строки, а строка сдвигаться. Осталось понять, что сдвиг влево — это тоже перестановка (назовём её C). Теперь всё просто, к строке нужно поочерёдно применять перестановки P и C, то есть нужно получить S·P·C·P·C... = S·(P·C)n - k + 1. После этого строку нужно сдвинуть ещё на k - 1 символ влево, чтобы получить то, что получится после операции перемешивания. Здесь используется умножение перестановок, а оно в свою очередь ассоциативно, а значит для вычисления (P·C)n - k + 1 можно воспользоваться бинарным алгоритмом. Итоговая сложность составит O(nmlogn).

    484D - Детский сад

    Заметим, что существует оптимальный ответ такой, что любой отрезок, который образует группу, содержит свои максимальные и минимальные значения на границах. Иначе было бы выгодно разбить отрезок хотябы на два. Так же можно заметить, что каждый отрезок будет строго монотонным (элементы на нём строго возрастают или убывают). Выделим интересные точки в последовательности — это либо локальные максимумы (то есть ai - 1 < ai > ai + 1), либо минимумы (ai - 1 > ai < ai + 1), либо соседние с ними точки. Будем решать задачу методом динамического программирования, Di — наилучший ответ для префикса i. Тогда при вычислении очередного значения необходимо обработать не более трёх предыдущих интересных точек, а так же предыдущую точку. Итоговая сложность — O(n).

    484E - Объявление на заборе

    Заметим, что при поиске ответа на конкретный запрос можно воспользоваться бинарным поиском по ответу. Пусть в какой-то момент зафиксировалась высота h и необходимо узнать, помещается ли прямоугольник размера w на h в участок забора с l-ой по r-ой доски включительно. Заведём перед обработкой запросов структуру данных, которая поможет отвечать на такой вопрос. Это будет персистентное дерево отрезков с необычной функцией: максимальное количество подряд идущих единиц на отрезке (далее maxOnes). В листьях дерева будут только числа 0 и 1. Чтобы реализовать такую функцию, необходимо ввести ещё некоторые величины, а именно:

    len — длина отрезка в вершине дерева отрезков, prefOnes — длина префикса, полностью состоящего из единиц, sufOnes — длина суффикса, полностью состоящего из единиц.

    Вычисляются эти функции следующим образом:

    maxOnes, требуемая функция, равна max(maxOnes(Left), maxOnes(Right), sufOnes(Left) + prefOnes(Right));

    prefOnes равна prefOnes(Right) + len(Left) в случае, если len(Left) = prefOnes(Left), иначе она равна prefOnes(Left);

    sufOnes равна sufOnes(Left) + len(Right) в случае, если len(Right) = sufOnes(Right), иначе она равна sufOnes(Right);

    len = len(left) + len(Right);

    Left и Right — соответственно левый и правые сыновья текущей вершины в структуре дерева отрезков.

    Как уже упоминалось выше, дерево должно быть персистентным (то есть хранить все свои версии после изменений), строиться оно должно следующим образом. Сначала строится пустое дерево — из одних нулей. Далее в позицию, где в заборе находится самая высокая доска, ставится единица. Тоже самое делается для второй по убыванию высоты доски и так далее. Например, если забор описывался последовательностью [2, 5, 5, 1, 3], то изменения последнего слоя дерева будут следующими:

    [0, 0, 0, 0, 0] -> [0, 1, 0, 0, 0] -> [0, 1, 1, 0, 0] -> [0, 1, 1, 0, 1] -> [1, 1, 1, 0, 1] -> [1, 1, 1, 1, 1].

    При этом для каждой высоты hi нужно запомнить соответствующую версию дерева, назовём её как treeheight. Теперь, чтобы ответить на вопрос выше, нужно сделать запрос на отрезке [l, r] у дерева treeh. Если maxOnes этого запроса меньше w, то прямоугольник невозможно разместить, иначе возможно.

    Построение дерева займёт O(nlogn) времени и памяти. Ответ на запрос займёт O(log2n) времени.

    Полный текст и комментарии »

    Разбор задач Codeforces Round 276 (Div. 1)
    Разбор задач Codeforces Round 276 (Div. 2)
    • Проголосовать: нравится
    • +66
    • Проголосовать: не нравится

    Автор oversolver, 10 лет назад, По-русски

    Привет всем!

    Сегодня состоится Codeforces Round #276, который пройдёт в обоих дивизионах. Время старта — 19:30 по московскому времени (перейдите по ссылке для просмотра времени в других регионах). За помощь в подготовке контеста спасибо Zlobober, за перевод на английский спасибо Delinur, и спасибо MikeMirzayanov за сам проект Codeforces.

    Желаю всем удачи, надеюсь, вам понравятся задачи :)

    UPD Разбалловка в обоих дивизионах будет динамическая (подробнее об этом можно почитать здесь). Задачи будут упорядочены по возрастанию сложности, тем не менее, не забудьте прочитать все задачи до конца контеста.

    UPD Контест окончен! Спасибо всем, кто решал задачи несмотря ни на что. Разбор будет опубликован позднее.

    UPD Разбор можно найти здесь. На моё удивление задача div1C оказалась довольно сложной и по количеству посылок сравнялась с div1E, а во втором дивизионе эту задачу вообще никто не сдал за время контеста. Удачи вам в покорении разбора :)

    Полный текст и комментарии »

    • Проголосовать: нравится
    • +276
    • Проголосовать: не нравится

    Автор oversolver, 10 лет назад, По-русски

    Привет сообществу CodeForces! Рад сообщить о предстоящем 256-м раунде, который пройдёт для представителей второго дивизиона. Представители первого дивизиона смогут поучаствовать вне конкурса.

    Надеюсь, для всех это юбилейный раунд. Для меня же это первый раунд, в котором я являюсь автором, по-этому я буду рад видеть всех. Хочу поблагодарить Gerald'а, который помог с подготовкой контеста, Delinur за перевод условий, и конечно MikeMirzayanov за сам проект CodeForces.

    Я сам из Красноярска, а героем задач будет наш незаменимый командный талисман Бизон-Чемпион. Надеюсь, вам понравится провести с ним время:) До встречи и удачи!

    UPD. До начала соревнования осталось несколько часов. Стоимость задач будет динамической (подробнее об этом можно почитать здесь).

    UPD. Раунд завершился, разбор можно прочитать здесь.

    Полный текст и комментарии »

    • Проголосовать: нравится
    • +138
    • Проголосовать: не нравится

    Автор oversolver, 10 лет назад, По-русски

    448A - Про награды

    Решение:7139559

    Так как награды одного типа можно ставить на одну полку, то посчитаем общее количество кубков a и общее количество медалей b. Чтобы узнать минимальное количество полок, которое потребуется например для кубков, можно посмотреть на целую часть дроби a / 5 и учесть остаток. Но можно воспользоваться и более лаконичной формулой: (a + 5 - 1) / 5. Аналогично считается и минимальное количество полок для медалей: (b + 10 - 1) / 10. Если сумма этих величин превосходит n, то разместить награды нельзя, иначе можно.

    448B - Про суффиксные структуры

    Решение:7139584

    Рассмотрим каждый случай отдельно. При использовании только суффиксного автомата от строки останется какая-то её подпоследовательность. Проверить, является ли t подпоследовательностью s можно разными способами, но самый простой и самый быстрый — известный метод двух указателей. При использовании только суффиксного массива можно добится любой перестановки символов исходной строки. Если вам не очевидно это, советую подумать, почему это так. Тем самым s и t должны быть анаграммами. Так ли это, можно проверить, посчитав количество каждой буквы в обоих строках. Если каждая буква входит в s столько же, сколько и в t, то это анаграммы. Осталось проверить случай применения обоих структур. Общая стратегия здесь такая: удалить ненужные символы и сделать нужную перестановку. Так ли это, можно проверить похожим образом, как и со случаем анаграмм, но каждая буква должна входить в s не меньшее число раз, чем в t. Если же ниодна проверка не выполнилась, то получить t из s невозможно. Итоговая сложность O(|s| + |t| + 26).

    448C - Про покраску забора

    Решение:7139610

    Для решения нужно понять несколько простых вещей. Например, каждая горизонтальная полоса должна быть как можно шире, то есть не упираться своими краями в другие полосы, так как иначе будет не оптимально. Вторая мысль — под каждой горизонтальной полосой могут быть только горизонтальные полосы, так как опять же, иначе это не оптимально. По-этому, если низ забора будет покрашен горизонтальным мазком, то этих мазков должно быть не менее min(a1, a2, ..., an). Эти мазки красят низ забора и, возможно, разбивают забор на несколько непокрашенных несвязных частей. Для каждой из этих частей нужно вызвать решение, учитывая что некоторая нижняя часть забора покрашена, и проссумировать эти значения. Теперь понятно, что функция рекурсивная — ей передаются границы отрезка l, r (часть забора) и уже покрашеная горизонтальными мазками высота h. Но нужно не забыть ещё про один вариант — когда нет горизонтальных мазков и все доски красятся вертикально. Это означает что ответ на отрезке следует ограничить числом r - l + 1. При данных ограничениях на n минимальный элемент на отрезке можно находить наивно, пробекаясь по отрезку [l, r]. Суммарная сложность в этом случае составит O(n2). Но если использовать для нахождения минимума например, дерево отрезков, то можно добиться сложности O(nlogn).

    448D - Про таблицу умножения

    Решение:7139620

    Воспользуемся бинарным поиском по ответу. Нам нужно найти такое максимальное x, что количество чисел из таблицы, меньших x, строго меньше k. Чтобы посчитать это количество для фиксированного x, просуммируем количество меньших чисел в каждой строке. В i-й строке их будет . Тем самым итоговая сложность — O(nlog(nm)).

    448E - Про делители

    Решение:7139644

    Научимся превращать Xi в Xi + 1. Для этого нужно действовать по определению и конкатенировать списки делителей каждого элемента Xi. Чтобы это сделать эффективно, предпосчитаем все делители числа X (потому что для любого i, Xi будет состоять только из его делителей). Это можно сделать стандартным методом за . Как посчитать делители делителей? Нужно знать, что для данных в задаче ограничений на X, максимальное количество делителей D(X) будет 6720 (у числа 963761198400), а значит, делители делителей можно посчитать за O(D2(X)). Имея эти списки, можно превратить Xi в Xi + 1 за время O(N), где N = 105 — ограничение на количество элементов в выводе. Теперь научимся превращать Xi в X2i. Что нам говорит последовательность Xi? Помимо того, куда перейдёт X за i шагов, она может сказать, куда перейдёт каждый делитель числа X за i - 1 шаг. На самом деле, Xi является конкатенацией всех Yi - 1, где Y — делители X. Например, 103 = [1, 1, 1, 2, 1, 1, 5, 1, 1, 2, 1, 5, 1, 2, 5, 10] = [1] + [1, 1, 2] + [1, 1, 5] + [1, 1, 2, 1, 5, 1, 2, 5, 10] = 12 + 22 + 52 + 102. Как узнать какой отрезок соответствует какому-то Y? Пусть pos(Y) — позиция первого вхождения числа Y в Xi. Тогда нужный отрезок будет начинаться в pos(prev(Y)) + 1, а заканчиваться в pos(Y). prev(Y) — это предыдущий перед Y делитель X (если их упорядочить по возрастанию). Итак, чтобы получить X2i из Xi, нужно для каждого элемента знать, куда он перейдёт за i шагов. Мы знаем его делители — это один шаг, а для каждого делителя знаем, куда он перейдёт за i - 1 шаг. Тем самым, нужно сконкатенировать нужные отрезки в определённом порядке. Это так же можно сделать за O(N). Как теперь найти Xk для любого k? Способ похож на алгоритм быстрого возведения числа в целую степень:

    при k = 0 Xk = [X],

    при нечётном k выполнить переход от Xk - 1 к Xk,

    при чётном k выполнить переход от Xk / 2 к Xk.

    Такой метод делает O(logk) итераций. Кстати, так как при X > 1 последовательность Xk будет иметь префикс из k единиц, то k можно ограничить числом N. Итоговая сложность составит .

    Полный текст и комментарии »

    Разбор задач Codeforces Round 256 (Div. 2)
    • Проголосовать: нравится
    • +78
    • Проголосовать: не нравится

    Автор oversolver, 12 лет назад, По-русски

    Здравствуй, сообщество CodeForces!

    Пишу я задание для получения зачёта по проге и сталкиваюсь со следующей проблемой. В общем, имеется игра Реверси. Имеются несколько типов ботов, из них:

    • жадный: ходит самым максимальным из доступных ходов (greedy)

    • перебор minmax дерева игры глубиной до N (aiN)

    • то же, что и выше, но написанное с ab-отсечением. (aiN_ab)

    Имеется проблема:

    Счёт за 300 игр:

    • ai4 vs. greedy — 287:8 и это, я считаю, нормально

    • ai4_ab vs. greedy — 250:47 и это не очень нормально.

    Насколько я понимаю, аб-отсечения должны давать такой же результат, но быстрее. Собственно имеется вопрос: это не так или я делаю что-то не так? Исходники-примеры я брал отсюда, отсюда, отсюда и ещё с некоторых других источников (мой код). Результат везде одинаковый. Может у кого-то есть реальный пример какой-то игры, а не просто сатья-мануал? Ну или какие-то комментарии по этому поводу, буду очень признателен.

    Полный текст и комментарии »

    • Проголосовать: нравится
    • +10
    • Проголосовать: не нравится