Friday, July 10, 2026

A humanity week

AtCoder World Tour Finals 2026 in Japan was the main event of this week (problems, results, top 13 on the left, commentary recording, analysis). With the three last problems significantly harder than the first two, it all came down to choosing the hard problem that fits one's strengths the most, as there was only enough time for solving one hard problem, despite the round lasting a whopping 7 hours. Only tour1st managed to do this well, getting D accepted after 6 hours, which would be enough for the first place by itself. Congratulations on the win!

Let me highlight the easiest (puts the concept of "easy" into perspective, doesn't it?) problem of this round, problem A: you are given a tree on n vertices (n<=250000) where edges have weights. We now construct a new undirected complete graph on the same n vertices where edges have capacities: the capacity of the edge between two vertices is equal to the smallest edge weight on the path connecting those two vertices in the given tree. Finally, for each possible choice of source and sink we compute the maximum flow (=minimum cut) between them in that graph. What is the sum of all those flows, modulo 998244353?

The other important track at the AWTF was the human versus AI competition. While LLMs have previously outperformed the humans at the ICPC, and it happened almost a year ago, which is an eternity in terms of LLM development, some people, including myself and Riku (the admin of AtCoder and the problemsetter for the AWTF) were still hoping that humans can hold their own on AtCoder problems, which require much more problem solving and mathematical intuition. Well, it was now clearly demonstrated, under proper contest conditions (as opposed to solving the problems after the contest, which always raises the question of the editorials leaking into the model training data), that the LLMs can handle such problems very well, too. Well done to the OpenAI team!

I'm looking forward to discovering how this will transform the world of algorithmic programming competitions. Notwithstanding the obvious issue of cheating, I expect quite a few positives: creating and preparing problems should become more efficient now, and the same could happen with the learning/training process. What do you think will change the most?

Thanks for reading, and check back next week.

Saturday, June 27, 2026

DragonByte, WEOI, and AWTF 2026

Long time no hear!

I wanted to mention three contests with onsite rounds happening soon.

First off, Wincent DragonByte 2026 qualification round is ongoing and will continue for about 12 more hours (you won't need so long to qualify!). This is a new competition (this year is the second edition) with onsite finals in September in Bratislava, and with problemsetters of the IPSC fame. Everybody can participate in the online rounds, but only citizens and residents of most European countries can qualify for the onsite. Good luck!

Then, WEOI 2026 is another new contest that happens this weekend in Luxembourg, with its 4th edition this year, limited to fewer European countries. This one is for secondary school students, and the contest starts in about one hour. You cannot participate, but you can maybe watch the scoreboard, even though I don't have a link so far :)

Finally, AtCoder WTF 2026 is happening in 1.5 weeks in Tokyo (well, technically in Chiba). There is nothing European in this one, everyone could qualify, but only 12 best in Algorithm and Heuristic divisions actually did. I will be there as a commentator on the Algorithm live stream, and check out the Heuristic live stream as well!

It is nice to see so many and so diverse onsite events, including recently created ones. I don't have enough time to visit all onsite events that I want to go to in 2026. I hope the community will continue to thrive and I will continute to have this FOMO :)

Thanks for reading, and check back next week for some Japan travel blogging.

Thursday, February 26, 2026

Codeforces Blitz Cup 2026 Round of 32 livestream

Codeforces is running Blitz Cup 2026, and my Round of 32 match against feecIe6418 starts in about 1 hour! You can watch my livestream or the scoreboard. Here is the full bracket.

Sunday, February 8, 2026

EUC 2026 contest day

ICPC EUC 2026 starts in about two hours (9:30 Warsaw time, photo on the left from the official gallery). Here are some useful links:

Note that these problems will be used for the Universal Cup next weekend, so please don't look at them (and don't watch the livestream I guess?) if you are planning to participate there.

In the other case, do tune in!

Saturday, February 7, 2026

A shift week

The Dec 22 - Dec 29 week wrapped up the competitive 2025 on the major platforms. The 4th Universal Cup. Stage 10: Grand Prix of Wrocław took place on Saturday (problems, results, top 5 on the left, onsite results, analysis). Team USA1 were not the first to finish, but they were still the fastest overall, and extended their streak of 8 (!) won rounds in a row. Well done!

Codeforces Good Bye 2025 followed the same day (problems, results, top 5 on the left, analysis). While the round lasted 3 hours, the top three needed just two of those to solve all problems. Similar to the above round, being first to finish did not mean first place, though, as Benq was a bit slower on the medium problems and therefore jiangly could overtake him. Congratulations!

I have enjoyed solving the problems and was making good progress, but it was nowhere near as fast, as problem F by itself took me over an hour of iterations, stress testing etc. 

AtCoder Grand Contest 076 took place on Sunday (problems, results, top 5 on the left, analysis). It turns out that it was indeed possible to qualify for AWTF with a win after a relatively poor season, only it was noimi and not myself who has achieved that feat. Well done!

Quite unsually for an AGC I was able to solve A very quickly, but did not score any points after that. In problem B, I have empirically discovered that the answer is always 1 or 2, but could not figure out a condition to separate those two cases, not to mention the actual sequence of steps to achieve that number. In problem C my incorrect solutions were quite close to the one described in the editorial by the idea, but not close enough by the details. At the end of the contest, I have tried to solve E as a heuristic problem, trying various greedy approaches with local optimizations on top, also without much success. Well, better luck next year!

Here is that heuristic-inviting problem E: you are given at most 250000 vectors, and need to choose a subset that maximizes the value of (squared length of the sum of the chosen vectors) minus three times (sum of squared lengths of the chosen vectors). Can you find an exact solution?

This round wrapped up the race for the 2026 World Tour Finals (results, top 12 on the left). Everyone with >=100 points qualified, but people had wildly different ways to achieve that amount of points, with some qualifying thanks to a round victory, and some never placing higher than fourth in any particular round. I'm looking forward to yet another exciting final in Japan!

It is also notable that some usual suspects did not qualify this time, including zhoukangyang who was first and first in qualification in the previous two seasons, and first and second in the last two World Tour Finals. Better luck next year as well!

In my previous summary, I have mentioned another AtCoder problem: you are given two sequences of integers ai and bi of the same length n up to 2*105, 1<=ai<=109, 0<=bi<=30. We will permute the sequence ai arbitrarily, and then compute sum(ai>>bi), where >> denotes the bitwise shift right. How many of the n! permutations yield the smallest possible value of that sum? You need to print the answer modulo 998244353.

First, let us sort both arrays, so we can assume they are sorted below. Intuitively, it feels that all permutations of ai that deliver the minimum must be sorted or almost sorted. To make further progress, we can either implement a brute force approach to find out experimentally what does almost mean exactly, or make the intuitive argument more formal, with the same goal.

Suppose we have only two elements. If a0 = a1 or b0 = b1, then they can be swapped freely without changing our target value. Now consider the case when they are distinct: a0 < a1 and b0 < b1. When is it OK to swap them?

Since we're going to shift right by at least b0, it makes sense to introduce p = a0 >> b0, q = a1 >> b0, and r = b1 - b0. Then we need the following to be true: p + q >> r = q + p >> r, which is equivalent to q - p = q >> r - p >> r.

What happens to the difference of two numbers if we cut their last bit? It is not hard to check that it stays the same in only one case: if the bigger number was even, and exactly 1 higher than the smaller number. In all other cases it becomes smaller. So for the above condition to be true, we need q = p + 1 and q divisible by 2r. These are the only cases where two distinct numbers can be swapped without increasing our target value.

Now we will consider our arrays in blocks of equal bi. Consider the first such block: b0 = b1 = ... = bk-1 = 0, bk > 0.

Denote x = ak if ak is even, and ak + 1 otherwise. Therefore x is always even. From the above argument, the only swaps we can do between this block and the rest of the array is swapping the values x-1 from the block with the values x outside the block.

Let us denote c as the number of i such that ai = x - 1 and i < k, d as the number of i such that ai = x - 1 and i >= k, e as the number of i such that ai = x and i < k, f as the number of i such that ai = x and i >= k. Note that we always have either d = 0, or e = 0, or both.

Suppose we decided to do g swaps of the above form (x - 1 from the block with x from the rest). Then we have (c + d choose c - g) ways to choose which values x - 1 go inside the block, (e + f choose e + g) ways to choose which values x go inside the block, and (k factorial) ways to permute the values inside the block. We need to multiply those three numbers by the answer for the remaining subproblem, which is considering the arrays starting from the position k, such that g values of x have been replaced with x - 1, and we must make sure that those replaced values all end up in the next r blocks, where r is the number of 0s at the end of the binary representation of x.

Now, what happens when we consider the next block, the one where bi = 1? We can now shift all values ai right by 1, and denote y = x >> 1 (remember that x is even), and now we can handle this block in the same way we handled the block with bi = 0. The only difference is that now we have the additional complexity that g values of y have been replaced with y - 1. Let us define z in the same way as x was defined above, as the "swap value" between this new block and the rest, and suppose we decide to do h swaps of z - 1 from the block with z from the rest.

We now have the following four cases:

  • y < z - 1. This means that all values of y - 1 must go in this block, so they are not special anymore, and we can just sum up the number of ways over all values of g to get a single multiplier, and then solve this block exactly like the first block.
  • y = z - 1. Here all values of y - 1 also must go in this block, but additionally g of the values of y (= z - 1) are no longer available for swapping forwards since they were already swapped backwards, so we need to subtract g from the number of occurrences of z - 1 in the block before computing the above combination numbers.
  • y = z. Here g and h refer to the same swaps (z with z - 1), but done over different block boundaries. The number of occurrences of z and z - 1 within the block (which are again used to compute the above combination numbers) will therefore depend on the difference between g and h (and we need to be careful to make sure h can be smaller than g, too).
  • y > z. Initially I thought this was impossible, but then an assertion failed in my solution :) And indeed, if in the first block the first value of the rest was 5, we'd get x = 6 and therefore y = 3; if the next block still had 5 after it, but now after the right shift we get z = 2. After some thinking I realized that this case means that we have no place to put the values of y - 1 without violating the restrictions, therefore we must only consider the value for g = 0 for the previous block.
