先判定愿望图是覆盖全部人的单一整环,再把 2n 种候选环序折算成模 n 偏移直方图取众数,答案就是 n 减去最大坐对人数。
OJ: roj
题目 ID: 3542
难度:提高
标签:图论环检测置换python
创建: 2026-10-02 06:33
更新: 2026-10-10 11:30
形式化题目
样例
graph LR
n1["1"] --- n3["3"]
n1["1"] --- n4["4"]
n2["2"] --- n4["4"]
n2["2"] --- n3["3"]
初始圈
正解
思路
第一步:把"有没有解"变成图论判定。
把每条愿望连成无向边。若合法座次存在,那么
第二步:合法座次只有
单一环上的循环序由"起点 × 方向"决定,而座位圈没有起点、也不分左右,所以全部合法座次就是:正向环序的
第三步:朴素枚举
朴素做法是枚举
也就是说,每个方向只需要把
第四步:总代价恰好等于错位人数,答案
- 下界:任何方案的最终座次必是那
个候选之一,设它最初坐对 人。坐错的 人最终座位不同于初始座位,每人至少被搬动一次;而每条命令的代价恰等于它搬动的人数,所以总代价 。又 ( 对全体候选取最大),故总代价 。 - 上界:取达到
的候选环序。坐对的人不选入任何命令;坐错的人占据的恰好是剩下的座位(坐对者已占住自己的目标座位),错位映射限制在其上是一个置换,拆成若干不相交循环。对每个长度 的循环发一条命令——让当前坐在 的人依次是 , 会被送到 的座位 ——一次就让整段循环归位,代价 。总代价等于错位人数 。
上下界相等,所以最小总代价就是
| 同学 | 当前座位 | 正向目标座位 | 正向偏移 |
反向目标座位 | 反向偏移 |
|---|---|---|---|---|---|
| 1 | 1 | 1 | 0 | 1 | 0 |
| 3 | 3 | 2 | 1 | 4 | 3 |
| 2 | 2 | 3 | 3 | 3 | 3 |
| 4 | 4 | 4 | 0 | 2 | 2 |
正向直方图
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-10 11:30
* update_at: 2026-10-10 11:30
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int n;
int wish[50005][2]; // 编号 i+1 最希望相邻的两人(0 基)
// 愿望图是单一环时返回环上顺序,否则 -1
vector<int> walk_cycle() {
int total = n;
vector<int> order;
order.push_back(0);
int prev = -1, cur = 0;
for (int step = 0; step < total; step++) {
int a = wish[cur][0], b = wish[cur][1];
int nxt = (a == prev) ? b : a;
if (nxt == 0) {
if ((int)order.size() == total) return order;
return vector<int>();
}
order.push_back(nxt);
prev = cur;
cur = nxt;
}
return vector<int>();
}
int min_cost() {
int total = n;
// 愿望必须互相成立
for (int i = 0; i < total; i++) {
int a = wish[i][0], b = wish[i][1];
bool ok_a = (wish[a][0] == i || wish[a][1] == i);
bool ok_b = (wish[b][0] == i || wish[b][1] == i);
if (!ok_a || !ok_b) return -1;
}
vector<int> order = walk_cycle();
if (order.empty()) return -1;
vector<int> hit_fwd(total, 0), hit_rev(total, 0);
for (int seat = 0; seat < total; seat++) {
int person = order[seat];
hit_fwd[((person - seat) % total + total) % total]++;
hit_rev[((person + seat) % total + total) % total]++;
}
int keep = 0;
for (int i = 0; i < total; i++) keep = max(keep, hit_fwd[i]);
for (int i = 0; i < total; i++) keep = max(keep, hit_rev[i]);
return total - keep;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 0; i < n; i++) {
cin >> wish[i][0] >> wish[i][1];
wish[i][0]--; wish[i][1]--;
}
cout << min_cost() << "\n";
return 0;
}#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-02 06:36
# update_at: 2026-10-02 06:36
import sys
def walk_cycle(wish: list[tuple[int, int]]) -> list[int] | None:
"""从 0 号出发沿愿望边走一圈:愿望图是覆盖全部人的单一环时返回环上顺序,否则 None。"""
total = len(wish)
order = [0]
prev: int | None = None
cur = 0
# 每个点度数恰为 2,走一圈必然回到 0;回到 0 时环的大小就是 order 的长度
for _ in range(total):
a, b = wish[cur]
nxt = b if a == prev else a # 两个愿望邻居里避开刚走来的那个
if nxt == 0:
return order if len(order) == total else None # 只覆盖了部分人 → 还有别的环
order.append(nxt)
prev, cur = cur, nxt
return None
def min_cost(wish: list[tuple[int, int]]) -> int:
"""愿望图是单一环时返回最小总代价,否则返回 -1。"""
total = len(wish)
# 愿望必须互相成立,否则按愿望连出的图不是每个点度数为 2 的环
for i, (a, b) in enumerate(wish):
if i not in wish[a] or i not in wish[b]:
return -1
order = walk_cycle(wish)
if order is None:
return -1
# 命令只能搬人:最终坐对的人里,最初就坐对的不必动,最初坐错的每人至少被搬一次,
# 故总代价 ≥ 错位人数;而错位者占据的恰是剩余座位,按目标位置拆成若干循环,
# 每个循环一条命令即可全部归位(代价 = 循环长度),总代价 = 错位人数。
# 座位圈没有起点、也不分左右:目标环可以整体旋转、也可以反向,
# 因此对「偏移 = 当前座位 - 目标座位」做直方图,取正反两个方向的最大重合数。
hit_fwd = [0] * total # 正向目标圈:偏移 r 的人已在位
hit_rev = [0] * total # 反向目标圈:反向座位 = -目标座位,偏移 = 当前 + 目标
for seat, person in enumerate(order): # person 的正向目标座位是 seat,它当前坐在 person 号座位
hit_fwd[(person - seat) % total] += 1
hit_rev[(person + seat) % total] += 1
keep = max(max(hit_fwd), max(hit_rev)) # 一个旋转/方向下最多能白坐对的人数
return total - keep
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
n = next(data)
wish = [(next(data) - 1, next(data) - 1) for _ in range(n)] # 编号 i+1 最希望相邻的两人(0 基)
print(min_cost(wish))
if __name__ == "__main__":
solve()正文的每一步在代码里都能对上:min_cost() 依次做互相愿望检查、walk_cycle() 走环核对是否为单一 hit_fwd / hit_rev 并取众数,最后返回 solve() 只做读入和输出。两个取模表达式 (person - seat) % total 与 (person + seat) % total 正是正文的正、反两个偏移公式。
复杂度
- 时间复杂度:
:互相愿望检查、走环、正反两张直方图各扫一遍。 ,实测每组真实数据 ,远低于 。 - 空间复杂度:
:愿望表 个整数、环序 、两张直方图 ,实测峰值约 ,低于 限制。
总结
- 本题是 NOIP2005 提高组第三题,拆开是两件事:有解性看愿望图是不是覆盖全部人的单一整环(互相愿望 + 走环核对环长),最小代价看哪个候选环序让最多的人"白坐对"。
- 圈没有起点、不分左右 → 合法座次只有
个候选;把"逐个旋转比较"改写成"偏移 的直方图众数",是本题从 到 的关键一步。 - 代价模型的上下界都压在同一个量上:坐对的人不用动,坐错的人按目标位置拆循环、一循环一条命令,总代价恰等于错位人数,于是答案就是
。 - 实现上是典型的 oj-python-style 写法:一次读入迭代消费、走环与直方图都是推导式级别的短循环、模运算交给 Python 的非负取余。
图示解析
这张图串起本题从输入到答案的主线:
输入:每人两个愿望
`- 愿望互相且构成单一整环? 否 → -1
`- 合法座次 = 2n 个候选(n 旋转 × 正反两方向)
`- 每人算偏移 r,直方图取众数 keep
`- 答案 = n − keep(上下界同为错位人数)顺着分支从上往下看:先用图判定把"不可能"筛掉,再把候选环序的比较降维成一次偏移统计,最后由代价的上下界夹出精确答案。图中只保留正文已证明的关键关系,样例的具体数字与命令见上文的表格推演。