All IOI entries
Competitive Programming

IOI 2015 - Scales

Six coins have distinct weights forming a hidden permutation of \ 1, 2,, 6\. Available queries (each with 3 outcomes): getLightest(A,B,C), getMedian(A,B,C), getHeaviest(A,B,C): return the lightest/median/heaviest of t...

Source sync Apr 19, 2026
Track IOI
Year 2015
Files TeX and C++
Folder competitive_programming/ioi/2015/scales
IOI2015TeXC++

Source-first archive entry

This page is built from the copied files in competitive_programming/ioi/2015/scales. Edit competitive_programming/ioi/2015/scales/solution.tex to update the written solution and competitive_programming/ioi/2015/scales/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.

Six coins have distinct weights forming a hidden permutation of $\{1, 2, \ldots, 6\}$. Available queries (each with 3 outcomes):

  • getLightest(A,B,C), getMedian(A,B,C), getHeaviest(A,B,C): return the lightest/median/heaviest of three coins.

  • getNextLightest(A,B,C,D): among $\{A,B,C\}$, return the lightest coin heavier than $D$ (wrapping cyclically).

  • Determine the complete ordering using at most a bounded number of queries.

Editorial

Rendered from the copied solution.tex file. The original TeX source remains available below.

Solution

Theorem.

Six queries suffice. Since each query has 3 outcomes, 6 queries distinguish $3^6 = 729$ cases, which is at least $6! = 720$ permutations.

Approach: Build a decision tree offline during init. Starting from the set of all 720 permutations, at each node choose the query that minimises the size of the largest branch (best 3-way split). Recurse until each leaf identifies a unique permutation.

During orderCoins, walk the precomputed tree, making queries at each internal node, until reaching a leaf.

C++ Implementation

#include <bits/stdc++.h>
using namespace std;

struct Query { int type, a, b, c, d; };

int simulate(const Query &q, int perm[]) {
    int w[3] = {perm[q.a], perm[q.b], perm[q.c]};
    int c[3] = {q.a, q.b, q.c};
    for (int i = 0; i < 3; i++)
        for (int j = i+1; j < 3; j++)
            if (w[i] > w[j]) { swap(w[i],w[j]); swap(c[i],c[j]); }
    if (q.type == 0) return c[0];
    if (q.type == 1) return c[1];
    if (q.type == 2) return c[2];
    int wd = perm[q.d];
    for (int i = 0; i < 3; i++) if (w[i] > wd) return c[i];
    return c[0];
}

vector<vector<int>> allPerms;
vector<Query> allQueries;

void generatePerms() {
    vector<int> p = {1,2,3,4,5,6};
    do { allPerms.push_back(p); } while (next_permutation(p.begin(), p.end()));
}

void generateQueries() {
    for (int a = 0; a < 6; a++)
    for (int b = a+1; b < 6; b++)
    for (int c = b+1; c < 6; c++) {
        allQueries.push_back({0,a,b,c,-1});
        allQueries.push_back({1,a,b,c,-1});
        allQueries.push_back({2,a,b,c,-1});
        for (int d = 0; d < 6; d++)
            if (d!=a && d!=b && d!=c)
                allQueries.push_back({3,a,b,c,d});
    }
}

struct TreeNode {
    Query query;
    int children[6];
    int permIdx;
    bool isLeaf;
};

vector<TreeNode> tree;

int buildTree(vector<int> &cands, int depth) {
    if (cands.size() == 1) {
        TreeNode nd; nd.isLeaf = true; nd.permIdx = cands[0];
        memset(nd.children, -1, sizeof(nd.children));
        tree.push_back(nd);
        return (int)tree.size() - 1;
    }
    if (depth >= 8) return -1;

    Query bestQ; int bestMax = INT_MAX;
    map<int, vector<int>> bestSplit;
    for (auto &q : allQueries) {
        map<int, vector<int>> split;
        for (int idx : cands) {
            int perm[6];
            for (int i = 0; i < 6; i++) perm[i] = allPerms[idx][i];
            split[simulate(q, perm)].push_back(idx);
        }
        int mx = 0;
        for (auto &[k,v] : split) mx = max(mx, (int)v.size());
        if (mx < bestMax) { bestMax = mx; bestQ = q; bestSplit = split; }
    }

    TreeNode nd; nd.isLeaf = false; nd.query = bestQ; nd.permIdx = -1;
    memset(nd.children, -1, sizeof(nd.children));
    tree.push_back(nd);
    int nodeIdx = (int)tree.size() - 1;
    for (auto &[coin, sub] : bestSplit)
        tree[nodeIdx].children[coin] = buildTree(sub, depth + 1);
    return nodeIdx;
}

int rootIdx;