This finally gives rise to a polynomial dynamic programming solution. The state of this dynamic programming will be the bit number that is being considered (the value that we have already shifted all ai right by, or alternatively the next value of bi to consider), and the number g of the swaps through the previous block boundary. Note that the other important values, such as the value of y that participated in those swaps, or the position k of the block, are uniquely determined by the state, and therefore do not need to be part of the state. And if we loop over the bit number on the outside, our dynamic programming array can be just one-dimensional. This solution has O(n) states in total over all bits, but each transition can also be O(n), therefore it is quadratic, and indeed it gets TLE.

However, the dynamic programming transition here is summation after multiplying by a combination number, which when expressed via factorials ends up being a product/fraction of factorials of g, h, and either g + h in the y = z - 1 case, or of g - h in the y = z case, and for that we have a trick that was described in this blog 9+ years ago: we can use fast polynomial multiplication via FFT, while using (const - g) and (g + h) as the monomial degrees in the y = z - 1 case to get (const + h) in the product, and using g and (const + h - g) as the monomial degrees in the y = z case to get (const + h) in the product. This gives us an O(n * log(n) * num_bits) solution, which is fast enough. You can check the linked solution for the implementation details.

Thanks for reading, and check back soon for more catch-up summaries and EUC 2026 reports :)

EUC 2026 first day

