UVa 597 Last Name First Please

题目描述

给定一组员工的照片文件,文件名格式为 FirstName LastName.jpg。现需将文件名改为 LastName, FirstName.jpg,即姓氏在前。文件列表按当前文件名(原名或已改名后的新名)的字典序实时更新,且每次改名后,被改名的文件仍保持选中状态。

管理员希望从列表中的第一个文件开始重命名,并且在每次重命名后,只使用一次方向键(上 / 下)来选择下一个待重命名的文件。方向键不会循环(即不会从列表末尾跳转到列表开头)。如果存在这样一条重命名顺序,使得所有文件都能依次被重命名,则输出该顺序下的新文件名;否则输出 NO QUICK RENAMING POSSIBLE

输入格式

输入包含多个测试用例,用例之间用一个空行分隔。
每个测试用例包含至多 202020 行,每行一个文件名。文件名长度不超过 323232 个字符,名字和姓氏均由字母组成且仅首字母大写。假设“姓氏”为第一个空格与末尾句点之间的部分,且所有姓氏互不相同。
输入以 EOF 结束。

输出格式

对于每个测试用例,若存在可行重命名顺序,则按该顺序每行输出一个重命名后的文件名(即 LastName, FirstName.jpg);否则输出一行 NO QUICK RENAMING POSSIBLE。两个连续测试用例的输出之间必须用一个空行隔开。

样例

输入

Christina Peter.jpg
Ganesh Ramanarayanan.jpg
Laurie Yorr.jpg
Lucy Kraus.jpg
Melanie Ayala.jpg
Nancy Schnell.jpg
Ruth Sandweiss.jpg

Christina Peter.jpg
Ganesh Ramanarayanan.jpg
Melanie Ayala.jpg

输出

NO QUICK RENAMING POSSIBLE

Peter, Christina.jpg
Ayala, Melanie.jpg
Ramanarayanan, Ganesh.jpg

(注意:样例输出中第一个用例后有一个空行,这是两个用例输出之间的分隔。)

题目分析

本题本质上是一个状态空间搜索问题。初始时,所有文件名均为原名,列表按原名排序,第一个文件(即字典序最小者)被选中。管理员必须立即重命名该文件(无需方向键)。之后,每一步需要根据当前所有文件的名称(已改名的用新名,未改名的用原名)重新排序,得到新列表;然后,当前选中的文件在列表中的位置已知,我们可以考察它的相邻文件(前一个和后一个)。如果某个相邻文件尚未被重命名,那么我们可以选择它作为下一步,并按下一次方向键完成选择。若两侧相邻文件都已被重命名或不存在,则无法继续。

由于文件数量 n≤20n \le 20n20,我们可以用状态压缩表示已经重命名的文件集合,同时记录当前选中的文件编号。每次从当前状态出发,按规则生成所有可能的下一状态,并进行深度优先搜索或广度优先搜索。如果找到一条覆盖所有文件的路径,则输出对应的重命名顺序。

需要特别注意的是:排序依据是文件名的当前显示名称,而不是固定顺序。已重命名的文件会移动到新的字典序位置,可能改变整个列表顺序,从而影响相邻关系。因此,每次状态转移时,必须重新计算当前列表顺序,再确定相邻候选。

另外,初始状态是强制选定第一个文件(原名排序后第一个)并立即重命名,这一步不需要方向键,也不计入方向键次数。我们的搜索从该状态开始。

解题思路

1. 数据表示

将每个文件抽象为一个结构体,包含原始文件名 original 和重命名后的新文件名 newName。文件编号为 0∼n−10 \sim n-10n1
对于任意状态,使用一个整数 mask 表示已重命名文件的集合(第 iii 位为 111 表示文件 iii 已重命名)。当前选中的文件编号记为 curId

2. 状态转移

给定状态 (mask, curId),需要计算当前列表顺序。为此,创建一个数组 order[0..n-1] 存储文件编号,然后按如下规则排序:

  • 如果文件 iii 已重命名(mask & (1<<i) 非零),则比较键为 files[i].newName
  • 否则比较键为 files[i].original

排序后,找到 curIdorder 中的位置 pos。然后,检查 pos-1pos+1 位置上的文件编号,若存在且尚未重命名,则它们就是合法候选。

将每个候选文件编号 nxtId 作为下一步的选择,递归调用 dfs(mask | (1<<nxtId), nxtId),并在路径中记录 nxtId

3. 搜索与剪枝

由于 n≤20n \le 20n20,状态总数最多为 n⋅2n≈20×106n \cdot 2^n \approx 20 \times 10^6n2n20×106,但实际可达状态远小于此,因为每一步只允许移动到相邻未访问节点,路径长度受限。DFS\texttt{DFS}DFS 配合回溯即可在合理时间内完成。

我们还可以加入记忆化(即记录某个状态是否访问过且失败),但本题 nnn 较小,直接搜索也足够。

4. 初始状态与终止条件

  • 初始状态:将原始文件名排序,第一个文件编号为 start,将其重命名,因此 mask = 1<<startcurId = start,路径 path = {start}
  • 终止条件:若 mask == (1<<n)-1,说明所有文件已重命名,搜索成功。

