IOI 2020 - Tickets
There are n colors and m tickets per color. Ticket j of color i has value x[i][j] (sorted in non-decreasing order within each color). There are k rounds. In each round, exactly one ticket from each color is used (each...
Source-first archive entry
This page is built from the copied files in competitive_programming/ioi/2020/tickets. Edit
competitive_programming/ioi/2020/tickets/solution.tex to update the written solution and
competitive_programming/ioi/2020/tickets/solution.cpp to update the implementation.
The website does not replace those files with hand-maintained HTML. It reads the copied source tree during the build and exposes the exact files below.
Problem Statement
Rendered from the "Problem Summary" section inside solution.tex because this entry has no separate statement file.
There are $n$ colors and $m$ tickets per color. Ticket $j$ of color $i$ has value $x[i][j]$ (sorted in non-decreasing order within each color). There are $k$ rounds. In each round, exactly one ticket from each color is used (each ticket is used in at most one round). The prize for a round is the sum of absolute differences from the median of the chosen tickets' values.
More precisely, if tickets with values $v_0, v_1, \ldots, v_{n-1}$ are chosen ($n$ is even), the game master picks a value $b$ that minimizes $\sum |v_i - b|$, which is achieved by the median. The prize equals $\sum |v_i - \text{median}|$, which equals the sum of the top $n/2$ values minus the sum of the bottom $n/2$ values.
Maximize the total prize across all $k$ rounds.
Editorial
Rendered from the copied solution.tex file. The original TeX source remains
available below.
Solution Approach
Key Observation
Since $n$ is even, the prize for a round with sorted values $v_0 \le v_1 \le \cdots \le v_{n-1}$ is: \[ \text{prize} = \sum_{i=n/2}^{n-1} v_i - \sum_{i=0}^{n/2-1} v_i \]
So we want to maximize the total prize. Each color contributes $k$ tickets across rounds: some are ``positive'' (among the top $n/2$ in their round, contributing $+v$) and some are ``negative'' (among the bottom $n/2$, contributing $-v$).
For each color, exactly $k$ tickets are used. Among these $k$ tickets, some number $p_i$ are positive and $k - p_i$ are negative. The constraint is $\sum_i p_i = k \cdot n/2$ (each round needs exactly $n/2$ positive tickets).
Greedy Strategy
To maximize total prize:
For positive contributions, use the largest tickets: tickets $x[i][m-1], x[i][m-2], \ldots, x[i][m-p_i]$.
For negative contributions, use the smallest tickets: tickets $x[i][0], x[i][1], \ldots, x[i][k-p_i-1]$.
Total contribution of color $i$: $\sum_{j=0}^{k-p_i-1} (-x[i][j]) + \sum_{j=m-p_i}^{m-1} x[i][j]$.
Optimal $p_i$ Values
Start with $p_i = 0$ for all $i$ (all negative). Then greedily increase $p_i$ values to maximize marginal gain. Increasing $p_i$ from $t$ to $t+1$: gain = $x[i][m-t-1] + x[i][k-t-1]$ (add a large positive and remove a small negative). Use a priority queue to select the best increments until $\sum p_i = k \cdot n/2$.
C++ Solution
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
long long find_maximum(int k, vector<vector<int>> x) {
int n = x.size(), m = x[0].size();
int half = n / 2;
// p[i] = number of "positive" uses for color i
vector<int> p(n, 0);
// Initially all p[i] = 0: all k tickets used negatively
// Marginal gain of incrementing p[i] from t to t+1:
// gain = x[i][m - t - 1] + x[i][k - 1 - t]
// (add top ticket as positive, remove bottom ticket from negative)
// Priority queue: (gain, color_index)
priority_queue<pair<ll, int>> pq;
for (int i = 0; i < n; i++) {
ll gain = (ll)x[i][m - 1] + x[i][k - 1];
pq.push({gain, i});
}
int total_positive = k * half;
ll total_prize = 0;
// Initial total with all negative:
for (int i = 0; i < n; i++)
for (int j = 0; j < k; j++)
total_prize -= x[i][j];
while (total_positive > 0) {
auto [gain, i] = pq.top(); pq.pop();
total_prize += gain;
p[i]++;
total_positive--;
if (p[i] < k) {
int t = p[i];
ll new_gain = (ll)x[i][m - t - 1] + x[i][k - 1 - t];
pq.push({new_gain, i});
}
}
// Now assign tickets to rounds
// For color i: positive tickets are x[i][m-p[i]], ..., x[i][m-1]
// negative tickets are x[i][0], ..., x[i][k-p[i]-1]
// Build the allocation matrix s[i][j] = round number or -1
vector<vector<int>> s(n, vector<int>(m, -1));
// Assign rounds: each round needs exactly n/2 positive and n/2 negative
// Collect (color, ticket_index, is_positive) and assign to rounds
// Simple assignment: sort colors by p[i], assign positive tickets round by round
// Use a greedy matching.
// For each color, create a list of (round assignments)
// Positive tickets: assign to rounds 0..p[i]-1 (will fix later)
// Negative tickets: assign to rounds p[i]..k-1
// Actually, we need each round to have exactly n/2 positive colors.
// This is a bipartite matching problem, but can be solved greedily.
// Create a list of (color, round_index) for positive assignments
// Each color i has p[i] positive assignments.
// Use a round-robin approach:
vector<vector<int>> pos_rounds(n), neg_rounds(n);
// Assign positive rounds: distribute using priority queue
// Each round needs exactly half colors as positive
vector<int> pos_count(k, 0);
vector<pair<int,int>> color_order; // (p[i], i) sorted descending
for (int i = 0; i < n; i++)
color_order.push_back({p[i], i});
sort(color_order.rbegin(), color_order.rend());
// Greedy: for each color (in order of most positive), assign to rounds with fewest positive
priority_queue<pair<int,int>, vector<pair<int,int>>, greater<>> round_pq;
for (int r = 0; r < k; r++) round_pq.push({0, r});
for (auto [pi, i] : color_order) {
vector<pair<int,int>> tmp;
for (int t = 0; t < pi; t++) {
auto [cnt, r] = round_pq.top(); round_pq.pop();
pos_rounds[i].push_back(r);
tmp.push_back({cnt + 1, r});
}
for (auto& pr : tmp) round_pq.push(pr);
}
// Assign negative rounds: remaining rounds for each color
for (int i = 0; i < n; i++) {
set<int> pos_set(pos_rounds[i].begin(), pos_rounds[i].end());
for (int r = 0; r < k; r++)
if (!pos_set.count(r))
neg_rounds[i].push_back(r);
}
// Fill allocation
for (int i = 0; i < n; i++) {
// Positive: largest tickets -> sort positive rounds doesn't matter
sort(pos_rounds[i].begin(), pos_rounds[i].end());
for (int t = 0; t < p[i]; t++) {
int ticket = m - p[i] + t;
int round = pos_rounds[i][t];
s[i][ticket] = round;
}
// Negative: smallest tickets
sort(neg_rounds[i].begin(), neg_rounds[i].end());
for (int t = 0; t < k - p[i]; t++) {
int ticket = t;
int round = neg_rounds[i][t];
s[i][ticket] = round;
}
}
// allocate_tickets(s); // IOI grader call
// For local testing:
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++)
printf("%d ", s[i][j]);
printf("\n");
}
return total_prize;
}
int main() {
int n, m, k;
scanf("%d %d %d", &n, &m, &k);
vector<vector<int>> x(n, vector<int>(m));
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++)
scanf("%d", &x[i][j]);
printf("%lld\n", find_maximum(k, x));
return 0;
}
Complexity Analysis
Time complexity: $O(nk \log(nk))$ for the priority queue operations during greedy selection and round assignment.
Space complexity: $O(nm)$ for the allocation matrix.
Code
Exact copied C++ implementation from solution.cpp.
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
// IOI 2020 - Tickets
// n colors, m tickets each (sorted), k rounds. Each round picks one ticket
// per color. Prize = sum of top n/2 values minus sum of bottom n/2 values.
// Maximize total prize across all rounds.
//
// Greedy: start with all tickets used negatively (smallest k tickets).
// Incrementally convert tickets to positive (largest) to maximize gain.
// Use priority queue on marginal gain of each conversion.
long long find_maximum(int k, vector<vector<int>> x) {
int n = (int)x.size(), m = (int)x[0].size();
int half = n / 2;
// p[i] = number of "positive" (top-half) uses for color i
vector<int> p(n, 0);
// Initial total with all k tickets used negatively per color
ll total_prize = 0;
for (int i = 0; i < n; i++)
for (int j = 0; j < k; j++)
total_prize -= x[i][j];
// Priority queue: (marginal gain, color_index)
// Gain of incrementing p[i] from t to t+1:
// x[i][m-t-1] + x[i][k-1-t]
priority_queue<pair<ll, int>> pq;
for (int i = 0; i < n; i++) {
ll gain = (ll)x[i][m - 1] + x[i][k - 1];
pq.push({gain, i});
}
int total_positive = k * half;
while (total_positive > 0) {
auto [gain, i] = pq.top();
pq.pop();
total_prize += gain;
p[i]++;
total_positive--;
if (p[i] < k) {
int t = p[i];
ll new_gain = (ll)x[i][m - t - 1] + x[i][k - 1 - t];
pq.push({new_gain, i});
}
}
// Assign tickets to rounds ensuring each round has exactly n/2 positive
vector<vector<int>> s(n, vector<int>(m, -1));
vector<vector<int>> pos_rounds(n), neg_rounds(n);
// Greedy round assignment: process colors by decreasing p[i],
// assign each to the rounds with fewest positive slots filled
vector<pair<int, int>> color_order;
for (int i = 0; i < n; i++)
color_order.push_back({p[i], i});
sort(color_order.rbegin(), color_order.rend());
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<>> round_pq;
for (int r = 0; r < k; r++) round_pq.push({0, r});
for (auto [pi, i] : color_order) {
vector<pair<int, int>> tmp;
for (int t = 0; t < pi; t++) {
auto [cnt, r] = round_pq.top();
round_pq.pop();
pos_rounds[i].push_back(r);
tmp.push_back({cnt + 1, r});
}
for (auto& pr : tmp) round_pq.push(pr);
}
// Negative rounds: all rounds not assigned as positive
for (int i = 0; i < n; i++) {
set<int> pos_set(pos_rounds[i].begin(), pos_rounds[i].end());
for (int r = 0; r < k; r++)
if (!pos_set.count(r))
neg_rounds[i].push_back(r);
}
// Fill the allocation matrix
for (int i = 0; i < n; i++) {
sort(pos_rounds[i].begin(), pos_rounds[i].end());
for (int t = 0; t < p[i]; t++) {
int ticket = m - p[i] + t; // largest tickets
s[i][ticket] = pos_rounds[i][t];
}
sort(neg_rounds[i].begin(), neg_rounds[i].end());
for (int t = 0; t < k - p[i]; t++) {
int ticket = t; // smallest tickets
s[i][ticket] = neg_rounds[i][t];
}
}
// Output allocation
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++)
printf("%d ", s[i][j]);
printf("\n");
}
return total_prize;
}
int main() {
int n, m, k;
scanf("%d %d %d", &n, &m, &k);
vector<vector<int>> x(n, vector<int>(m));
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++)
scanf("%d", &x[i][j]);
printf("%lld\n", find_maximum(k, x));
return 0;
}
Source Files
Exact copied source-of-truth files. Edit solution.tex for the write-up and solution.cpp for the implementation.
\documentclass[11pt,a4paper]{article}
\usepackage[utf8]{inputenc}
\usepackage[T1]{fontenc}
\usepackage{amsmath,amssymb,amsthm}
\usepackage{listings}
\usepackage{geometry}
\usepackage{hyperref}
\geometry{margin=2.5cm}
\lstset{
language=C++,
basicstyle=\ttfamily\small,
keywordstyle=\bfseries,
breaklines=true,
numbers=left,
numberstyle=\tiny,
frame=single,
tabsize=2
}
\title{IOI 2020 -- Tickets}
\author{Solution}
\date{}
\begin{document}
\maketitle
\section{Problem Summary}
There are $n$ colors and $m$ tickets per color. Ticket $j$ of color $i$ has value $x[i][j]$ (sorted in non-decreasing order within each color). There are $k$ rounds. In each round, exactly one ticket from each color is used (each ticket is used in at most one round). The prize for a round is the sum of absolute differences from the median of the chosen tickets' values.
More precisely, if tickets with values $v_0, v_1, \ldots, v_{n-1}$ are chosen ($n$ is even), the game master picks a value $b$ that minimizes $\sum |v_i - b|$, which is achieved by the median. The prize equals $\sum |v_i - \text{median}|$, which equals the sum of the top $n/2$ values minus the sum of the bottom $n/2$ values.
Maximize the total prize across all $k$ rounds.
\section{Solution Approach}
\subsection{Key Observation}
Since $n$ is even, the prize for a round with sorted values $v_0 \le v_1 \le \cdots \le v_{n-1}$ is:
\[ \text{prize} = \sum_{i=n/2}^{n-1} v_i - \sum_{i=0}^{n/2-1} v_i \]
So we want to maximize the total prize. Each color contributes $k$ tickets across rounds: some are ``positive'' (among the top $n/2$ in their round, contributing $+v$) and some are ``negative'' (among the bottom $n/2$, contributing $-v$).
For each color, exactly $k$ tickets are used. Among these $k$ tickets, some number $p_i$ are positive and $k - p_i$ are negative. The constraint is $\sum_i p_i = k \cdot n/2$ (each round needs exactly $n/2$ positive tickets).
\subsection{Greedy Strategy}
To maximize total prize:
\begin{itemize}
\item For positive contributions, use the largest tickets: tickets $x[i][m-1], x[i][m-2], \ldots, x[i][m-p_i]$.
\item For negative contributions, use the smallest tickets: tickets $x[i][0], x[i][1], \ldots, x[i][k-p_i-1]$.
\item Total contribution of color $i$: $\sum_{j=0}^{k-p_i-1} (-x[i][j]) + \sum_{j=m-p_i}^{m-1} x[i][j]$.
\end{itemize}
\subsection{Optimal $p_i$ Values}
Start with $p_i = 0$ for all $i$ (all negative). Then greedily increase $p_i$ values to maximize marginal gain. Increasing $p_i$ from $t$ to $t+1$: gain = $x[i][m-t-1] + x[i][k-t-1]$ (add a large positive and remove a small negative). Use a priority queue to select the best increments until $\sum p_i = k \cdot n/2$.
\section{C++ Solution}
\begin{lstlisting}
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
long long find_maximum(int k, vector<vector<int>> x) {
int n = x.size(), m = x[0].size();
int half = n / 2;
// p[i] = number of "positive" uses for color i
vector<int> p(n, 0);
// Initially all p[i] = 0: all k tickets used negatively
// Marginal gain of incrementing p[i] from t to t+1:
// gain = x[i][m - t - 1] + x[i][k - 1 - t]
// (add top ticket as positive, remove bottom ticket from negative)
// Priority queue: (gain, color_index)
priority_queue<pair<ll, int>> pq;
for (int i = 0; i < n; i++) {
ll gain = (ll)x[i][m - 1] + x[i][k - 1];
pq.push({gain, i});
}
int total_positive = k * half;
ll total_prize = 0;
// Initial total with all negative:
for (int i = 0; i < n; i++)
for (int j = 0; j < k; j++)
total_prize -= x[i][j];
while (total_positive > 0) {
auto [gain, i] = pq.top(); pq.pop();
total_prize += gain;
p[i]++;
total_positive--;
if (p[i] < k) {
int t = p[i];
ll new_gain = (ll)x[i][m - t - 1] + x[i][k - 1 - t];
pq.push({new_gain, i});
}
}
// Now assign tickets to rounds
// For color i: positive tickets are x[i][m-p[i]], ..., x[i][m-1]
// negative tickets are x[i][0], ..., x[i][k-p[i]-1]
// Build the allocation matrix s[i][j] = round number or -1
vector<vector<int>> s(n, vector<int>(m, -1));
// Assign rounds: each round needs exactly n/2 positive and n/2 negative
// Collect (color, ticket_index, is_positive) and assign to rounds
// Simple assignment: sort colors by p[i], assign positive tickets round by round
// Use a greedy matching.
// For each color, create a list of (round assignments)
// Positive tickets: assign to rounds 0..p[i]-1 (will fix later)
// Negative tickets: assign to rounds p[i]..k-1
// Actually, we need each round to have exactly n/2 positive colors.
// This is a bipartite matching problem, but can be solved greedily.
// Create a list of (color, round_index) for positive assignments
// Each color i has p[i] positive assignments.
// Use a round-robin approach:
vector<vector<int>> pos_rounds(n), neg_rounds(n);
// Assign positive rounds: distribute using priority queue
// Each round needs exactly half colors as positive
vector<int> pos_count(k, 0);
vector<pair<int,int>> color_order; // (p[i], i) sorted descending
for (int i = 0; i < n; i++)
color_order.push_back({p[i], i});
sort(color_order.rbegin(), color_order.rend());
// Greedy: for each color (in order of most positive), assign to rounds with fewest positive
priority_queue<pair<int,int>, vector<pair<int,int>>, greater<>> round_pq;
for (int r = 0; r < k; r++) round_pq.push({0, r});
for (auto [pi, i] : color_order) {
vector<pair<int,int>> tmp;
for (int t = 0; t < pi; t++) {
auto [cnt, r] = round_pq.top(); round_pq.pop();
pos_rounds[i].push_back(r);
tmp.push_back({cnt + 1, r});
}
for (auto& pr : tmp) round_pq.push(pr);
}
// Assign negative rounds: remaining rounds for each color
for (int i = 0; i < n; i++) {
set<int> pos_set(pos_rounds[i].begin(), pos_rounds[i].end());
for (int r = 0; r < k; r++)
if (!pos_set.count(r))
neg_rounds[i].push_back(r);
}
// Fill allocation
for (int i = 0; i < n; i++) {
// Positive: largest tickets -> sort positive rounds doesn't matter
sort(pos_rounds[i].begin(), pos_rounds[i].end());
for (int t = 0; t < p[i]; t++) {
int ticket = m - p[i] + t;
int round = pos_rounds[i][t];
s[i][ticket] = round;
}
// Negative: smallest tickets
sort(neg_rounds[i].begin(), neg_rounds[i].end());
for (int t = 0; t < k - p[i]; t++) {
int ticket = t;
int round = neg_rounds[i][t];
s[i][ticket] = round;
}
}
// allocate_tickets(s); // IOI grader call
// For local testing:
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++)
printf("%d ", s[i][j]);
printf("\n");
}
return total_prize;
}
int main() {
int n, m, k;
scanf("%d %d %d", &n, &m, &k);
vector<vector<int>> x(n, vector<int>(m));
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++)
scanf("%d", &x[i][j]);
printf("%lld\n", find_maximum(k, x));
return 0;
}
\end{lstlisting}
\section{Complexity Analysis}
\begin{itemize}
\item \textbf{Time complexity:} $O(nk \log(nk))$ for the priority queue operations during greedy selection and round assignment.
\item \textbf{Space complexity:} $O(nm)$ for the allocation matrix.
\end{itemize}
\end{document}
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
// IOI 2020 - Tickets
// n colors, m tickets each (sorted), k rounds. Each round picks one ticket
// per color. Prize = sum of top n/2 values minus sum of bottom n/2 values.
// Maximize total prize across all rounds.
//
// Greedy: start with all tickets used negatively (smallest k tickets).
// Incrementally convert tickets to positive (largest) to maximize gain.
// Use priority queue on marginal gain of each conversion.
long long find_maximum(int k, vector<vector<int>> x) {
int n = (int)x.size(), m = (int)x[0].size();
int half = n / 2;
// p[i] = number of "positive" (top-half) uses for color i
vector<int> p(n, 0);
// Initial total with all k tickets used negatively per color
ll total_prize = 0;
for (int i = 0; i < n; i++)
for (int j = 0; j < k; j++)
total_prize -= x[i][j];
// Priority queue: (marginal gain, color_index)
// Gain of incrementing p[i] from t to t+1:
// x[i][m-t-1] + x[i][k-1-t]
priority_queue<pair<ll, int>> pq;
for (int i = 0; i < n; i++) {
ll gain = (ll)x[i][m - 1] + x[i][k - 1];
pq.push({gain, i});
}
int total_positive = k * half;
while (total_positive > 0) {
auto [gain, i] = pq.top();
pq.pop();
total_prize += gain;
p[i]++;
total_positive--;
if (p[i] < k) {
int t = p[i];
ll new_gain = (ll)x[i][m - t - 1] + x[i][k - 1 - t];
pq.push({new_gain, i});
}
}
// Assign tickets to rounds ensuring each round has exactly n/2 positive
vector<vector<int>> s(n, vector<int>(m, -1));
vector<vector<int>> pos_rounds(n), neg_rounds(n);
// Greedy round assignment: process colors by decreasing p[i],
// assign each to the rounds with fewest positive slots filled
vector<pair<int, int>> color_order;
for (int i = 0; i < n; i++)
color_order.push_back({p[i], i});
sort(color_order.rbegin(), color_order.rend());
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<>> round_pq;
for (int r = 0; r < k; r++) round_pq.push({0, r});
for (auto [pi, i] : color_order) {
vector<pair<int, int>> tmp;
for (int t = 0; t < pi; t++) {
auto [cnt, r] = round_pq.top();
round_pq.pop();
pos_rounds[i].push_back(r);
tmp.push_back({cnt + 1, r});
}
for (auto& pr : tmp) round_pq.push(pr);
}
// Negative rounds: all rounds not assigned as positive
for (int i = 0; i < n; i++) {
set<int> pos_set(pos_rounds[i].begin(), pos_rounds[i].end());
for (int r = 0; r < k; r++)
if (!pos_set.count(r))
neg_rounds[i].push_back(r);
}
// Fill the allocation matrix
for (int i = 0; i < n; i++) {
sort(pos_rounds[i].begin(), pos_rounds[i].end());
for (int t = 0; t < p[i]; t++) {
int ticket = m - p[i] + t; // largest tickets
s[i][ticket] = pos_rounds[i][t];
}
sort(neg_rounds[i].begin(), neg_rounds[i].end());
for (int t = 0; t < k - p[i]; t++) {
int ticket = t; // smallest tickets
s[i][ticket] = neg_rounds[i][t];
}
}
// Output allocation
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++)
printf("%d ", s[i][j]);
printf("\n");
}
return total_prize;
}
int main() {
int n, m, k;
scanf("%d %d %d", &n, &m, &k);
vector<vector<int>> x(n, vector<int>(m));
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++)
scanf("%d", &x[i][j]);
printf("%lld\n", find_maximum(k, x));
return 0;
}