Yesterday I have arrived in Warsaw, which will host ICPC EUC 2026 this Sunday. As you can see on the left, Warsaw in Feburary (much like Wijk aan Zee in January) is very conductive for indoor activities, which is perfect to be able to truly focus on a programming competition.

I have arrived too late to get to the opening ceremony, but from the slides that were shared it seems that, among other things, Bill has announced the dates for the upcoming ICPC World Finals: November 15-20, 2026 in Dubai (which was already announced before), and September 14-19, 2027 in a yet undisclosed (but seemingly known to the ICPC organizers) location. The ICPC EUC 2027 location was announced last year to be Eindhoven, Netherlands.

But for now the focus is on the upcoming championship, where 8 teams have already qualified for the Dubai World Finals from the various ERCs and are just competing for fun and glory, while ~8 more teams will qualify tomorrow. Best of luck!

Wednesday, December 24, 2025

A digging week

Codeforces Global Round 31 last Friday wrapped up the 2025 Global Round series (problems, results, top 5 on the left, analysis, serie standings that use the "sum all 3" instead of the actual "sum 2 best out of 3" scoring system). I spent way too much time on implementation and debugging once again, solved A-F1 and then decided not to go for even more standard-ish implementation in F2 and instead tried to get H1 in the end, but could not actually solve it. ecnerwala had roughly the same amount of time as myself for H1 after solving A-G (even the strong version of G), and he did solve H1 as well to claim a clear first place. Well done!

Meta Hacker Cup 2025 held its final, this time online, on Saturday (problems, results, top 5 on the left). Gennady has won his 6th Hacker Cup, and it was not easy: he scored 44 out of his 71 points in the last hour, and Yui could still have snatched the first place were her last-second submission in C1 correct. Congratulations to both! Looking at the scoreboard of this contest, it feels sad that all other contests have abandoned the system test concept and no longer reward writing good code and testing it before submission as much.

AtCoder is wrapping up the year with two last chances to qualify for AWTF 2026, the first of which was Grand Contest 075 (problems, results, top 5 on the left, analysis, serie standings). In problem A, I was looking for an algorithmic solution, trying hard to find some sort of knapsack subproblem with slowly increasing weights that can be solved by greedy or backtracking. However, it seems that this was a dead end, and one had to pull the main idea out of thin air, or to print the answers for small n and generalize.

I've then tried to solve problems B and C, but there I actually had an opposite issue: I was digging in the right direction (maintaining a piecewise-constant rolling window of size y in B, going in increasing order of bi and checking how much we can deviate from the sorted order of ai in C), but at some point I stopped digging, with the justification that in an AGC problem I should look for a beautiful alternative approach instead, which I could not find. After reading the editorial it turned out that the missing piece was more digging :)

Luckily, in problem D the required digging was not very deep, and I managed to get a non-zero score with a whopping 2 minutes remaining in the round. This did not get me any tour points, but I guess there is still an imaginary chance to qualify if I win the upcoming AGC 076. That is not going to happen, of course :)

tourist did his share of digging faster than others, but as his AWTF qualification was set in stone anyway, these results might be celebrated more by Kevin090228 who has likely guaranteed his AWTF qualification with this second place, and f3e6g4 who has jumped into the magical top 12 for now. Congratulations to all three!

Let me highlight problem C from this round, at a risk of not being able to fully describe a solution next week: you are given two sequences of integers ai and bi of the same length n up to 2*105, 1<=ai<=109, 0<=bi<=30. We will permute the sequence ai arbitrarily, and then compute sum(ai>>bi), where >> denotes the bitwise shift right. How many of the n! permutations yield the smallest possible value of that sum? You need to print the answer modulo 998244353. 

Thanks for reading, and check back next week.

Monday, December 15, 2025

A 13-10 week

The Universal Cup Grand Prix of Jinan was the only round of last week (problems, results, top 5 on the left). This season is turning out to be the most one-sided to date, with team USA1, which won all previous seasons, winning 8 out of the first 9 Grand Prix. This particular contest was very one-sided as well, with USA1 winning 13 problems to 10, with more than an hour to spare. Well done!

One slightly disappointing thing about the Universal Cup is that the problem discussions happen on Discord, so they are not publicly searchable/discoverable, which I think is quite bad for the overall programming contest ecosystem: most people won't see those discussions, so they are not inspired to solve and create new problems by seeing the greats share their passion for solving, not to mention the (likely inevitable) day in the far future where the Discord server no longer works, and the information is gone or at least no longer discoverable via old links. I'm wondering if the Universal Cup team has considered holding the discussions on Codeforces, or creating their own public forum instead, or maybe making the Discord discussions visible via a public/indexed page?

