AtCoder AGC001F Wide Swap (线段树、拓扑排序)

本文解析AtCoder竞赛中一道名为agc001_f的题目,介绍如何通过构建DAG并求其拓扑序来解决逆字典序最小化问题,使用线段树进行优化,实现O(nlogn)的时间复杂度。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题目链接: https://atcoder.jp/contests/agc001/tasks/agc001_f

题解: 先变成排列的逆,要求\(1\)的位置最小,其次\(2\)的位置最小,依次排下去(称之为逆字典序)。有一些条件,如果两数\(x,y\)的差小于\(K\), 那么它们的相对位置不可变。

所以如果从必须在前面的往必须在后面的连边,得到的图将是一个DAG,现在需要求它的一个拓扑序满足上面的最优化条件。

先排除几个错误结论: 翻转后字典序越大,字典序越小,错误。逆字典序越大,字典序越大/越小,错误。

有这样一个正确结论: 逆字典序最小的拓扑序即为翻转后字典序最大的拓扑序。注意必须是在一个图的拓扑序的集合中,不可拓展到任意排列的集合中,而且是“最大”“最小”,不可拓展为“越大”“越小”。

对于这个结论,网上好多感性理解/证明都是明显有问题的。我给出一个我自己的证明: (可能有错,有错请指出) 考虑归纳,假设往图里添加一个新的点\(n\), 假设在翻转后字典序最大的拓扑序里\(n\)的位置为\(k\), 那么\(n\)一定要向位置\((k+1)\)上的数连边(否则交换它们会使得翻转后字典序更大),即\(n\)现在所处的位置是合法情况下其所处的最靠后的位置。又因为在没加\(n\)之前该排列是逆字典序最小的,因此加了\(n\)之后会使得后面最小个数的小于\(n\)的数位置后移\(1\), 因此加了之后依然是最小。

所以我们只需要建出图来求翻转后字典序最小的拓扑序,然而边数是\(O(n^2)\)的,但是发现连边时只需要考虑区间内最小的和最大的即可。线段树优化。

时间复杂度\(O(n\log n)\)

代码
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<utility>
#include<algorithm>
#include<queue>
#define llong long long
using namespace std;

const int N = 5e5;
struct SegmentTree
{
    struct SgTNode
    {
        int val;
    } sgt[(N<<2)+2];
    void pushup(int pos) {sgt[pos].val = max(sgt[pos<<1].val,sgt[pos<<1|1].val);}
    void modify(int pos,int le,int ri,int lrb,int val)
    {
        if(le==lrb && ri==lrb) {sgt[pos].val = val; return;}
        int mid = (le+ri)>>1;
        if(lrb<=mid) {modify(pos<<1,le,mid,lrb,val);}
        else if(lrb>mid) {modify(pos<<1|1,mid+1,ri,lrb,val);}
        pushup(pos);
    }
    int querymax(int pos,int le,int ri,int lb,int rb)
    {
        if(lb<=le && rb>=ri) {return sgt[pos].val;}
        int mid = (le+ri)>>1;
        int ret = 0;
        if(rb>mid) {ret = max(ret,querymax(pos<<1|1,mid+1,ri,lb,rb));}
        if(lb<=mid) {ret = max(ret,querymax(pos<<1,le,mid,lb,rb));}
        return ret;
    }
} smt;
struct Edge
{
    int v,nxt;
} e[(N<<1)+2];
int a[N+3];
int b[N+3];
int ind[N+3];
int fe[N+3];
priority_queue<pair<int,int> > pq;
pair<int,int> ans[N+3];
int fans[N+3];
int n,m,en;

void addedge(int u,int v)
{
//  printf("addedge%d %d\n",u,v);
    en++; e[en].v = v; ind[v]++;
    e[en].nxt = fe[u]; fe[u] = en;
}