void init(int T) {
    generatePerms(); generateQueries();
    tree.clear();
    vector<int> all(720); iota(all.begin(), all.end(), 0);
    rootIdx = buildTree(all, 0);
}

void orderCoins() {
    int cur = rootIdx;
    while (!tree[cur].isLeaf) {
        Query &q = tree[cur].query;
        int res;
        if (q.type==0) res = getLightest(q.a+1,q.b+1,q.c+1)-1;
        else if (q.type==1) res = getMedian(q.a+1,q.b+1,q.c+1)-1;
        else if (q.type==2) res = getHeaviest(q.a+1,q.b+1,q.c+1)-1;
        else res = getNextLightest(q.a+1,q.b+1,q.c+1,q.d+1)-1;
        cur = tree[cur].children[res];
    }
    vector<int> &perm = allPerms[tree[cur].permIdx];
    vector<pair<int,int>> wc(6);
    for (int i = 0; i < 6; i++) wc[i] = {perm[i], i+1};
    sort(wc.begin(), wc.end());
    int W[6];
    for (int i = 0; i < 6; i++) W[i] = wc[i].second;
    answer(W);
}

Complexity Analysis

  • Queries per test case: at most 6 (information-theoretically tight, since $3^6 = 729 \ge 720 = 6!$).

  • Initialisation: $O(720 \times |\text{queries}| \times 6)$, which is constant for fixed problem size.

  • Space: $O(720)$ for the decision tree.

Code

Exact copied C++ implementation from solution.cpp.

C++ competitive_programming/ioi/2015/scales/solution.cpp

Exact copied implementation source.

Raw file
#include <bits/stdc++.h>
using namespace std;

// Grader functions (provided by IOI grader):
int getMedian(int A, int B, int C);
int getLightest(int A, int B, int C);
int getHeaviest(int A, int B, int C);
int getNextLightest(int A, int B, int C, int D);
void answer(int W[]);  // W[0] = lightest, ..., W[5] = heaviest

// We precompute a decision tree. Each query is (type, a, b, c [, d]).
// Type: 0=lightest, 1=median, 2=heaviest, 3=nextLightest

struct Query {
    int type; // 0=lightest, 1=median, 2=heaviest, 3=nextLightest
    int a, b, c, d; // d only used for type 3
};

// Simulate a query on a known permutation (perm[i] = weight of coin i)
int simulate(const Query &q, int perm[]) {
    int wa = perm[q.a], wb = perm[q.b], wc = perm[q.c];
    int coins[3] = {q.a, q.b, q.c};
    int weights[3] = {wa, wb, wc};

    // Sort by weight
    for (int i = 0; i < 3; i++)
        for (int j = i+1; j < 3; j++)
            if (weights[i] > weights[j]) {
                swap(weights[i], weights[j]);
                swap(coins[i], coins[j]);
            }

    if (q.type == 0) return coins[0]; // lightest
    if (q.type == 1) return coins[1]; // median
    if (q.type == 2) return coins[2]; // heaviest
    // nextLightest: lightest among {a,b,c} that is heavier than d
    int wd = perm[q.d];
    for (int i = 0; i < 3; i++) {
        if (weights[i] > wd) return coins[i];
    }
    return coins[0]; // wrap around: lightest overall
}

// Generate all permutations
vector<vector<int>> allPerms;

void generatePerms() {
    allPerms.clear();
    vector<int> p = {1, 2, 3, 4, 5, 6};
    do {
        allPerms.push_back(p);
    } while (next_permutation(p.begin(), p.end()));
}

// Generate all possible queries
vector<Query> allQueries;

void generateQueries() {
    allQueries.clear();
    for (int a = 0; a < 6; a++)
    for (int b = a+1; b < 6; b++)
    for (int c = b+1; c < 6; c++) {
        allQueries.push_back({0, a, b, c, -1});
        allQueries.push_back({1, a, b, c, -1});
        allQueries.push_back({2, a, b, c, -1});
        for (int d = 0; d < 6; d++) {
            if (d != a && d != b && d != c) {
                allQueries.push_back({3, a, b, c, d});
            }
        }
    }
}

// Decision tree node
struct TreeNode {
    Query query;
    int children[6]; // indexed by answer (coin 0-5), -1 if leaf
    int permIdx;     // if leaf, which permutation
    bool isLeaf;
};

vector<TreeNode> tree;