In my previous summary, I have mentioned a M(IT)^2 problem: you are given a string of length <=105. You can replace any occurrence of MIT with TIM and vice versa, and do that as many times as you want. What is the maximum number of occurrences of MITIT you can get?

If we look only at every second letter in the string, then what happens is that we swap adjacent M and T in that half-string, but this is only possible if the other half-string has an I in the right place. Since the Is never change, this condition is static. So what actually happens is we find the substrings of the original string that satisfy the following criteria:
  • It has M/T on positions of one parity.
  • It has I on positions of the other parity.
Now consider all maximal substrings (ones that cannot be extended) with this property:
  • They won't overlap so they can be handled independently, since if two such strings overlap then their union is also such string.
  • Each occurrence of MITIT will always be entirely within one of them.
  • We can arbitrarily rearrange Ms and Ts within each of them, since adjacent swaps can produce any permutation.
  • Therefore the maximum number of occurrences of MITIT is min(m, t/2) where m is the number of Ms and t is the number ot Ts.
Finally, we need to be able to implement this cleanly. I suggest to have an outside loop for the parity (whether odd or even positions have Is), then within this loop every position either has a good letter or not, so we can iterate over the string and maintain the values of m and t for the running substring, resetting both to zero when we see a bad letter for a certain position. Something like this:

int res = 0;
for (int p = 0; p < 2; ++p) {
  int m = 0;
  int t = 0;
  for (int i = 0; i <= s.size(); ++i) {
    bool ok = i < s.size() && ((s[i] == 'I') ^ ((i + p) % 2));
    if (ok) {
      if (s[i] == 'M') ++m;
      if (s[i] == 'T') ++t;
    } else { 
      res += min(m, t / 2);
      m = 0;
      t = 0;
    }
  }
}

Thanks for reading, and check back next week!

Friday, December 12, 2025

A winter week

The last weekend was packed with contests. First off, the Yandex Cup 2025 onsite final took place in Istanbul early on Saturday (results, top 5 on the left). The usual suspects topped the scoreboard, and Kevin got the highest score in each problem and earned the well-deserved first place. Congratulations!

This was already the 10th cphof-worthy contest of 2025, and with the addition of the upcoming Hacker Cup and the completed without published results TopCoder Marathon Match Tournament (yes, TopCoder is still around! But does anybody know what happened to the results?) it could be 12. That is a big improvement over the recent years, and the overall record of 17 per year in 2016 does not look too far away. Huge thanks to all organizers, and I'm looking forward to the 2026 editions!

Codeforces Round 1069, which reused some of the problems of the Yandex final, took place in parallel (problems, results, top 5 on the left, analysis). A Kevin won here as well, solving problem D with just 6 minutes remaining in the round. Kudos for the perseverance!

The Universal Cup Grand Prix of Poland also took place on Saturday (problems, results, top 5 on the left). The first Kevin, together with his teammates, needed less than half of the contest to wrap things up, almost 90 minutes faster than everybody else. The closest pursuers were the other veteran teams Almost Retired Dandelion and Amstelpark, and I am of course partial to seeing veteran teams perform well. Congratulations!


The M(IT)^2 25-26 Winter Contest followed on Sunday, starting with the Individual Round (problems, results, top 5 on the left, analysis). The second Kevin came out ahead this time, but he had to wait anxiously for quite some time since Gennady had a big penalty time advantage after the first four problems, and therefore could afford to solve the fifth problem twenty minutes later but still win. This did not happen, so congratulations on the victory!

The easiest problem turned out to be (relatively) challenging for me, as I used 12 minutes to get it right while some others needed only 3. It went like this: you are given a string of length <=105. You can replace any occurrence of MIT with TIM and vice versa, and do that as many times as you want. What is the maximum number of occurrences of MITIT you can get? Can you see how to solve and implement this without too much casework?

The Team Round followed a few hours later (problems, results, top 5 on the left, analysis, award ceremony stream). Finally, no Kevins in the first place: team T1 had Jaehyun, Gennady and Sunghyeon instead. Congratulations!

Thanks for reading, and check back next week!

Monday, December 1, 2025

A 29 week

Meta Hacker Cup 2025 Round 3 on Saturday narrowed the field down to the 25 finalists (problems, results, top 5 on the left, my screencast). After solving B relatively quickly, I got stuck on A for about 50 minutes. The overall solution plan was relatively clear — k=2 is a special case, in all other cases we can achieve the minimum amount derived from the area by solving greedily with some small backtracking or heuristic to deal with the diagonal. However, I could not figure out that heuristic, neither on paper nor by trying things on the computer. When I decided to give up and switch to other problems, a lot of time had already been wasted.

I've then solved D and C relatively quickly again, and also figured out the overall plan for E on paper: we just need to put two 1s at the end and solve recursively, and to achieve that we can first move two 1s into one of the last 6 positions by big swaps that essentially swap the first and second halves of some suffix, and then do some casework to move them to exactly the last 2 positions. However, I got bogged down in that casework, and only a few minutes before the end I realized that I can just implement any bfs, dfs, or bruteforce to solve the problem for n=3 without manual casework, and it was a bit too late, I have finished debugging only by using 5-10 additional minutes after the end of the round.

