并查集用于同类修改操作和几何解释 —— NC266929 多重映射

题目来源:

牛客小白月赛88

题目如下:

已知一个由数字构成的序列 A ,不妨定义一个函数 M(x)= y 将序列 A 中全部的数字 x 都替换为 y 。例如 A = {2,2,4,5,1,4},执行一次 M(2)= 1操作后,序列变为 A'={1,1,4,5,1,4},很神奇吧!
这样的替换显然是能够进行反复叠加的,举例说明,对于上方操作过后的 A' 再执行一次 M(1)=2 操作,序列会变为 A” = {2, 2, 4, 5, 2, 4}
现在,给出若干个这样的操作,请你输出修改过后的序列。

第一次做的时候是直接模拟的操作,显然这种做法直接超时了。

第二次做的时候决定把原数组分成若干个只记录序号的数组,利用哈希表减少不必要的内存开销:

vector<vector<int>> vec;
    unordered_map<int,vector<int>*> mymap;
    int val = 0;
    for(int i=0;i<n;i++){
        int ipt = array[i];
        if(mymap.find(ipt) == mymap.end()){
            val++;
            mymap.insert({ipt,new vector<int>(1,val)});
            vec.resize(val+1); 
        }        
        vec[(*mymap[ipt])[0]].push_back(i);
    }//i是序号,vec[mymap[ipt]]存储ipt的序号.

然后分情况去讨论每次变换会造成的影响:如果被改变值不在原数组则不作任何操作,如果被改变值在原数组而改变之后的值不在原数组,则只修改哈希映射的键值,并删除原先的哈希映射,如果被改变值和改变之后的值都在原数组,在以改变之后的值为键的哈希映射给出的向量地址里利用此访问向量并给该向量里面添加被改变的值在原数组中的序号,并且把以被改变值为键的哈希映射里面的内存清干净。

for(int i=0;i<m;i++){
        int x,y;
        x = operation[i][0];
        y = operation[i][1];
        //x为被改变,y为改变值
    if(x!=y){
        if(mymap.find(x) != mymap.end()){
            if(mymap.find(y) == mymap.end()){
                mymap[y]=mymap[x];
                mymap.erase(mymap.find(x));
            }
            // for(int elem : vec[mymap[x]]){
            //     vec[mymap[y]].push_back(elem);
            // }
            else{
                for(int i=0;i<mymap[x]->size();i++){
                    mymap[y]->push_back((*mymap[x])[i]);
                }
                mymap[x]->clear();
                mymap[x]->shrink_to_fit();
                mymap.erase(mymap.find(x));
            }
        }
    }
    }

最后构建完整数组:

    vector<int> ret(n);
    for(const auto& pair : mymap){
        for(int readvecindex : *pair.second){
            for(int elem : vec[readvecindex]){
                ret[elem] = pair.first;
            }
        }
    }
    for(int i=0;i<ret.size();i++){
        cout << ret[i] << " ";
    }
    cout << endl;

完整代码如下:

#include<bits/stdc++.h>
using namespace std;
  
void fun(int n,int m,vector<int> array,vector<vector<int>> operation){
    vector<vector<int>> vec;
    unordered_map<int,vector<int>*> mymap;
    int val = 0;
    for(int i=0;i<n;i++){
        int ipt = array[i];
        if(mymap.find(ipt) == mymap.end()){
            val++;
            mymap.insert({ipt,new vector<int>(1,val)});
            vec.resize(val+1); 
        }        
        vec[(*mymap[ipt])[0]].push_back(i);
    }//i是序号,vec[mymap[ipt]]存储ipt的序号.
    for(int i=0;i<m;i++){
        int x,y;
        x = operation[i][0];
        y = operation[i][1];
        //x为被改变,y为改变值
    if(x!=y){
        if(mymap.find(x) != mymap.end()){
            if(mymap.find(y) == mymap.end()){
                mymap[y]=mymap[x];
                mymap.erase(mymap.find(x));
            }
            // for(int elem : vec[mymap[x]]){
            //     vec[mymap[y]].push_back(elem);
            // }
            else{
                for(int i=0;i<mymap[x]->size();i++){
                    mymap[y]->push_back((*mymap[x])[i]);
                }
                mymap[x]->clear();
                mymap[x]->shrink_to_fit();
                mymap.erase(mymap.find(x));
            }
        }
    }
    }
    vector<int> ret(n);
    for(const auto& pair : mymap){
        for(int readvecindex : *pair.second){
            for(int elem : vec[readvecindex]){
                ret[elem] = pair.first;
            }
        }
    }
    for(int i=0;i<ret.size();i++){
        cout << ret[i] << " ";
    }
    cout << endl;
}
  
