【UR #23】地铁规划

简记思维碎屑,做法可能除了 \(\Theta(m^2)\) 的都和官方题解不太一样

Description

一句话题意:有一个长度为 \(m\) 的边序列,你需要使用交互库提供的可撤销并查集接口,在线地对于每个 \(l\) 求出最大的 \(r\),使得区间 \([l,r]\) 内的边不形成环。

可行的操作为

  • merge(x) 表示合并第 \(x\) 条边的两个端点所在的并查集,如果已经同集合,则交互过程不合法,判 \(0\) 分

  • undo() 表示撤销上一次操作,如果操作序列为空时进行则交互过程不合法

  • check(x) 表示检查编号为 \(x\) 的两个边连接的两条点是否已经在相同连通块中,是则返回 \(0\)。换句话说即第 \(x\) 条边是不是可以合并

可以执行的 merge 操作量级为 \(\Theta(m\log m)\),可以执行的 check 操作次数上界为 \(8\times 10^6\)

\(n\le 10^5,m\le 2\times 10^5\)

Solution

步步为营


Subtask 1

复杂度 \(\Theta(m^2)\)

对于每个询问的 \(l\) 先 checkmerge 找到 \(r\),函数返回前撤销所有操作

期望得分:\(10\)


Subtask 2

复杂度 \(\Theta(m\sqrt m)\)

观察上面做法 merge 操作冗余是什么?

其实是每次要弹出所有的边是不必要的,由于本题询问顺序是固定的,在弹出第 \(x(x>1)\) 条边时前面的边已经被弹出了

为了让 \(x\) 后面的边 \(y\) 弹出时尽量减少操作序列中 \(>y\) 的边的移动,可以将 \(y\) 也弹出来并放到当前的操作序列顶端

尝试确定 \(y\) 的范围,由于这档限制很松,可以将操作栈中 \(x\) 之上的元素和 \(x\) 之下的 \(\sqrt m\) 个元素取出并按标号排序后逆序放回栈顶,如果栈中元素不够 \(\sqrt m\) 个就全部取出

考察一个元素的移动次数,被移动一定是在操作栈中靠上的元素被删除或者靠下的元素被删除所致。

靠上的元素删除时最多有 \(\sqrt m\) 个能导致其移动,即标号在 \([x-\sqrt m,x-1]\),靠下的元素删除一个就会有 \(\sqrt m\) 个元素保持和它有序了,只会重排 \(\sqrt m\) 次

至于标号和 \(x\) 绝对差小于根号的元素导致的移动算常数不再赘述

期望得分: \(40\)

Subtask 3

保证数据随机,复杂度 \(\Theta(\texttt{能过})\)

诶我根号做法为啥过不了呀?

Subtask 4

复杂度 \(\Theta(m\log m)\)

接着根号做法编,尝试多种减少冗余的方式中可以选择这样子做:

每次删除一个元素时,计算其到栈顶的距离,并在栈中再取出等量元素进行合并,合并即排序后逆序加入栈中

听起来很简洁是不是!体量小如此的做法也确实有线性对数复杂度!

证明仍然考察每个边被移动只能是其靠上/靠下的边删除所致,那么在其下面的边被删除时每次合并的元素个数会乘 \(2\) 所以不超过 \(\log\) 次

其上面的元素被合并时每次需要花费 \(x\) 个元素让 \(x\) 个元素移动一次并将段长减少 \(1\),复杂度甚至分析不出来高于 \(\Theta(m)\)

(这个量级有点难以置信,所以可能有问题,还请指教)

期望得分: \(100\)

const int N=2e5+10;
int n,m;
void init(int n,int _m,int lim){m=_m;}
int lans,st[N],top;
int solve(int x){
    if(x>1){
        vector<int> now;
        while(top){
            undo();
            if(x-1!=st[top]) now.pb(st[top--]);
            else{--top; break;}
        }
        int siz=now.size();
        while(siz--&&top){
            undo();
            now.pb(st[top--]);
        }
        sort(now.begin(),now.end());
        while(now.size()){
            merge(st[++top]=now.back());
            now.pop_back();
        }
    }
    int ans=lans+1;
    while(ans<=m&&check(ans)) merge(ans),st[++top]=ans,++ans; 
    --ans;
    return lans=ans;
}

总结

其实个人想的做法是让边分组,数量到一定量级就重构,尝试了一些重构方式但是都不能简单证明复杂度合法,甚至一种做法和正解 一致 类似都是取出大小相同的一块

赛时尝试证明复杂度是方式通过合并边所在块的个数,赛后改成在操作栈中低于/高于之的就清晰了许多,而且也能意识到编的分块重构没有意义

题目还是总体有趣的,只是我自己赛时没有想清楚,也没有必要放马后炮了

上一篇:一个sql语句实现数据的有则更新,无则插入


下一篇:Js timeStamp时间戳转换