It was therefore only place 29 for me, and top 25 qualify for the final round :( It is an improvement over place 42 last year, and unlike the AtCoder WTF qualification, here I felt that the qualification was well within reach, and I had only myself to blame for wasting too much time on A. So, I have big hopes to qualify next year!

What is more disappointing (but maybe expected?) is that I did well on quite standard problems B, C and D that were mostly about quick implementation for me, but poorly on the heuristic/ad-hoc problems A and E. Those problems were set nicely in such a way that many approaches work, without squeezing the constraints so much that only a specific heuristic is required, and I enjoy both solving and setting such problems a lot. I think such problems reward the will and ability to experiment using a computer and practical problem solving skills, which are quite general qualities useful outside of the programming competition world. So please do try to solve problems A and E for practice if the above did not spoil them too much, I think you will find it quite enjoyable!

As usual, the strategy does not matter as much if you can just solve everything :) Congratulations to Yui, Gennady and Kevin on the great performance!

The 4th Universal Cup Stage 7: Grand Prix of Zhengzhou also took place on Saturday (problems, results, top 5 on the left). Gennady and Kevin showed up again to claim the first place together with Andrew, even though Yuhao, Lingyu and Qiwen (with an average Codeforces rating even higher than USA1!) put up a good fight and were trying to solve L and overtake them right until the end. Well done to both teams!

Thanks for reading, and check back next week!

Thursday, November 27, 2025

A double prix week

Before I cover the last week, I need to mention the 4th Universal Cup Stage 5: Nanjing, which happened the week before and which I forgot to cover (problems, results, top 5 on the left, analysis in Chinese). Even having only 2 out of 3 teammates did not stop team USA1 from winning by a whole problem. Congratulations!

The Universal Cup can't stick to the plan of having rounds only every other week, so Stage 6: Shenyang took place last week (problems, results, top 5 on the left, analysis in Chinese). The first two places look suspiciously similar to the previous round, but for the veteran teams Polish Mafia and Almost Retired Dandelion who also solved everything this was the best result of the season. Well done!

Codeforces Round 1066 wrapped up the week (problems, results, top 5 on the left, analysis). Some of the Universal Cup stars were also strong this time, but they were complemented in the top 5 by jiangbowen and PEIMUDA who were not at the very top in the team rounds. Congratulations on the nice individual performance!

Thanks for reading, and check back next week for a more meaningful summary!

An array week

Meta Hacker Cup 2025 Round 2 was the first event of the Nov 10 — Nov 16 week (problems, results, top 5 on the left, analysis, my screencast). Once again getting the problems right was more important than solving them fast, but it is still more impressive to do both at the same time. Congratulations to Benq and Um_nik who got quite a margin in penalty time compared to the rest of the full scorers!

In problem D, my solution (like all other solutions) was exponential in k. When I ran it on the validation input, it turned out that one case with k=25 (the maximum) takes about 30 seconds, so I was not sure if 80 such cases would run in time, even with the multithreaded template. But then I realized that the exponential part of the solution depends only on k, so we can precompute it once for all possible values of k, and then solve the 80 testcases in polynomial time. The precomputation only took about 40 seconds for all values of k (since the running time is exponential, k=24 is already much faster than k=25 and so on), and I was able to comfortably submit within the 6 minute window.

What I did not realize is that even if the precomputation took 10 minutes, I would still be able to submit, because one can run the precomputation before downloading the input! And unlike regular contests where the result of the precomputation in such cases must fit into the source limit, here we can just keep it in memory (in case we trust that the program will not crash on the first run) or save it to disk. I never had to do something like this in the Hacker Cup, but will definitely keep this trick in mind!

Codeforces Round 1064 wrapped up the week on Sunday (problems, results, top 5 on the left, analysis). maroonrk was done with 42 minutes to go, but only JDScript0117 was able to join him with 5 problems, and only with 5 minutes remaining in the round. Congratulations to both on solving everything!

In my previous summary, I have mentioned my search for a nice and simple templated treap. I have decided to try building one myself, and you can see the result in action in this submission to problem F2 from Global Round 30 (treap class itself at the top, its usage at the bottom).

