#!/usr/bin/env python3
import argparse
import collections
import random
import time


def init_schedule(w, g, p, rng):
    n = g * p
    sched = []
    base = [i // p for i in range(n)]
    sched.append(base[:])
    players = list(range(n))
    for _ in range(1, w):
        rng.shuffle(players)
        a = [0] * n
        for idx, pl in enumerate(players):
            a[pl] = idx // p
        sched.append(a)
    return sched


def build_counts(sched, w, g, p):
    n = g * p
    cnt = [[0] * n for _ in range(n)]
    score = 0
    for week in range(w):
        groups = [[] for _ in range(g)]
        for x, gr in enumerate(sched[week]):
            groups[gr].append(x)
        for group in groups:
            for i in range(len(group)):
                a = group[i]
                row = cnt[a]
                for b in group[i + 1 :]:
                    c = row[b] + 1
                    row[b] = c
                    cnt[b][a] = c
                    if c > 1:
                        score += c - 1
    return cnt, score


def delta_swap(assign, cnt, score, a, b):
    ga, gb = assign[a], assign[b]
    if ga == gb:
        return 0
    group_a = [x for x, gr in enumerate(assign) if gr == ga and x != a]
    group_b = [x for x, gr in enumerate(assign) if gr == gb and x != b]
    delta = 0
    for x in group_a:
        c = cnt[a][x]
        delta -= c - 1 if c > 1 else 0
        c -= 1
        delta += c - 1 if c > 1 else 0
        c = cnt[b][x]
        delta -= c - 1 if c > 1 else 0
        c += 1
        delta += c - 1 if c > 1 else 0
    for x in group_b:
        c = cnt[b][x]
        delta -= c - 1 if c > 1 else 0
        c -= 1
        delta += c - 1 if c > 1 else 0
        c = cnt[a][x]
        delta -= c - 1 if c > 1 else 0
        c += 1
        delta += c - 1 if c > 1 else 0
    return delta


def apply_swap(assign, cnt, a, b):
    ga, gb = assign[a], assign[b]
    group_a = [x for x, gr in enumerate(assign) if gr == ga and x != a]
    group_b = [x for x, gr in enumerate(assign) if gr == gb and x != b]
    for x in group_a:
        cnt[a][x] -= 1
        cnt[x][a] -= 1
        cnt[b][x] += 1
        cnt[x][b] += 1
    for x in group_b:
        cnt[b][x] -= 1
        cnt[x][b] -= 1
        cnt[a][x] += 1
        cnt[x][a] += 1
    assign[a], assign[b] = gb, ga


def find_conflict(cnt, n, rng):
    # Random probing is much cheaper than rebuilding the whole conflict list.
    for _ in range(200):
        a = rng.randrange(n)
        b = rng.randrange(n - 1)
        if b >= a:
            b += 1
        if cnt[a][b] > 1:
            return a, b
    conflicts = [(i, j) for i in range(n) for j in range(i + 1, n) if cnt[i][j] > 1]
    return rng.choice(conflicts) if conflicts else None


def solve(w, g, p, seconds, seed=1):
    n = g * p
    rng = random.Random(seed)
    deadline = time.time() + seconds
    restarts = 0
    best = 10**9
    while time.time() < deadline:
        sched = init_schedule(w, g, p, rng)
        cnt, score = build_counts(sched, w, g, p)
        restarts += 1
        temp = 1.0
        steps = 0
        while time.time() < deadline and steps < 2000000:
            if score == 0:
                return sched, restarts, steps
            best = min(best, score)
            pair = find_conflict(cnt, n, rng)
            if pair is None:
                return sched, restarts, steps
            x, y = pair
            # Pick a conflicted week and try swaps touching one endpoint.
            weeks = [i for i in range(w) if sched[i][x] == sched[i][y]]
            week = rng.choice(weeks)
            endpoint = x if rng.randrange(2) == 0 else y
            assign = sched[week]
            best_moves = []
            best_delta = 10**9
            for z in range(n):
                if assign[z] == assign[endpoint]:
                    continue
                d = delta_swap(assign, cnt, score, endpoint, z)
                if d < best_delta:
                    best_delta = d
                    best_moves = [z]
                elif d == best_delta:
                    best_moves.append(z)
            if best_moves and (best_delta <= 0 or rng.random() < pow(2.718281828, -best_delta / max(temp, 1e-9))):
                z = rng.choice(best_moves)
                apply_swap(assign, cnt, endpoint, z)
                score += best_delta
            else:
                # Diversification: random balanced swap in a non-fixed week.
                week = rng.randrange(1, w) if w > 1 else 0
                assign = sched[week]
                a = rng.randrange(n)
                b = rng.randrange(n)
                while assign[a] == assign[b]:
                    b = rng.randrange(n)
                d = delta_swap(assign, cnt, score, a, b)
                apply_swap(assign, cnt, a, b)
                score += d
            temp *= 0.999995
            if temp < 0.01:
                temp = 0.01
            steps += 1
    return None, restarts, best


def iter_bits(mask):
    while mask:
        lb = mask & -mask
        yield lb.bit_length() - 1
        mask ^= lb


def clique_choices(v, rem, allowed, p, rng, limit=5000):
    target = p - 1
    cand = allowed[v] & rem & ~(1 << v)
    out = []

    def rec(chosen, chosen_mask, candidates):
        if len(out) >= limit:
            return
        if len(chosen) == target:
            out.append(chosen[:])
            return
        if candidates.bit_count() < target - len(chosen):
            return
        bits = list(iter_bits(candidates))
        rng.shuffle(bits)
        bits.sort(key=lambda x: ((allowed[x] & rem).bit_count(), rng.random()))
        for x in bits:
            if not (candidates >> x) & 1:
                continue
            rec(chosen + [x], chosen_mask | (1 << x), candidates & allowed[x] & ~((1 << (x + 1)) - 1))

    rec([], 1 << v, cand)
    rng.shuffle(out)
    return out


def partition_week(allowed, g, p, rng, deadline):
    n = g * p
    full = (1 << n) - 1
    groups = []
    seen = {}

    def rec(rem):
        if time.time() > deadline:
            return None
        if rem == 0:
            return []
        key = rem
        if key in seen:
            return None
        # Pick the most constrained remaining player.
        players = list(iter_bits(rem))
        v = min(players, key=lambda x: (allowed[x] & rem).bit_count())
        choices = clique_choices(v, rem, allowed, p, rng)
        choices.sort(key=lambda ch: sum((allowed[x] & rem).bit_count() for x in ch))
        for ch in choices:
            group = [v] + ch
            mask = 0
            ok = True
            for i, a in enumerate(group):
                mask |= 1 << a
                for b in group[i + 1 :]:
                    if not ((allowed[a] >> b) & 1):
                        ok = False
                        break
                if not ok:
                    break
            if not ok:
                continue
            got = rec(rem & ~mask)
            if got is not None:
                return [group] + got
        seen[key] = True
        return None

    return rec(full)


def solve_seq(w, g, p, seconds, seed=1):
    n = g * p
    rng = random.Random(seed)
    deadline = time.time() + seconds
    full = (1 << n) - 1
    restarts = 0
    while time.time() < deadline:
        restarts += 1
        used = [0] * n
        sched_groups = []
        first = [list(range(a * p, (a + 1) * p)) for a in range(g)]
        sched_groups.append(first)
        for group in first:
            for i, a in enumerate(group):
                for b in group[i + 1 :]:
                    used[a] |= 1 << b
                    used[b] |= 1 << a
        ok = True
        for week in range(1, w):
            allowed = [full & ~(1 << i) & ~used[i] for i in range(n)]
            part = partition_week(allowed, g, p, rng, deadline)
            if part is None:
                ok = False
                break
            rng.shuffle(part)
            sched_groups.append(part)
            for group in part:
                for i, a in enumerate(group):
                    for b in group[i + 1 :]:
                        used[a] |= 1 << b
                        used[b] |= 1 << a
        if ok:
            sched = []
            for week_groups in sched_groups:
                assign = [0] * n
                for gr, group in enumerate(week_groups):
                    for pl in group:
                        assign[pl] = gr
                sched.append(assign)
            return sched, restarts, 0
    return None, restarts, -1


def group_candidates(rem, avail, p, rng, limit=2000):
    out = []
    players = list(iter_bits(rem))
    # Start from low-freedom pairs/cliques.
    players.sort(key=lambda x: ((avail[x] & rem).bit_count(), rng.random()))

    def rec(chosen, cand):
        if len(out) >= limit:
            return
        if len(chosen) == p:
            common = rem
            pair_sum = 0
            for a in chosen:
                common &= avail[a]
                pair_sum += (avail[a] & rem).bit_count()
            out.append((common.bit_count(), pair_sum, chosen[:]))
            return
        if cand.bit_count() < p - len(chosen):
            return
        bits = list(iter_bits(cand))
        bits.sort(key=lambda x: ((avail[x] & rem).bit_count(), rng.random()))
        for x in bits[:80]:
            rec(chosen + [x], cand & avail[x] & rem & ~((1 << (x + 1)) - 1))

    rec([], rem)
    out.sort(key=lambda t: (t[0], t[1], rng.random()))
    return [x[2] for x in out]


def solve_greedy(w, g, p, seconds, seed=1, rcl=8):
    n = g * p
    rng = random.Random(seed)
    deadline = time.time() + seconds
    full = (1 << n) - 1
    restarts = 0
    best_week = 0
    while time.time() < deadline:
        restarts += 1
        avail = [full & ~(1 << i) for i in range(n)]
        sched_groups = []
        ok = True
        for week in range(w):
            rem = full
            week_groups = []
            for _ in range(g):
                cands = group_candidates(rem, avail, p, rng, limit=1000)
                if not cands:
                    ok = False
                    break
                take = cands[rng.randrange(min(rcl, len(cands)))]
                mask = 0
                for x in take:
                    mask |= 1 << x
                week_groups.append(take)
                rem &= ~mask
                # Reserve these pairings immediately, affecting later groups
                # in this week only through the remaining mask and later weeks
                # through avail after the week is complete.
            if not ok:
                best_week = max(best_week, week)
                break
            for group in week_groups:
                for i, a in enumerate(group):
                    for b in group[i + 1 :]:
                        avail[a] &= ~(1 << b)
                        avail[b] &= ~(1 << a)
            sched_groups.append(week_groups)
        if ok:
            sched = []
            for week_groups in sched_groups:
                assign = [0] * n
                for gr, group in enumerate(week_groups):
                    for pl in group:
                        assign[pl] = gr
                sched.append(assign)
            return sched, restarts, 0
    return None, restarts, best_week


def solve_triples(w, g, seconds, seed=1):
    p = 3
    n = g * p
    rng = random.Random(seed)
    deadline = time.time() + seconds
    full = (1 << n) - 1
    used = [0] * n
    weeks = [[] for _ in range(w)]
    rems = [full for _ in range(w)]
    # Fix the first week.
    rems[0] = 0
    for gr in range(g):
        tri = [3 * gr, 3 * gr + 1, 3 * gr + 2]
        weeks[0].append(tri)
        for i, a in enumerate(tri):
            for b in tri[i + 1 :]:
                used[a] |= 1 << b
                used[b] |= 1 << a
    start_week = 1
    if w > 1:
        # Also fix a transversal week: after player relabeling this is a
        # standard symmetry break for many triple systems.
        rems[1] = 0
        start_week = 2
        for gr in range(g):
            tri = [gr, g + gr, 2 * g + gr]
            weeks[1].append(tri)
            for i, a in enumerate(tri):
                for b in tri[i + 1 :]:
                    if (used[a] >> b) & 1:
                        return None, 1, nodes
                    used[a] |= 1 << b
                    used[b] |= 1 << a
    nodes = 0

    def rec(week):
        nonlocal nodes
        nodes += 1
        if nodes % 5000 == 0 and time.time() > deadline:
            return False
        if week == w:
            return True
        rem = rems[week]
        if rem == 0:
            return rec(week + 1)
        players = list(iter_bits(rem))
        a = min(players, key=lambda x: ((~used[x] & rem).bit_count(), rng.random()))
        cand = [x for x in players if x != a and not ((used[a] >> x) & 1)]
        pairs = []
        for ii, b in enumerate(cand):
            for c in cand[ii + 1 :]:
                if (used[b] >> c) & 1:
                    continue
                # Prefer choices that leave constrained players for later.
                score = ((~used[a] & rem).bit_count() +
                         (~used[b] & rem).bit_count() +
                         (~used[c] & rem).bit_count())
                pairs.append((score, rng.random(), b, c))
        pairs.sort()
        if len(pairs) > 80:
            # Keep the most constrained choices plus a little diversification.
            pairs = pairs[:60] + rng.sample(pairs[60:], min(20, len(pairs) - 60))
        for _, _, b, c in pairs:
            mask = (1 << a) | (1 << b) | (1 << c)
            weeks[week].append([a, b, c])
            rems[week] = rem & ~mask
            used[a] |= (1 << b) | (1 << c)
            used[b] |= (1 << a) | (1 << c)
            used[c] |= (1 << a) | (1 << b)
            if rec(week):
                return True
            used[a] &= ~((1 << b) | (1 << c))
            used[b] &= ~((1 << a) | (1 << c))
            used[c] &= ~((1 << a) | (1 << b))
            rems[week] = rem
            weeks[week].pop()
        return False

    ok = rec(start_week)
    if not ok:
        return None, 1, nodes
    sched = []
    for week_groups in weeks:
        assign = [0] * n
        for gr, group in enumerate(week_groups):
            for pl in group:
                assign[pl] = gr
        sched.append(assign)
    return sched, 1, nodes


def solve_repartition(w, g, p, seconds, seed=1):
    n = g * p
    rng = random.Random(seed)
    deadline = time.time() + seconds

    def groups_to_assign(groups):
        sched = []
        for wg in groups:
            a = [0] * n
            for gr, group in enumerate(wg):
                for x in group:
                    a[x] = gr
            sched.append(a)
        return sched

    def init():
        players = list(range(n))
        groups = []
        for week in range(w):
            rng.shuffle(players)
            groups.append([players[i*p:(i+1)*p][:] for i in range(g)])
        return groups

    def counts(groups):
        cnt = [[0] * n for _ in range(n)]
        score = 0
        occ = [[[] for _ in range(n)] for __ in range(n)]
        for week in range(w):
            for gr, group in enumerate(groups[week]):
                for i, a in enumerate(group):
                    for b in group[i+1:]:
                        if a > b:
                            a, b = b, a
                        c = cnt[a][b]
                        if c >= 1:
                            score += 1
                        cnt[a][b] = cnt[b][a] = c + 1
                        occ[a][b].append((week, gr))
        return cnt, occ, score

    def pair_score_delta(c, d):
        before = c - 1 if c > 1 else 0
        c2 = c + d
        after = c2 - 1 if c2 > 1 else 0
        return after - before

    restarts = 0
    best_global = 10**9
    while time.time() < deadline:
        restarts += 1
        groups = init()
        cnt, occ, score = counts(groups)
        temp = 2.0
        steps = 0
        while time.time() < deadline and steps < 200000:
            if score == 0:
                return groups_to_assign(groups), restarts, steps
            best_global = min(best_global, score)
            conflicts = [(i, j) for i in range(n) for j in range(i+1, n) if cnt[i][j] > 1]
            a, b = rng.choice(conflicts)
            week, gr1 = rng.choice(occ[a][b])
            gr2 = rng.randrange(g - 1)
            if gr2 >= gr1:
                gr2 += 1
            old1 = groups[week][gr1]
            old2 = groups[week][gr2]
            pool = old1 + old2
            old_pairs = []
            for group in (old1, old2):
                for i, x in enumerate(group):
                    for y in group[i+1:]:
                        if x > y:
                            x, y = y, x
                        old_pairs.append((x, y))
            best = []
            best_delta = 10**9
            idxs = list(range(2*p))
            # Enumerate one side; force pool[0] into side A to avoid duplicate complements.
            import itertools
            for rest in itertools.combinations(idxs[1:], p - 1):
                side = {0, *rest}
                new1 = [pool[i] for i in side]
                new2 = [pool[i] for i in idxs if i not in side]
                new_pairs = []
                for group in (new1, new2):
                    for i, x in enumerate(group):
                        for y in group[i+1:]:
                            if x > y:
                                x, y = y, x
                            new_pairs.append((x, y))
                delta = 0
                changes = collections.Counter()
                for pr in old_pairs:
                    changes[pr] -= 1
                for pr in new_pairs:
                    changes[pr] += 1
                for (x, y), d in changes.items():
                    delta += pair_score_delta(cnt[x][y], d)
                if delta < best_delta:
                    best_delta = delta
                    best = [(new1, new2, changes)]
                elif delta == best_delta:
                    best.append((new1, new2, changes))
            if best and (best_delta <= 0 or rng.random() < pow(2.718281828, -best_delta / temp)):
                new1, new2, changes = rng.choice(best)
                groups[week][gr1] = new1
                groups[week][gr2] = new2
                for (x, y), d in changes.items():
                    # Rebuild occurrence lists lazily via full recompute; cheaper and safer in Python.
                    cnt[x][y] += d
                    cnt[y][x] += d
                cnt, occ, score = counts(groups)
            else:
                cnt, occ, score = counts(groups)
            temp *= 0.99995
            if temp < 0.05:
                temp = 0.05
            steps += 1
    return None, restarts, best_global


def print_schedule(sched, g, p):
    for week, assign in enumerate(sched):
        groups = [[] for _ in range(g)]
        for player, gr in enumerate(assign):
            groups[gr].append(player)
        print(f"week {week}")
        for group in groups:
            print(" ".join(map(str, sorted(group))))


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("w", type=int)
    ap.add_argument("g", type=int)
    ap.add_argument("p", type=int)
    ap.add_argument("--seconds", type=float, default=30)
    ap.add_argument("--seed", type=int, default=1)
    ap.add_argument("--seq", action="store_true")
    ap.add_argument("--greedy", action="store_true")
    ap.add_argument("--triples", action="store_true")
    ap.add_argument("--repartition", action="store_true")
    args = ap.parse_args()
    if args.repartition:
        sol, restarts, info = solve_repartition(args.w, args.g, args.p, args.seconds, args.seed)
    elif args.triples:
        sol, restarts, info = solve_triples(args.w, args.g, args.seconds, args.seed)
    elif args.greedy:
        sol, restarts, info = solve_greedy(args.w, args.g, args.p, args.seconds, args.seed)
    elif args.seq:
        sol, restarts, info = solve_seq(args.w, args.g, args.p, args.seconds, args.seed)
    else:
        sol, restarts, info = solve(args.w, args.g, args.p, args.seconds, args.seed)
    if sol is None:
        print(f"no solution restarts={restarts} best={info}")
        raise SystemExit(1)
    print(f"solution restarts={restarts} steps={info}")
    print_schedule(sol, args.g, args.p)


if __name__ == "__main__":
    main()
