ekzhang's blog

By ekzhang, history, 2 months ago, In English


During Facebook Hacker Cup Round 2 today, Problem D caught my attention. It reminded me of a particular data structure called a kinetic tournament, which is not very well known in this community. However, it offers an extremely clean and slick solution to the problem, in my opinion.

I first learned about this data structure from dragonslayerintraining, who described a variant of it in this Codeforces blog comment. Since the data structure is so interesting, I feel like it deserves a longer explanation, some template code, and more examples. That's why I am writing this blog post.

Kinetic Tournaments

Briefly, the functionality of the data structure is a mix between a line container, i.e., "convex hull trick", and a segment tree.

Suppose that you have an array containing pairs of nonnegative integers, $$$A[i]$$$ and $$$B[i]$$$. You also have a global parameter $$$T$$$, corresponding to the "temperature" of the data structure. Your goal is to support the following queries on this data:

  • update(i, a, b): set $$$A[i] = \text a$$$ and $$$B[i] = \text b$$$
  • query(s, e): return $$$\displaystyle \min_{s \leq i \leq e} A[i] * T + B[i]$$$
  • heaten(new_temp): set $$$T = \text{new_temp}$$$
    • [precondition: new_temp >= current value of T]

(For simplicity, we set A[i] = 0 and B[i] = LLONG_MAX for uninitialized entries, which should not change the query results.)

This allows you to essentially do arbitrary lower convex hull queries on a collection of lines, as well as any contiguous subcollection of those lines. This is more powerful than standard convex hull tricks and related data structures (Li-Chao Segment Tree) for three reasons:

  • You can arbitrarily remove/edit lines, not just add them.
  • Dynamic access to any subinterval of lines, which lets you avoid costly merge small-to-large operations in some cases.
  • Easy to reason about and implement from scratch, unlike dynamic CHT.

The tradeoff is that you can only query sequential values (temperature is only allowed to increase) for amortization reasons, but this happens to be a fairly common case in many problems.

Here's the kicker. If you implement the data structure correctly, you get the following time complexities:

  • query: $$$O(\log n)$$$
  • update: $$$O(\log n)$$$
  • heaten: $$$O(\log^2 n)$$$ [amortized]