The API is modeled on lazysegtree, and it is parametrized with exactly the same types S and F and operations on them, with the same requirements on the composition of operations. A treap contains zero or more blocks, where each block represents an array of values of type S. We support the following operations:

  • t.prod(block) -> S: computes op(...(op(op(e(), a), b), ...) if the given block has elements (a, b, ...). Runs in O(1).
  • t.apply(f, block): replaces each element x of the given block with f(x). Runs in O(1).
  • t.single(s) -> block: creates a new block with a single element with the given value. Runs in O(1).
  • t.destroy(block): destroys a block so that its memory can be reused. Runs in O(block_size).
  • t.concat(block1, block2) -> block: concatenates two blocks into one. The old blocks are consumed by this operation and are no longer valid after it. Runs in O(log(block_size)).
  • t.split_left(block, g) -> (block, block): splits a block into a prefix and a suffix, given a predicate g. The split point is chosen in such a way that both of the following are true: either prefix is empty or g(prod(prefix)) is true; either suffix is empty or g(prod(concat(prefix, first_element(suffix)))) is false. If g is monotonic on prod(prefix), then this will find the point where it changes from true to false. Runs in O(log(block_size)).
  • t.interleave_left(block1, block2, g) -> block: interleaves two blocks into one. The predicate g tells, given the products of a prefix of block1 and a prefix of block2, whether the last element of that prefix of block1 should come before the last element of that prefix of block2. Runs in O(smaller_block_size*log^2(block_size)) maybe?
This API tries to unify and generalize the two common cases of splitting by index and splitting by key. Splitting by index can be achieved by adding a 1 as one of the elements of struct S, implementing op as addition for that element, and using a predicate on that sum for splitting. Splitting by key can be achieved by storing the key as one of the elements of struct S, and implementing op(k1, k2)=k2 (this is what the submission linked above does).

I've also tried to make the API harder to misuse by making the block type moveable but non-copyable, so that the compiler helps make sure that we don't use a block after it has been consumed by an operation. This requires some std::move's in user code, but hopefully that is not too big of a burden.

We can also add more operations that can be expressed using the above ones, but allow a dedicated implementation with a better constant factor, such as insert (instead of single+interleave) or a product on a range (instead of two splits, product, then two concats back). However, I wanted to start with a small API to be able to iterate faster.

It is certainly not as powerful as a fully custom treap, but I think it can express most of the operations from BYOT, with the exception of range reverse and segment tree beats. And for me it is much more convenient to think in terms of concatenating and splitting arrays and applying associative operations on them, rather than directly in terms of treap implementation. What do you think, and do you have improvement suggestions?

Thanks for reading, and check back soon for last week's summary!

Saturday, November 15, 2025

A treap week

Codeforces Global Round 30 was the main event of last week (problems, results, top 5 on the left, analysis, my screencast). I was doing quite well on the first six problems, but got bogged down with implementing treaps with some additional accounting for problem F2, spending more than an hour on that. Right after the contest ended, I had two thoughts:
  1. Wow, the people at the top of the scoreboard are so much better than me at implementation, how did they dig themselves out of this treap mess so fast?
  2. I should implement some kind of templated treap and have it available for the next rounds. Currently I do not have a library of my own, and rely on the AtCoder library which has very nice templated segment trees and other algorithms, but not treaps.
Well, it turns out that the first thought was wrong. Out of the top 9 contestants (those that solved 8 problems), exactly the first three did not have treaps or splay trees in their solution for F2, but only segment trees plus standard STL data structures (while the next 6 had treaps or splay trees)! So these results clearly show that thinking some more to simplify the implementation beats just jumping into the first tedious simplementation approach one comes up with :) Well done to Otomachi_Una, Kevin114514 and dXqwq!

The second thought still stands though. However, it is not obvious to me what is the best templated abstraction for a treap. From the 9 contestants mentioned above, it seems that only hos.lyric has a templated splay tree, but even in her case the abstraction seems to leak the implementation details (push/pull), and the function splitAt might have been added specifically for this problem since it is not aligned with the rest of the abstraction, relying on the custom field "size" of the node that no other methods use. Compare this to AtCoder's lazysegtree, which is defined in abstract mathematical terms of applying two operations on ranges of data.

So, does somebody know a templated treap/splay tree abstraction that expresses the operations in abstract mathematical terms, and yet is as fast and powerful as tinkering with treap's implementation details?

In my previous summary, I have mentioned another Codeforces problem: you are given a tree with n<=5000 vertices. Consider the 2n ways to choose a subset S of its vertices, and for each subset S build a complete graph where S is the vertex set, and every two vertices are connected by an edge with a weight equal to the length of the tree path connecting them. What is the sum of weights of minimum spanning trees of those 2n graphs?

A natural question to ask is: given a particular pair of vertices a and b, in how many of the 2n subsets will they be directly connected by an edge of the minimum spanning tree? If we can answer that question, then we can just add up those answers multiplied by the distance between a and b to obtain the answer to the problem.

The first difficulty on that path is that "the minimum spanning tree" is not well defined, there might be multiple spanning trees of the same minimum weight. To deal with that, we can choose a particular minimum spanning tree, say, the one found by the Kruskal's algorithm that processes the edges in the order of weights, breaking ties in the lexicographical order of the edge ends.

In order for an edge between a and b to be chosen by the algorithm, it is necessary and sufficient that there is no path connecting a and b using the previous edges in the aforementioned order, namely using all edges of a smaller weight, and edges of the same weight that come before in the lexicographical order. Let us check how this path might go along the tree. Let us draw the tree in the following manner: the path from a to b is at the top, and a subtree grows from each of its vertices. We need to get from a to b, stopping only in vertices in the chosen subset S, such that each jump satisfies the above criteria.

Suppose there is a vertex c in S, hanging somewhere from the middle of the path, such that d(a,c)<d(a,b) and d(b,c)<d(a,b), then we can just go from a to c to b, so in those cases the edge between a and b will not be chosen. Suppose c hangs from the vertex x on the path (x might not be in S, so we cannot stop there). Then d(a,c)=d(a,x)+d(x,c), d(a,b)=d(a,x)+d(b,x), d(b,c)=d(b,x)+d(x,c), so the above inequalities translate to d(x,c)<d(b,x) and d(x,c)<d(a,x), in other words d(x,c)<min(d(a,x),d(b,x)). So in order for the edge between a and b to be chosen, we must have d(x,c)>=min(d(a,x),d(b,x)) for all c in S.

Now, suppose that inequality is strict: d(x,c)>min(d(a,x),d(b,x)). Then it's not hard to see that instead of going to/from c when passing through x, we can go directly to/from a or b, therefore such vertices are not useful to construct the path we are looking for. So the existence or nonexistence of the path depends only on the vertices c with d(x,c)<=min(d(a,x),d(b,x)). Since we already know that the path will exist when there is c such that d(x,c)<min(d(a,x),d(b,x)), the only interesting remaining case is where there is one or more vertices c such that d(x,c)=min(d(a,x),d(b,x)).