// Build decision tree: given a set of candidate permutation indices, find best query
int buildTree(vector<int> &cands, int depth) {
    if (cands.size() == 1) {
        TreeNode node;
        node.isLeaf = true;
        node.permIdx = cands[0];
        memset(node.children, -1, sizeof(node.children));
        tree.push_back(node);
        return tree.size() - 1;
    }
    if (depth >= 8) return -1; // shouldn't happen

    // Try each query, find one that splits well
    Query bestQuery;
    int bestMax = INT_MAX;
    map<int, vector<int>> bestSplit;

    for (auto &q : allQueries) {
        map<int, vector<int>> split;
        for (int idx : cands) {
            int perm[6];
            for (int i = 0; i < 6; i++) perm[i] = allPerms[idx][i];
            int result = simulate(q, perm);
            split[result].push_back(idx);
        }
        int maxPart = 0;
        for (auto &[k, v] : split) maxPart = max(maxPart, (int)v.size());

        if (maxPart < bestMax) {
            bestMax = maxPart;
            bestQuery = q;
            bestSplit = split;
        }
    }

    TreeNode node;
    node.isLeaf = false;
    node.query = bestQuery;
    node.permIdx = -1;
    memset(node.children, -1, sizeof(node.children));
    tree.push_back(node);
    int nodeIdx = tree.size() - 1;

    for (auto &[coin, subcands] : bestSplit) {
        tree[nodeIdx].children[coin] = buildTree(subcands, depth + 1);
    }

    return nodeIdx;
}

int rootIdx;

void init(int T) {
    generatePerms();
    generateQueries();
    tree.clear();
    vector<int> all(720);
    iota(all.begin(), all.end(), 0);
    rootIdx = buildTree(all, 0);
}

void orderCoins() {
    int cur = rootIdx;
    while (!tree[cur].isLeaf) {
        Query &q = tree[cur].query;
        int result;
        if (q.type == 0) result = getLightest(q.a+1, q.b+1, q.c+1) - 1;
        else if (q.type == 1) result = getMedian(q.a+1, q.b+1, q.c+1) - 1;
        else if (q.type == 2) result = getHeaviest(q.a+1, q.b+1, q.c+1) - 1;
        else result = getNextLightest(q.a+1, q.b+1, q.c+1, q.d+1) - 1;

        cur = tree[cur].children[result];
    }

    // Found the permutation
    vector<int> &perm = allPerms[tree[cur].permIdx];
    // perm[i] = weight of coin i+1
    // answer wants W[0] = lightest coin, ..., W[5] = heaviest
    vector<pair<int,int>> wc(6);
    for (int i = 0; i < 6; i++) wc[i] = {perm[i], i + 1};
    sort(wc.begin(), wc.end());
    int W[6];
    for (int i = 0; i < 6; i++) W[i] = wc[i].second;
    answer(W);
}

int main() {
    // Grader handles interaction
    return 0;
}

Source Files

Exact copied source-of-truth files. Edit solution.tex for the write-up and solution.cpp for the implementation.

TeX write-up competitive_programming/ioi/2015/scales/solution.tex

Exact copied write-up source.

Raw file
\documentclass[11pt,a4paper]{article}
\usepackage[utf8]{inputenc}
\usepackage[margin=1in]{geometry}
\usepackage{amsmath,amssymb,amsthm}
\usepackage{listings}
\usepackage{xcolor}
\usepackage{hyperref}

\newtheorem{theorem}{Theorem}
\newtheorem{lemma}[theorem]{Lemma}

\lstset{
  language=C++,
  basicstyle=\ttfamily\small,
  keywordstyle=\color{blue}\bfseries,
  commentstyle=\color{gray},
  stringstyle=\color{red},
  numbers=left,
  numberstyle=\tiny\color{gray},
  breaklines=true,
  frame=single,
  tabsize=4,
  showstringspaces=false
}

\title{IOI 2015 -- Scales}
\author{}
\date{}

\begin{document}
\maketitle

\section{Problem Summary}

Six coins have distinct weights forming a hidden permutation of $\{1, 2, \ldots, 6\}$. Available queries (each with 3 outcomes):
\begin{itemize}
  \item \texttt{getLightest(A,B,C)}, \texttt{getMedian(A,B,C)}, \texttt{getHeaviest(A,B,C)}: return the lightest/median/heaviest of three coins.
  \item \texttt{getNextLightest(A,B,C,D)}: among $\{A,B,C\}$, return the lightest coin heavier than $D$ (wrapping cyclically).
\end{itemize}
Determine the complete ordering using at most a bounded number of queries.

\section{Solution}

\begin{theorem}
Six queries suffice. Since each query has 3 outcomes, 6 queries distinguish $3^6 = 729$ cases, which is at least $6! = 720$ permutations.
\end{theorem}

\textbf{Approach}: Build a \emph{decision tree} offline during \texttt{init}. Starting from the set of all 720 permutations, at each node choose the query that minimises the size of the largest branch (best 3-way split). Recurse until each leaf identifies a unique permutation.