int main(){
    int t;
    cin >> t;
    vector<int> numsN(t);
    vector<int> numsM(t);
    vector<vector<int>> arrays(t);
    vector<vector<vector<int>>> operations(t);
    for(int i=0;i<t;i++){
        cin >> numsN[i] >> numsM[i] ;
        for(int j=0;j<numsN[i];j++){
            int ipt;
            cin >> ipt;
            arrays[i].push_back(ipt);
        }
        for(int j=0;j<numsM[i];j++){
            int x,y;
            cin >> x >> y;
            operations[i].push_back({x,y});
        }
    }
    for(int i=0;i<t;i++){
        fun(numsN[i],numsM[i],arrays[i],operations[i]);
    }
    return 0;
}

结果:

心态崩了,我觉得这已经是这类算法优化的极限,于是我看了题解。题解用了并查集,但却不是简单地用并查集。

正难则反!注意到在前面进行的操作会被后面进行的操作所覆盖,不如逆序处理每一个操作。如果学习过并查集或者链表,那么会比较方便的理解本题的处理过程。

作者:WIDA
链接:https://ac.nowcoder.com/discuss/1260072?type=101&order=0&pos=2&page=1&channel=-1&source_id=1
来源:牛客网

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
     
    int Task = 1;
    for (cin >> Task; Task; Task--) {
        int n, m;
        cin >> n >> m;
        vector<int> in(n), l(m), r(m);
        map<int, int> fa;
        for (int i = 0; i < n; i++) {
            cin >> in[i];
        }
        for (int i = 0; i < m; i++) {
            cin >> l[i] >> r[i];
        }
         
        for (int i = m - 1; i >= 0; i--) {
            if (fa.count(r[i])) {
                fa[l[i]] = fa[r[i]];
            } else {
                fa[l[i]] = r[i];
            }
        }
        for (int i = 0; i < n; i++) {
            cout << (fa[in[i]] ? fa[in[i]] : in[i]) << " \n"[i == n - 1];
        }
    }
}

为什么要逆序处理?我认为的覆盖操作应当是a->b,b->c,c->d之类的,但是给出的操作序列怎会如此一帆风顺,简简单单地就把前面的操作覆盖了呢?

为了理解题解,我把注意点放在了链表之上,注意到刚刚所说的覆盖操作正好形成一个链表,由此去举一些例子去理解逆序处理的精髓。

重点在于这一段代码:


        for (int i = m - 1; i >= 0; i--) {
            if (fa.count(r[i])) {
                fa[l[i]] = fa[r[i]];
            } else {
                fa[l[i]] = r[i];
            }
        }

在传统并查集中,假设节点20的父节点定义为2,然后又定义为3,无论中间发生了什么,最终节点20的父节点一定为3。但这道题的情境下:所有为20的数字改为2之后,再令所有为20的数字改为3。后者的操作应当是无效的。什么时候后者的操作有效?中间得再插一步令所有为2的数字为20才行,这就好比一个链表:中间断了,那么后面的操作就进行不下去,由此我画出了以下图解:

如果从下往上去链接每个节点的父节点,最终就会得到这样一幅图。注意用标记笔涂红和涂绿的两部分对比,这就是前文所写的链表断掉的情况。而用绿色水笔画出来的线路就是链表没有断开的情况,这便是逆序操作的效果,希望能加深对并查集和该题的理解。

感谢你能看到这里。

  • 13
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值