The distances between such vertices can be equal to d(a,b), so this is where the lexicographical ordering of the edges comes into play. It is possible to carefully consider it and solve the problem, but we can also make another small observation instead: vertices c such that d(x,c)=min(d(a,x),d(b,x)) are exactly those vertices that are at the distance of d(a,b)/2 from point z that is the middle of the path between a and b (note that z is either a vertex or a midpoint of an edge)!

This raises a natural question: what if, instead of focusing on the edge between a and b, we focus on all edges of the spanning tree with the given middle point z? If d is the smallest distance between z and a vertex in S, then from the above argument we can see that only edges of length 2d can exist in the spanning tree. If we root the original tree at z, and count the number m of subtrees of the root that contain a vertex at depth d (none of them will have a vertex at depth less than d by the definition of d), then it's not hard to observe that we will have exactly m-1 edges with length 2d and middle point z in the spanning tree. Note that in case z is a midpoint of an edge, then m<=2.

So we just need to count for each subtree how many ways are there to choose S in this subtree such that the smallest depth is d, and then we can combine those values to obtain the answer using the above observation, and then sum up those answers over all choices of z.

It was not strictly necessary to go through the Kruskal's algorithm and the lexicographical order part. Instead, we can immediately start with the "consider all edges of the spanning tree with the given middle point z" argument, and the solution will be short and beautiful. However, I also wanted to demonstrate how one can arrive at this idea analytically.

Thanks for reading, and check back next week!

Wednesday, November 5, 2025

A QOJ week

Pinely Round 5 started the competitive part of the last week on Thursday (problems, results, top 5 on the left, analysis). This was my first individual appearance in a screenshot on this blog since April 2024, so I think it went pretty well :) I was figuring out the solutions pretty quickly, and did not get stuck in implementation like I often do. However, it was not nearly good enough for the first place, since tourist was both the fastest, and with zero incorrect attempts. Congratulations on the convincing win!

Problem F gave me a nice warm feeling when I figured out the solution, so maybe it will be enjoyable for you as well: you are given a tree with n<=5000 vertices. Consider the 2n ways to choose a subset S of its vertices, and for each subset S build a complete graph where S is the vertex set, and every two vertices are connected by an edge with a weight equal to the length of the tree path connecting them. What is the sum of weights of minimum spanning trees of those 2n graphs?

The 4th Universal Cup Stage 4: Chengdu took place on Saturday, as usual (problems, results, top 5 on the left, analysis in Chinese). Team USA1 won the third stage out of four this year, defying the parity rule. The second place went to a team that is not in the season rating, so I assume this was a team that was participating in the original onsite contest that this stage was a mirror of. Congratulations to both teams on solving all problems!

Yandex Cup 2025 Qualification Round  wrapped up the week on Sunday (results, top 5 on the left). A few people were in contention on the easier problems, but starting from problem J ecnerwala was significantly faster than the competition, so it is not a surprise that only he was able to solve everything this time. Well done!

After the 3rd Universal Cup Semifinals, this was the second big contest that required proctoring for those who want to qualify to the onsite round, and it was certainly not the last one to do so.

In my previous summary, I have mentioned an AtCoder problem: you are given a single integer n<=210, and you need to produce a sequence pi of n integers such that 0<=pi<230, such that for every value of k between 1 and n there exists a number a such that the longest strictly increasing subsequence of the sequence qi=pi&a has length k. Here & denotes bitwise and, in other words we consider a certain subset of bits of pi, the same subset for all i.

Many, if not most, constructive problems that have a single integer n in the input are solved by figuring out a way to produce solutions for 2n and 2n+1 from a solution for n, and this problem was also one of those. The constraints strongly hinted that we need to learn to double n using at most 3 additional bits.

Suppose we have a sequence pi that solves the problem for some n. There are actually two ways to easily double the LIS length: we can either add a new bit at the end, replacing each number pi with two consecutive numbers 2pi and 2pi+1, or add a new bit in the beginning, appending a second copy of the sequence sequence pi to itself, but with all numbers increased by the same value 2t chosen in such a way that all pi<2t. In both cases we set the corresponding bit of a to 1. This way by adding just one more bit we are now able to get all even LIS lengths from 2 to 2n. But what to do about the odd lengths?

Let us use one more bit, and append a number 2r that is bigger than all existing numbers to the end of the (doubled) sequence. Now if we use the same a as before, the LIS length will still be 2k since the new number becomes 0 if the r-th bit of a is not set. However, if we set the r-th bit of a to 1, then the LIS length will be 2k+1, since the new big number can be appended to any increasing sequence. This means that we now have a sequence of length 2n+1 for which we can get any LIS length between 2 and 2n+1. The only missing length is 1, but that is also easy to achieve: just set a=0.

So we can go from n to 2n+1 using 2 additional bits. What about going from n to 2n? Well, actually we can use the "append a big power of two" trick once again, and go from n to 2n+2 using 3 additional bits. Being able to go from n to 2n+1 and 2n+2 means that we can get any number if we also have solutions for the base cases n=1 and n=2.

As is often the case with AtCoder problems, after seeing the solution one cannot fathom struggling for several hours to come up with it, it looks so straightforward :)

I was discussing an old Open Cup gem with my friends recently, motivated by a lecture on binary search for beginner Swiss high school students. I was almost certain there is no way to upsolve that problem now. However, it turns out that this problem (and probably many other old Open Cup contests?) are now available for upsolving on QOJ.ac, the same website that hosts Universal Cup. Huge thanks to the admins of QOJ for preserving the old contests!