(The runtime complexity might actually be $$$O(m \alpha(m) \log^2 n)$$$ instead of $$$O(m \log^2 n)$$$ if your updates include both adding and removing lines, and you're also very careful about constructing an example. See the discussion below from Elegia about Davenport-Schinzel sequences.)


How does it work? With kinetic tournaments, you essentially build a min segtree as usual, but you add a global priority queue that stores whenever a "contract" breaks. We can put this in the analogy of increasing temperature. For every node, you store the temperatures at which that node "melts", meaning that the minimum value changes from one child to the other. Then, you can just keep popping events from this priority queue as your data structure heats up.

Kinetic tournament overview

However, this unfortunately can be slow if you're not careful, which isn't good enough, especially if you have many concurrent lines (might give you quadratic runtime, depending on implementation of priority queue). Luckily, there's a neat implementation trick that circumvents the priority queue and prevents this possible quadratic runtime. In every node of the segment tree, you store the minimum temperature at which any part of its subtree could melt. This fits in naturally with the segment tree definition.

To optimize the data structure, you do not recompute at every certificate failure. Instead, whenever the user calls heaten(), you batch all of the recompute operations and recursively rebuild only the parts of the segment tree that changed, once.

The runtime analysis follows from comparing the data structure to computing a convex hull by divide-and-conquer (thanks dragonslayerintraining!). The number of times each node "melts" is bounded at most by the number of leaves in its subtree. This is because you can never have two lines, $$$A$$$ and $$$B$$$, such that $$$A(x_1) < B(x_1)$$$, $$$A(x_2) > B(x_2)$$$, and $$$A(x_3) < B(x_3)$$$, where $$$x_1 < x_2 < x_3$$$. In other words, as you increase temperature, the number of recalculations is at most $$$O(n \log n)$$$. Combining this with the fact that we have to walk down the segment tree, which has logarithmic depth, every time we do a recalculation, this yields a final amortized time complexity of $$$O(\log^2 n)$$$ for each "heaten" operation.


You can find my implementation of a modified kinetic tournament here:


It defines a single struct with template type (~100 LoC), which has a plug-and-play interface that you can use in your own solutions, and no global variable pollution. Example usage:

int N = 2;
kinetic_tournament<long long> kt(N);

kt.update(0, 2, 10);  // 2t + 10
kt.update(1, 4, 1);   // 4t + 1

kt.heaten(1);         // t = 1
kt.query(0, 0);       // => 2*1 + 10 = 12
kt.query(1, 1);       // => 4*1 + 1 = 5
kt.query(0, 1);       // => min(2*1 + 10, 4*1 + 1) = 5

kt.heaten(5);         // t = 5
kt.query(0, 0);       // => 2*5 + 10 = 20
kt.query(1, 1);       // => 4*5 + 1 = 21
kt.query(0, 1);       // => min(2*5 + 10, 4*5 + 1) = 20

I've verified this implementation on Problem D from Facebook Hacker Cup Round 2, titled "Log Drivin' Hirin'". In this problem, you can simply sort the queries and nodes in order of increasing "carelessness", and the kinetic tournament finishes the task. Full solution is available below. It runs in less than 5 seconds on the entire test input.



I hope to see more of this interesting data structure pop up in competitive programming! Hopefully this explanatory blog post is useful for you, and please let me know if you find any errors.

Read more »

  • Vote: I like it
  • +242
  • Vote: I do not like it

By ekzhang, history, 5 months ago, In English

Hello all,

Like you, I enjoy solving Codeforces problems on the go. However, sometimes I am not working on my computer with all my environment and setup. There are many automated problem parsers and test runners to help make solving Codeforces problems painless, but they all require a complicated installation process.

About a year ago, I was introduced to CS Academy, which I thought had an amazing interface for solving problems. It shows the problems and your code side-by-side, and you can automatically run your program on the example test cases. Also, there is auto-saving in case you accidentally close your window. Why can't we have this for Codeforces?

I couldn't find anything on the web that was easy to set up and fun to use, so I made my own online workspace at wkspace.herokuapp.com.

Screenshot of wkspace

Here are some features:

  • No sign-up or installation required
  • Parses Codeforces problems and displays them side-by-side with code
  • One click to run your code on all example test cases (for non-interactive problems)
  • One click to submit your code to Codeforces (if logged in)
  • Auto-saving of code in the cloud
  • List workspaces you have recently edited
  • Share a read-only paste of your code by link
  • Support for new versions of the most common languages (C, C++, Python, Java, Ruby)
  • Configurable themes (Monokai, Dawn, Solarized)
  • Configurable keybindings (Vim, Emacs)

I have been using and developing this myself for the past year, and it is fairly stable now, so I would like to share it with you. Please let me know if you have any feedback on how to improve it!

Note: The code is open source and available at https://github.com/ekzhang/wkspace.

Thanks to Herman Došilović for making the Judge0 API, which is used in this project.

Read more »

  • Vote: I like it
  • +419
  • Vote: I do not like it

By ekzhang, history, 21 month(s) ago, In English

Hello everyone!

I would like to invite you to participate in the Plano West High School Programming Contest online round. It will be held on Saturday, February 9th at 15:00 EST.

The contest will be 3 hours long and scored by the sum of points for problems solved (harder problems worth more points). There will be a wide range of problem difficulties, from Codeforces Div3 level to about Div1 B/C. Competitors in the second and third divisions will most likely find it interesting.

The problems have been prepared by students at my school: me (ekzhang), Vincent Huang (tastymath75025), Autumn Tan, Wuyou Xie, Sam Ziegelbein, and Timothy Qin. We would also like to thank Kevin Meng (mtr361) and Maxwell Jiang (rocketscience) for helping test the round.

Please visit this link to register for the contest. The round will be held on HackerRank (binary scoring will be used), and a live scoreboard will be available.

Good luck and and have fun!

UPD: Contest starts in 10 minutes!

UPD 2: Contest is over, thanks to everyone for participating! We encourage you to try upsolving the problems; the model solutions, test data, and a PDF of the problem packet are available here: https://tinyurl.com/pwshpc

Read more »

  • Vote: I like it
  • +38
  • Vote: I do not like it

By ekzhang, history, 3 years ago, In English

I recently submitted twice to problem 865C/866C (same problem), Gotta Go Fast.



The first submission got wrong answer on test 1 on Codeforces. But I tested the solution both locally and on Ideone, and the answer was correct.

In the second submission my only change was adding the following one "useless" line of code to a for loop:

if (hi > 2 && hi < 1) cout << "this should never run" << endl;

It got AC.

My question is, why is this happening? I've tried debugging it locally and looking at the assembly, but there are no bugs when it runs on my local machine or on Ideone; it's only acting strangely on Codeforces. I've been staring at this code for many hours now thinking it might be undefined behavior, but I've tried many slightly different submissions (with long double, without the call to min) that work just fine without the extra useless if statement.

Tl;dr: I am utterly confused and would greatly appreciate if you could help me find where my UB is, or if there's some other problem with floating-point precision :-)


Read more »

  • Vote: I like it
  • +28
  • Vote: I do not like it

By ekzhang, history, 3 years ago, In English

Four team members from the US:

  • Zhezheng Luo (C_SUNSHINE)
  • Dhruv Rohatgi (pacu)
  • Ben Qi (Benq)
  • Eric Zhang (ekzhang)

Read more »

  • Vote: I like it
  • +78
  • Vote: I do not like it

By ekzhang, history, 4 years ago, In English

Hi, I am currently trying an O(n(sqrt n)(lg n)) to problem 375D using Mo's Algorithm and a Fenwick Tree. My initial solution got a time limit exceeded on test case 53, so I tried to optimize it.

To optimize, I made a couple of changes to my check() function, that is called every time the Mo's Algorithm window is slid 1 tree node. I was thinking that this would make the algorithm run faster since check is the part that is run the most (n^1.5 lg n times).

My second submission didn't end up working. In fact, somehow, my new submission got TLE on test case 5, which the previous code could solve in 150ms!

Can someone help me figure out what could've happened that would make this attempted optimization run 10x slower?

Original Submission: http://codeforces.com/contest/375/submission/18813131

Second Submission: http://codeforces.com/contest/375/submission/18813311

Diff: https://www.diffchecker.com/nhyeayxp

Thank you very much!

Read more »

  • Vote: I like it
  • 0
  • Vote: I do not like it

By ekzhang, history, 5 years ago, In English

Hi, could someone help me on this problem? I keep getting WA on test case 74, and I can't figure out why. This is my latest submission without debugging code.

My code works like this: I first turn all the replies into [L, R] ranges on the bottom row. Then I take all the "yes" replies, and use all of them to get an initial, single [lo, hi] bound on the exit node. I sort the "no" replies in ascending order of L, then iterate through them, while updating lo. If at any time, lo > hi, we can end the loop.

For each of the "no" replies, check if there's any gap between lo and L. If there is, those gaps are all possible exits, and deal with them like that. Regardless, set lo to be max(lo, R + 1) at the end of every loop, as we've handled all possible nodes  ≤ R.

At the end of the program, do some casework, and output different answers based on that.

The program works for the first 73 cases, but it outputs "Data not sufficient!" for test case 74, which is incorrect. Help finding the bug would be greatly appreciated!

Read more »

  • Vote: I like it
  • +15
  • Vote: I do not like it