5. 特殊情况

n=1n=1n=1 时,无需任何方向键,直接输出该文件的新名即可。

6. 多测试用例输出格式

两个用例之间用空行分隔,注意在输出第一个用例前不要输出多余空行。可以使用一个静态标志变量来控制。

7. 复杂度分析

  • 每个状态需要 O(nlog⁡n)O(n \log n)O(nlogn) 的时间来排序,以确定相邻关系。状态数最多约为 n⋅2nn \cdot 2^nn2n,但实际分支有限,n=20n=20n=20 时最坏情况可接受。
  • 空间复杂度为 O(n)O(n)O(n) 用于路径记录,DFS\texttt{DFS}DFS 栈深度为 nnn

实际上,由于每一步只能向左或向右移动,路径宽度受限,搜索效率很高。

代码实现

// Last Name First Please
// UVa ID: 597
// Verdict: Accepted
// Submission Date: 2026-06-25
// UVa Run Time: 1.810s
//
// 版权所有(C)2026,邱秋。metaphysis # yeah dot net

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

struct File {
    string original;   // 原始名称
    string newName;    // 新名称
};

int n;
vector<File> files;

// DFS:mask 已重命名集合,curId 当前选中的文件 ID,path 记录重命名顺序
bool dfs(int mask, int curId, vector<int>& path) {
    if (mask == (1 << n) - 1) return true;
    // 按当前文件名排序,得到列表顺序
    vector<int> order(n);
    iota(order.begin(), order.end(), 0);
    sort(order.begin(), order.end(), [&](int a, int b) {
        string nameA = (mask & (1 << a)) ? files[a].newName : files[a].original;
        string nameB = (mask & (1 << b)) ? files[b].newName : files[b].original;
        return nameA < nameB;
    });
    // 找到当前文件在列表中的位置
    int pos = find(order.begin(), order.end(), curId) - order.begin();
    // 收集相邻且未重命名的候选文件
    vector<int> candidates;
    if (pos > 0) {
        int id = order[pos - 1];
        if (!(mask & (1 << id))) candidates.push_back(id);
    }
    if (pos + 1 < n) {
        int id = order[pos + 1];
        if (!(mask & (1 << id))) candidates.push_back(id);
    }
    // 尝试每个候选
    for (int id : candidates) {
        path.push_back(id);
        if (dfs(mask | (1 << id), id, path)) return true;
        path.pop_back();
    }
    return false;
}

// 处理单个测试用例
void processCase(const vector<string>& rawLines) {
    n = (int)rawLines.size();
    files.resize(n);
    // 解析每个文件名
    for (int i = 0; i < n; ++i) {
        const string& s = rawLines[i];
        size_t spacePos = s.find(' ');
        size_t dotPos = s.rfind('.');
        string first = s.substr(0, spacePos);
        string last = s.substr(spacePos + 1, dotPos - spacePos - 1);
        string ext = s.substr(dotPos);          // 保留扩展名(如 .jpg)
        files[i].original = s;
        files[i].newName = last + ", " + first + ext;
    }
    // 按原始文件名排序(初始列表顺序)
    sort(files.begin(), files.end(), [](const File& a, const File& b) {
        return a.original < b.original;
    });
    // 从第一个文件开始重命名(索引 0)
    vector<int> path;
    path.push_back(0);
    int mask = 1 << 0;
    bool ok = (n == 1) ? true : dfs(mask, 0, path);  // 若只有一个文件则直接成功
    // 输出结果(全局控制用例间空行)
    static bool firstOutput = true;
    if (!firstOutput) cout << '\n';
    firstOutput = false;
    if (!ok) cout << "NO QUICK RENAMING POSSIBLE\n";
    } else {
        for (int id : path)
            cout << files[id].newName << '\n';
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    string line;
    vector<string> currentLines;
    while (getline(cin, line)) {
        if (line.empty()) {
            if (!currentLines.empty()) {
                processCase(currentLines);
                currentLines.clear();
            }
            // 忽略多余空行
        } else currentLines.push_back(line);
    }
    // 处理最后一个用例(可能没有结尾空行)
    if (!currentLines.empty()) processCase(currentLines);
    return 0;
}

总结

本题的核心在于将“重命名顺序”问题转化为状态空间搜索问题,利用 nnn 较小的特点,采用状态压缩 + DFS 枚举所有可能的相邻选择路径。需要注意的关键点是:

  • 动态排序:每次状态变化后,列表顺序会因文件名改变而重新调整,因此必须实时计算当前顺序,才能确定相邻文件。
  • 初始条件:题目要求从列表第一个文件开始重命名(无需按键),因此初始状态是固定的,无需遍历所有起点。
  • 可行性:只要有一条路径能覆盖所有节点,即认为可行。搜索过程中通过回溯找到一条路径即可,无需枚举全部。

本题也展示了如何将实际场景抽象为图论中的路径搜索问题,虽然 nnn 不大,但状态数可能较多,因此合理的设计和剪枝是必要的。代码实现上,采用函数式递归,结构清晰,易于调试。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值