Thanks for reading, and check back next week!

Sunday, October 26, 2025

A moonlit week

The Universal Cup Stage 3: Polar was the first event of this week (problems, results, top 5 on the left, still not updated cup standings). In all three stages of this season the first two places went to the same two teams, and in odd-numbered rounds team USA1 comes out on top, if only by less than one wrong submission this time. Congratulations to both season leaders!

AtCoder Grand Contest 074 wrapped up the week just a few hours ago (problems, results, top 5 on the left, analysis, race standings). Just one minute before the end of the round it seemed like Nachia executed a perfect strategy, solving C and B first, then deciding to go for D instead of A since it was clear at that point that A+B+C would not be enough to win the round, and getting D with 15 minutes remaining to go on top with a unique set of problems solved.

However, ksun48 made not one but two submissions on C during the last minute, one of which turned out to be correct. How does one submit, find and fix a bug, and submit again, all within the last minute of the round? Well done, Kevin!

I have spent most of the round on the same problem C. It was clear from the constraints that one should dig in the direction of going from ~n to ~2n using 3 additional bits, and in the end this is exactly how my solution works, but somehow this digging took me more than 2 hours, mostly on paper.

I think it took me so long since I have made a reduction that made the solution more difficult, and did not consider rolling it back (instead, I have persevered and solved the more difficult reduced problem): I was looking for a way to configure the 3 additional bits in such a way that we can go from a LIS of length k for n/2 either to lengths k, 2k and 2k+1 or to lengths k, 2k+1 and 2k+2, depending on the parity of n.

The reason I wanted three options instead of the usual two was that the 2k+x options would not cover small values of LIS, so I thought it would make sense to make sure we also can keep k unchanged; however, on further thought after the round it became clear that the small values not covered are only 1 and possibly 2; and it is always very easy to obtain a LIS of 1 by just making everything equal, and also not hard to obtain a LIS of 2 by making almost everything equal.

Despite a few spoilers above, I hope you can still enjoy solving that problem if you have not already: you are given a single integer n<=210, and you need to produce a sequence pi of n integers such that 0<=pi<230, such that for every value of k between 1 and n there exists a number a such that the longest strictly increasing subsequence of the sequence qi=pi&a has length k. Here & denotes bitwise and, in other words we consider a certain subset of bits of pi, the same subset for all i.

In my previous summary, I have mentioned a Hacker Cup problem: you are given a string of 0s and 1s of length <=600000. Two players are playing a game, making moves in turns. On each move of the first player, they erase any prefix of the string that ends with a 0. On each move of the second player, they erase any suffix of the string that starts with a 1. The player who cannot make a move loses. Who will win if both play optimally?

First of all, the first player always takes a 0, and the second player always takes a 1. Naively, one might think that we should just count if there are more 0s or 1s in the string. However, it is very easy to come up with a counter-example: string 110 has more 1s than 0s, but the first player still wins because they can erase it in one go. The 1s in the beginning of the string don't really matter, as long as there is at least one 0.

However, since having more 0s still seems useful for the first player, a somewhat natural idea is to check: what if the first player tries to make a move that maximizes the balance of the remaining string — the difference between the number of 0s and 1s? Here a counterexample is also not that hard: from the string 0100 we can go to two different strings 100 and 0 with the maximum balance of 1, however if we go to 100 then the second player can take the remaining string and win, while if we go to 0, the second player loses.

Guided by this counterexample, what if we always break ties by choosing the shortest possible string with the maximum balance? After spending some time finding yet another counterexample to this approach and failing, I've decided to look for a proof that it works instead.

Suppose we have used that strategy for the first move, and the remaining string has some non-negative balance (it feels intuitive that the balance has to be non-negative for us to win). What happens to the balance after the move of the second player? After some experimentation, we can notice that it always becomes strictly positive. And indeed, it is not hard to prove by contradiction: suppose it is non-positive after the move of the second player. Suppose the string remaining after the move of the first player is s, and the second player has removed then removed the suffix u, and what remained is t, so s=tu. 

The balance of s is non-negative, the balance of t is non-positive, and u starts with a 1. Since the balance of u is the balance of s minus the balance of t, is is also non-negative, so it must have a 0. Consider its first 0, which splits the string u as u=v0w, where v consists only of 1s. But then the first player could have additionally erased the string tv0 in their first move. Since the balance of t is non-positive, and the balance of v0 is non-positive, the balance of tv0 is non-positive, which means that the balance of w is the same or higher than the balance of s=tv0w. But this contradicts the choice of s.

So, after the move of the second player the balance will become strictly positive. But then we can apply the same approach (make the move that maximizes the balance of the remaining string, breaking ties to shorter remaining strings), and the balance of the remaining string will be non-negative (since at least if we take the first 0 it is non-negative, and we maximize it), so we can repeat this until we win.

We can already submit our solution at this point, but formally speaking we also need to prove that if the first player cannot make a move that leaves a string with a non-negative balance, then the second player wins. And indeed, we can apply the same argument as above: if we have left a string with a strictly negative balance, then from the point of view of the second player it has a strictly positive balance, and they can apply exactly the same strategy as above to win.

This solution is longer than the one in the editorial, but it is how I solved the problem during the round, and I hope it shows how to arrive at a solution without having to pull an idea out of thin air, for example an idea to consider balanced parentheses sequences as the editorial does.

Thanks for reading, and check back next week!