During \texttt{orderCoins}, walk the precomputed tree, making queries at each internal node, until reaching a leaf.

\section{C++ Implementation}

\begin{lstlisting}
#include <bits/stdc++.h>
using namespace std;

struct Query { int type, a, b, c, d; };

int simulate(const Query &q, int perm[]) {
    int w[3] = {perm[q.a], perm[q.b], perm[q.c]};
    int c[3] = {q.a, q.b, q.c};
    for (int i = 0; i < 3; i++)
        for (int j = i+1; j < 3; j++)
            if (w[i] > w[j]) { swap(w[i],w[j]); swap(c[i],c[j]); }
    if (q.type == 0) return c[0];
    if (q.type == 1) return c[1];
    if (q.type == 2) return c[2];
    int wd = perm[q.d];
    for (int i = 0; i < 3; i++) if (w[i] > wd) return c[i];
    return c[0];
}

vector<vector<int>> allPerms;
vector<Query> allQueries;

void generatePerms() {
    vector<int> p = {1,2,3,4,5,6};
    do { allPerms.push_back(p); } while (next_permutation(p.begin(), p.end()));
}

void generateQueries() {
    for (int a = 0; a < 6; a++)
    for (int b = a+1; b < 6; b++)
    for (int c = b+1; c < 6; c++) {
        allQueries.push_back({0,a,b,c,-1});
        allQueries.push_back({1,a,b,c,-1});
        allQueries.push_back({2,a,b,c,-1});
        for (int d = 0; d < 6; d++)
            if (d!=a && d!=b && d!=c)
                allQueries.push_back({3,a,b,c,d});
    }
}

struct TreeNode {
    Query query;
    int children[6];
    int permIdx;
    bool isLeaf;
};

vector<TreeNode> tree;

int buildTree(vector<int> &cands, int depth) {
    if (cands.size() == 1) {
        TreeNode nd; nd.isLeaf = true; nd.permIdx = cands[0];
        memset(nd.children, -1, sizeof(nd.children));
        tree.push_back(nd);
        return (int)tree.size() - 1;
    }
    if (depth >= 8) return -1;

    Query bestQ; int bestMax = INT_MAX;
    map<int, vector<int>> bestSplit;
    for (auto &q : allQueries) {
        map<int, vector<int>> split;
        for (int idx : cands) {
            int perm[6];
            for (int i = 0; i < 6; i++) perm[i] = allPerms[idx][i];
            split[simulate(q, perm)].push_back(idx);
        }
        int mx = 0;
        for (auto &[k,v] : split) mx = max(mx, (int)v.size());
        if (mx < bestMax) { bestMax = mx; bestQ = q; bestSplit = split; }
    }

    TreeNode nd; nd.isLeaf = false; nd.query = bestQ; nd.permIdx = -1;
    memset(nd.children, -1, sizeof(nd.children));
    tree.push_back(nd);
    int nodeIdx = (int)tree.size() - 1;
    for (auto &[coin, sub] : bestSplit)
        tree[nodeIdx].children[coin] = buildTree(sub, depth + 1);
    return nodeIdx;
}

int rootIdx;

void init(int T) {
    generatePerms(); generateQueries();
    tree.clear();
    vector<int> all(720); iota(all.begin(), all.end(), 0);
    rootIdx = buildTree(all, 0);
}

void orderCoins() {
    int cur = rootIdx;
    while (!tree[cur].isLeaf) {
        Query &q = tree[cur].query;
        int res;
        if (q.type==0) res = getLightest(q.a+1,q.b+1,q.c+1)-1;
        else if (q.type==1) res = getMedian(q.a+1,q.b+1,q.c+1)-1;
        else if (q.type==2) res = getHeaviest(q.a+1,q.b+1,q.c+1)-1;
        else res = getNextLightest(q.a+1,q.b+1,q.c+1,q.d+1)-1;
        cur = tree[cur].children[res];
    }
    vector<int> &perm = allPerms[tree[cur].permIdx];
    vector<pair<int,int>> wc(6);
    for (int i = 0; i < 6; i++) wc[i] = {perm[i], i+1};
    sort(wc.begin(), wc.end());
    int W[6];
    for (int i = 0; i < 6; i++) W[i] = wc[i].second;
    answer(W);
}
\end{lstlisting}

\section{Complexity Analysis}

\begin{itemize}
  \item \textbf{Queries per test case}: at most 6 (information-theoretically tight, since $3^6 = 729 \ge 720 = 6!$).
  \item \textbf{Initialisation}: $O(720 \times |\text{queries}| \times 6)$, which is constant for fixed problem size.
  \item \textbf{Space}: $O(720)$ for the decision tree.
\end{itemize}

\end{document}