zoukankan      html  css  js  c++  java
  • [LuoguP2403][SDOI2010]所驼门王的宝藏

    题目描述

    在宽广的非洲荒漠中,生活着一群勤劳勇敢的羊驼家族。被族人恭称为“先知”的Alpaca L. Sotomon是这个家族的领袖,外人也称其为“所驼门王”。所驼门王毕生致力于维护家族的安定与和谐,他曾亲自率军粉碎河蟹帝国主义的野蛮侵略,为族人立下赫赫战功。所驼门王一生财宝无数,但因其生性节俭低调,他将财宝埋藏在自己设计的地下宫殿里,这也是今天Henry Curtis故事的起点。Henry是一个爱财如命的贪婪家伙,而又非常聪明,他费尽心机谋划了这次盗窃行动,破解重重机关后来到这座地下宫殿前。

    整座宫殿呈矩阵状,由R×C间矩形宫室组成,其中有N间宫室里埋藏着宝藏,称作藏宝宫室。宫殿里外、相邻宫室间都由坚硬的实体墙阻隔,由一间宫室到达另一间只能通过所驼门王独创的移动方式——传送门。所驼门王为这N间藏宝宫室每间都架设了一扇传送门,没有宝藏的宫室不设传送门,所有的宫室传送门分为三种:

    1. “横天门”:由该门可以传送到同行的任一宫室;
    2. “纵寰门”:由该门可以传送到同列的任一宫室;
    3. “九宫门”:由该门可以传送到以该门所在宫室为中心周围8格中任一宫室(如果目标宫室存在的话)。

    深谋远虑的Henry当然事先就搞到了所驼门王当年的宫殿招标册,书册上详细记录了每扇传送门所属宫室及类型。而且,虽然宫殿内外相隔,但他自行准备了一种便携式传送门,可将自己传送到殿内任意一间宫室开始寻宝,并在任意一间宫室结束后传送出宫。整座宫殿只许进出一次,且便携门无法进行宫室之间的传送。不过好在宫室内传送门的使用没有次数限制,每间宫室也可以多次出入。

    现在Henry已经打开了便携门,即将选择一间宫室进入。为得到尽多宝藏,他希望安排一条路线,使走过的不同藏宝宫室尽可能多。请你告诉Henry这条路线最多行经不同藏宝宫室的数目。

    输入输出格式

    输入格式:

    输入文件sotomon.in第一行给出三个正整数N, R, C。

    以下N行,每行给出一扇传送门的信息,包含三个正整数xi, yi, Ti,表示该传送门设在位于第xi行第yi列的藏宝宫室,类型为Ti。Ti是一个1~3间的整数,1表示可以传送到第xi行任意一列的“横天门”,2表示可以传送到任意一行第yi列的“纵寰门”,3表示可以传送到周围8格宫室的“九宫门”。

    保证1≤xi≤R,1≤yi≤C,所有的传送门位置互不相同。

    输出格式:

    输出文件sotomon.out只有一个正整数,表示你确定的路线所经过不同藏宝宫室的最大数目。

    一个着实很水但是细节颇多的Tarjan题~~

    恩没错,这道题的难点其实并不是很多,但是由于其细节非常多,于是导致这道题的AC率比较低。。

    我们来分析这道题目:怎么跟Tarjan扯上了关系呢??

    首先我们考虑建图:关于这三个门,我们按照最平常的思路应该是对于三种门无一例外暴力前向星add()。

    但是想像一下假如我们有这样一组数据使某一行全部都是横天门,或者说某一列全部都是纵寰门,那么我们的建图就会跑到大概O(n2)或者O(m2)的速度,建图都成n^2了这还做啥。于是我们考虑优化。

    那么由于各自的门的特殊性,我肯应该分开建图,也就是三种算法,对于每一行的横天门,我们将其建成一个环,对于每一列的纵寰门,我们也将其建成一个环。因为我们知道:每一行的所有横天门之间肯定都是相互通达,而每一列的所有纵寰门之间也肯定是相互通达的。所以我们可以说:

    当你到达了某一行的一个横天门的时候,你就到达了这一行所有的横天门,当你到达了某一列的纵寰门的时候,你就到达了这一列所有的纵寰门。*

    于是我们完全可以将其建成一个环。而Tarjan的用途就来了。我们可以利用Tarjan将这两种环缩成点。而对于九宫门本人并没有想到比暴力建边更好的方法,于是就暴力建边啦~~~。

    而对于这个建环的过程其实我们也是可以有一定的优化的,比如sort。当我们要建横天门的时候,我们肯定是想快点循环到当前行所有的横天门,于是我们就有了这么一个sort函数:

    //我们这个sort函数的意思就是将同一行的所有横天门放到前面。 
    bool xf_cmp(st1 a,st1 b){
        if(a.x!=b.x) return a.x<b.x;
        //不是同一行我们自然不用管 
        if(a.opt==1) return 1;
        //如果是横天门就先把他放在前面 
        if(b.opt==1) return 0; 
        return a.y<b.y;
    }
    

    然后对于纵寰门其实也是一个道理。

    bool yf_cmp(st1 a,st1 b){
        if(a.y!=b.y) return a.y<b.y;
        if(a.opt==2) return 1;
        //如果是纵寰门就先把他放在前面 
        if(b.opt==2) return 0;
        return a.x<b.x;
    }
    

    接下来是真正的建环,我们在这里定义了一个first:表示每一次循环到的第一个横门。last:表示上一次扫到的横门

    sort(point+1,point+all+1,xf_cmp);
        //我们想尽量快的循环到所有横天门,于是这个sort就是为了吧所有的横天门放在前面
        int first=1,last=1;
        //对于每一次循环到的第一个横门,我们记为first
        //规定last变量是上一个扫到的横门
        for(int i=1;i<=all;i++){ //横向建环 (横天门)
            if(point[i].x!=point[i+1].x){//相邻两个输入的坐标不在同一行
                if(first!=last)
                add(point[last].number,point[first].number);
                //我们连边肯定要连初始序号的啦
                last=first=i+1;
            }
            else{//在同一行
                if(point[last].opt==1)
                //如果i和i+1在同一行并且i是横天门就把last和i+1连起来
                add(point[last].number,point[i+1].number);
                if(point[i+1].opt==1)//如果i+1点是横天门
                last=i+1;//因为last是指上一个扫到的横天门,而且i+1是横天门,所以就更新last
                if(point[first].opt!=1)//如果first点不是横天门 
                //first要更新,last肯定也要更新 
                last=first=i+1;//那么更新last和first 
            }
        }
    

    其实以上的过程就是利用了这个(first)(last)的不断变换的过程进行建边.....然后纵寰门也是一样。

    暴力建边自不必说,在这里我们有一个很头疼的问题,就是宝藏公室的记录。因为这里是一个有x,y的图,所以图的连边包括宝藏宫室的记录都比较麻烦。我们在这里引入一个STL里面的pair就很好解决了。pair就是对组,包含两个元素,我们可定义为是一个点的横纵坐标,然后就可以很愉快的用map了。

    因为pair和map的引用,这个(tarjan)的建图就会很恶心(因为Yeasion​并不喜欢用指针.....)但其实如果明白了Tarjan的原理,一切都并不难了。

    我们首先扫描Tarjan之后的整个前向星,记录(from)(to)在旧图中不属于一个点的位置,然后可以再定义一个map:mat进行记录,然后清空前向星的(head)之后我们就可以定义一个(map)指针从(mat)的头指向尾然后添加。当然这个mat的pair指的就是原来符合条件的前向星咯((belong[edge[i].from],belong[edge[i].to]))

    for(int i=1;i<=total;i++){//for整个前向星 
            int f=edge[i].from;
            int t=edge[i].to;
            if(belong[f]!=belong[t])//如果在新图中不属于一个点 
            mat[pir(belong[f],belong[t])]=1;
        }
        memset(head,0,sizeof(head)); total=0;
        //清空前向星留下在新图接着用咯~~ 
        for(it=mat.begin();it!=mat.end();it++){
            add(it->first.first,it->first.second); 
            //it->first.first表示mat这个map的第一个元素的pair中的前面 
            //it->first.second表示mat的第一个元素的pair中的后面那个元素 
            ind[it->first.second]++;
        }
    

    然后我们现在有了一个有向无环的图,然后我们要在这上面跑最长路(边权为1嘛),然后大家肯定就都知道怎么做了...(DP)啊,(DAG)上跑(DP)就是了!

     for(int i=1;i<=cnt;i++){
            if(ind[i]==0){//入度为0 从入度为0开始跑 
            //因为我们可以知道从入度不为0的节点开始跑的ans一定小于从入度为0的节点开始跑的ans 
                dfs(i,0);//入度为0当然没有前前驱节点咯 
                ans=max(ans,dp[i]);//取max 
            }
        }
    void dfs(int now,int fa){//动规 
    //now是当前节点,fa是从哪里来的节点。可以理解为树上的父亲节点。 
        if(dp[now]>sum[now])return ;
        dp[now]=sum[now];
        for(int i=head[now];i;i=edge[i].next){
            if(edge[i].to==fa) continue;
            dfs(edge[i].to,now);
            dp[now]=max(dp[now],dp[edge[i].to]+sum[now]);
            //要知道,在新图中跑过了i节点就相当于是在就图中跑了sum[i]个宝藏室
        }
    }
    

    恩没错就是这样,那么我们整个题就跑完了,最后我们输出ans就可以了。

    #include<iostream>
    #include<cstring>
    #include<cstdio>
    #include<algorithm>
    #include<utility>
    #include<map>
    #define MAXN 100010
    #define pir pair<int,int> 
    using namespace std;
    int all,n,m,ind[MAXN];
    map<pir,bool>mat;//就是记录新图的连边。哪些边可以连(基于新图。
    map<pir, int>tre;//用来记录某一个有宝藏的坐标位置的序号
    map<pir,bool>::iterator it;//::iterator是STL里,代表指针
    int dx[9]={0,-1,-1,-1,0,0,1,1,1};
    int dy[9]={0,-1,0,1,-1,1,-1,0,1};
    struct st1{
        int x;//横坐标 
        int y;//纵坐标 
        int opt;//门的类型 
        int number;//因为要sort所以肯定要记录初始序号啦 
    }point[MAXN];
    struct st2{//前向星用 
        int from;
        int to;
        int next;
    }edge[MAXN*100];
    int total,head[MAXN];
    void add(int f,int t){//前向星 
        total++;
        edge[total].from=f;
        edge[total].to=t;
        edge[total].next=head[f];
        head[f]=total;
    }
    bool xf_cmp(st1 a,st1 b){
        if(a.x!=b.x) return a.x<b.x;
        if(a.opt==1) return 1;
        //如果是横天门就先把他放在前面 
        if(b.opt==1) return 0; 
        return a.y<b.y;
    }
    bool yf_cmp(st1 a,st1 b){
        if(a.y!=b.y) return a.y<b.y;
        if(a.opt==2) return 1;
        //如果是纵寰门就先把他放在前面 
        if(b.opt==2) return 0;
        return a.x<b.x;
    }
    inline int read()
    {  
       int s=0,w=1;  
       char ch=getchar();  
       while(ch<='0'||ch>'9')
       {
            if(ch=='-')
                w=-1;
            ch=getchar();
        }  
       while(ch>='0'&&ch<='9')
       s=s*10+ch-'0',ch=getchar();  
       return s*w;  
    } 
    int Yeasion[MAXN];//Yeasion[i]表示i的dfs序 
    int Nein[MAXN];//Nein[i]表示i节点所能回到的最早的节点的编号 
    int ken,top,stack[MAXN];
    bool insta[MAXN];//flag[i]表示i是不是被访问过,insta[i]表示在不在栈中。 
    int belong[MAXN],cnt;
    int sum[MAXN];
    void Tarjan(int now){//基本的Tarjan 
        Yeasion[now]=Nein[now]=++ken;
        stack[++top]=now; insta[now]=1;
        for(int i=head[now];i;i=edge[i].next){
            if(!Yeasion[edge[i].to]){
                Tarjan(edge[i].to);
                Nein[now]=min(Nein[now],Nein[edge[i].to]);
            }
            else if(!belong[edge[i].to])
            Nein[now]=min(Nein[now],Yeasion[edge[i].to]);
        }
        if(Yeasion[now]==Nein[now]){
            cnt++; int pass;
            do{
                pass=stack[top--];
                belong[pass]=cnt;
                sum[cnt]++; //记录cnt含原图中点的个数 
                insta[pass]=0;
            }while(pass!=now);
        }
    }
    int ans,dp[MAXN];       
    void dfs(int now,int fa){//动规 
    //now是当前节点,fa是从哪里来的节点。可以理解为树上的父亲节点。 
        if(dp[now]>sum[now])return ;
        dp[now]=sum[now];
        for(int i=head[now];i;i=edge[i].next){
            if(edge[i].to==fa) continue;
            dfs(edge[i].to,now);
            dp[now]=max(dp[now],dp[edge[i].to]+sum[now]);
            //要知道,在新图中跑过了i节点就相当于是在就图中跑了sum[i]个宝藏室
        }
    }
    int main(){
        all=read(); n=read(); m=read();
        for(int i=1;i<=all;i++){
            point[i].x=read();
            point[i].y=read();
            point[i].opt=read();
            point[i].number=i;
            tre[pir(point[i].x,point[i].y)]=i;
            //记录坐标为(point[x],point[y])的点的序号为i
        }
        sort(point+1,point+all+1,xf_cmp);
        //我们想尽量快的循环到所有横天门,于是这个sort就是为了吧所有的横天门放在前面
        int first=1,last=1;
        //对于每一次循环到的第一个横门,我们记为first
        //规定last变量是上一个扫到的横门
        for(int i=1;i<=all;i++){ //横向建环 (横天门)
            if(point[i].x!=point[i+1].x){//相邻两个输入的坐标不在同一行
                if(first!=last)
                add(point[last].number,point[first].number);
                //我们连边肯定要连初始序号的啦
                last=first=i+1;
            }
            else{//在同一行
                if(point[last].opt==1)
                //如果i和i+1在同一行并且i是横天门就把last和i+1连起来
                add(point[last].number,point[i+1].number);
                if(point[i+1].opt==1)//如果i+1点是横天门
                last=i+1;//因为last是指上一个扫到的横天门,而且i+1是横天门,所以就更新last
                if(point[first].opt!=1)//如果first点不是横天门 
                //first要更新,last肯定也要更新 
                last=first=i+1;//那么更新last和first 
            }
        }
        sort(point+1,point+all+1,yf_cmp);
        first=1,last=1;//别忘了重置first和last 
        //同理,我们要想尽快的循环完所有的纵寰门,那么sort吧所有的纵寰门排在前面
        //然后循环结构基本和上面是完全一样的qwq 
        for(int i=1;i<=all;i++){  //纵向建环 (纵寰门) 
            if(point[i].y!=point[i+1].y){
            //纵寰门可不要打成point[i].x!!照搬上文是不行滴 
                if(first!=last)
                add(point[last].number,point[first].number);
                last=i+1; first=i+1;
                //将last和first都更新为i+1。 
            }
            else{
                if(point[last].opt==2)
                add(point[last].number,point[i+1].number);
                if(point[i+1].opt==2)
                last=i+1;
                if(point[first].opt!=2)
                last=first=i+1;
            }
        } 
        for(int i=1;i<=all;i++)//暴力建九宫门
         if(point[i].opt==3){
            for(int j=1;j<=8;j++)//循环8个方向
            if(tre.count(pir(point[i].x+dx[j],point[i].y+dy[j])))
         //如果这8个方向上有map的元素的话。因为我们知道没有宝藏的房间我们不跑。
            add(point[i].number,tre[pir(point[i].x+dx[j],point[i].y+dy[j])]);
         }    
        for(int i=1;i<=all;i++)
        //因为一行上的横天门和一列上的纵寰门我们都已经连成了一个环。
        //所以肯定是强连通分量,所以我们就可以利用Tarjan进行缩点 
        if(!Yeasion[i]) Tarjan(i);
        //因为一次tarjan不一定能够遍历完所有的点 所以放在for里面
        //因为tarjan已经完成了,所以下面我们用到新图的节点都应该在belong[]里面。 
        for(int i=1;i<=total;i++){//for整个前向星 
            int f=edge[i].from;
            int t=edge[i].to;
            if(belong[f]!=belong[t])//如果在新图中不属于一个点 
            mat[pir(belong[f],belong[t])]=1;
        }
        memset(head,0,sizeof(head)); total=0;
        //清空前向星留下在新图接着用咯~~ 
        for(it=mat.begin();it!=mat.end();it++){
            add(it->first.first,it->first.second); 
            //it->first.first表示mat这个map的第一个元素的pair中的前面 
            //it->first.second表示mat的第一个元素的pair中的后面那个元素 
            ind[it->first.second]++;
        }
        //上面的操作简而言之就是:旧图中的edge如果在新图中依然可以连,就把他们连起来。
        for(int i=1;i<=cnt;i++){
            if(ind[i]==0){//入度为0 从入度为0开始跑 
            //因为我们可以知道从入度不为0的节点开始跑的ans一定小于从入度为0的节点开始跑的ans 
                dfs(i,0);//入度为0当然没有前前驱节点咯 
                ans=max(ans,dp[i]);//取max 
            }
        }
        printf("%d",ans); return 0;
    }
    
  • 相关阅读:
    警惕:利用Dropbox链接散播的恶意软件
    repo的小结
    【网络协议】TCP的拥塞控制机制
    具体总结 Hive VS 传统关系型数据库
    站点防止攻击
    Java凝视Override、Deprecated、SuppressWarnings具体解释
    括号配对问题
    java的System.getProperty()方法能够获取的值
    HTTP协议是无状态协议,怎么理解?
    C++
  • 原文地址:https://www.cnblogs.com/sue_shallow/p/10180204.html
Copyright © 2011-2022 走看看