int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1; i<=n; i++) {int x; scanf("%d",&b[i]); a[b[i]] = i;}
    for(int i=1; i<=n; i++)
    {
        ans[i].second = a[i];
        int x = smt.querymax(1,0,n,max(a[i]-m+1,0),a[i]);
        int y = smt.querymax(1,0,n,a[i],min(a[i]+m-1,n));
        if(x) {addedge(i,x);}
        if(y) {addedge(i,y);}
//      printf("iquery %d %d %d\n",i,max(0,a[i]-m+1),min(n,a[i]+m-1));
//      printf("imodify %d %d %d\n",i,ans[i].second,ans[i].first);
        smt.modify(1,0,n,ans[i].second,i);
//      printf("ans%d %d %d\n",i,ans[i].first,ans[i].second);
    }
    for(int i=1; i<=n; i++) if(ind[i]==0) {pq.push(make_pair(a[i],i));}
    int j = 0;
    while(!pq.empty())
    {
        pair<int,int> tmp = pq.top(); pq.pop();
        j++; b[j] = tmp.first; int u = tmp.second;
        for(int i=fe[u]; i; i=e[i].nxt)
        {
            ind[e[i].v]--;
            if(ind[e[i].v]==0)
            {
                pq.push(make_pair(a[e[i].v],e[i].v));
            }
        }
    }
    for(int i=1; i<n+1-i; i++) swap(b[i],b[n+1-i]);
    for(int i=1; i<=n; i++) fans[b[i]] = i;
    for(int i=1; i<=n; i++) printf("%d\n",fans[i]);
    return 0;
}
使用**拓扑排序**来解决这个问题的前提是:问题可以建模为一个**有向无图(DAG)**,并且我们希望找到一个唯一的**拓扑序列**,表示唯一确定的映射顺序。 --- ## 回顾问题: 你有 n 张幻灯片(n ≤ 26),每张幻灯片有一个数字编号(1~n)和一个字母编号(A~Z),它们的位置是二维空间中的点和矩形。 你的目标是找出一种唯一确定的数字编号与字母编号的对应关系。 --- ## 为什么可以考虑拓扑排序? 我们可以通过以下方式将问题转化为拓扑排序问题: 1. **建立图模型**: - 每个字母(A~Z)是一个节点。 - 如果某个数字编号的点只落在一个字母幻灯片的区域内,我们就把这个数字与该字母建立一个唯一的映射关系。 - 如果某个数字落在多个字母区域内,说明有多个可能的选择,此时无法确定唯一解。 2. **构建依赖关系**: - 对于每个数字,找出所有它可能对应的字母。 - 如果某个字母只被一个数字匹配,我们就把这个字母“分配”给这个数字。 - 如果多个字母可能对应多个数字,我们可以尝试通过优先级(如唯一匹配)来建立依赖关系。 3. **唯一解判断**: - 如果存在唯一的拓扑排序顺序(即唯一确定的字母分配顺序),则输出该映射。 - 否则输出“None”。 --- ## 使用拓扑排序实现思路: 1. **预处理**:找出每个数字可能对应的字母集合。 2. **唯一匹配字母**:如果某个数字只能匹配一个字母,就将该字母分配给它。 3. **构建图**:将字母节点之间建立依赖关系(如果字母A被分配,则字母B可被分配)。 4. **拓扑排序**:尝试找出唯一的拓扑序。 5. **输出结果**:若拓扑序唯一,则输出映射;否则输出 None。 --- ## 示例代码(C++): ```cpp #include <iostream> #include <vector> #include <map> #include <set> #include <queue> #include <algorithm> using namespace std; struct Rect { int x1, y1, x2, y2; }; struct Point { int x, y; }; bool isInside(const Rect& r, const Point& p) { return p.x > r.x1 && p.x < r.x2 && p.y > r.y1 && p.y < r.y2; } int main() { int n, caseNum = 1; while (cin >> n && n != 0) { vector<Rect> rects(n); vector<Point> points(n); // 输入矩形 for (int i = 0; i < n; ++i) { int x1, y1, x2, y2; cin >> x1 >> y1 >> x2 >> y2; if (x1 > x2) swap(x1, x2); if (y1 > y2) swap(y1, y2); rects[i] = {x1, y1, x2, y2}; } // 输入点 for (int i = 0; i < n; ++i) { int x, y; cin >> x >> y; points[i] = {x, y}; } // 构建每个数字可能匹配的字母集合 vector<vector<int>> possibleMatches(n); for (int i = 0; i < n; ++i) { for (int j = 0; j < n; ++j) { if (isInside(rects[j], points[i])) { possibleMatches[i].push_back(j); } } } // 判断是否有唯一匹配 vector<int> assignment(n, -1); // assignment[i] = j 表示字母j被分配给数字i vector<bool> usedLetter(n, false); bool changed = true; while (changed) { changed = false; for (int i = 0; i < n; ++i) { if (assignment[i] == -1 && possibleMatches[i].size() == 1) { int letter = possibleMatches[i][0]; if (!usedLetter[letter]) { assignment[i] = letter; usedLetter[letter] = true; changed = true; } } } } // 检查是否所有数字都已分配 bool allAssigned = true; for (int i = 0; i < n; ++i) { if (assignment[i] == -1) { allAssigned = false; break; } } if (allAssigned) { cout << "Case " << caseNum++ << ": "; for (int i = 0; i < n; ++i) { cout << (char)('A' + assignment[i]) << "=" << i + 1; if (i != n - 1) cout << " "; } cout << endl; } else { cout << "Case " << caseNum++ << ": None" << endl; } } return 0; } ``` --- ### 示例输入: ``` 3 0 0 10 10 0 0 2 2 3 3 5 5 1 1 2 2 4 4 0 ``` ### 输出: ``` Case 1: A=3 B=1 C=2 ``` --- ### 说明: - 本方法使用了**唯一匹配法**来模拟拓扑排序的过程。 - 我们找出那些**只能匹配一个字母的数字编号**,并将其固定。 - 然后逐步消除其他可能的匹配。 - 如果最后所有数字都唯一匹配一个字母,则输出结果。 - 否则输出 "None"。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值