[csp]201604-3 路径解析

题意

在操作系统中,数据通常以文件的形式存储在文件系统中。文件系统一般采用层次化的组织形式,由目录(或者文件夹)和文件构成,形成一棵树的形状。文件有内容,用于存储数据。目录是容器,可包含文件或其他目录。同一个目录下的所有文件和目录的名字各不相同,不同目录下可以有名字相同的文件或目录。
  为了指定文件系统中的某个文件,需要用路径来定位。在类 Unix 系统(Linux、Max OS X、FreeBSD等)中,路径由若干部分构成,每个部分是一个目录或者文件的名字,相邻两个部分之间用 / 符号分隔。
  有一个特殊的目录被称为根目录,是整个文件系统形成的这棵树的根节点,用一个单独的 / 符号表示。在操作系统中,有当前目录的概念,表示用户目前正在工作的目录。根据出发点可以把路径分为两类:
  Ÿ 绝对路径:以 / 符号开头,表示从根目录开始构建的路径。
  Ÿ 相对路径:不以 / 符号开头,表示从当前目录开始构建的路径。

例如,有一个文件系统的结构如下图所示。在这个文件系统中,有根目录 / 和其他普通目录 d1、d2、d3、d4,以及文件 f1、f2、f3、f1、f4。其中,两个 f1 是同名文件,但在不同的目录下。在这里插入图片描述
对于 d4 目录下的 f1 文件,可以用绝对路径 /d2/d4/f1 来指定。如果当前目录是 /d2/d3,这个文件也可以用相对路径 …/d4/f1 来指定,这里 … 表示上一级目录(注意,根目录的上一级目录是它本身)。还有 . 表示本目录,例如 /d1/./f1 指定的就是 /d1/f1。注意,如果有多个连续的 / 出现,其效果等同于一个 /,例如 /d1///f1 指定的也是 /d1/f1。
  本题会给出一些路径,要求对于每个路径,给出正规化以后的形式。一个路径经过正规化操作后,其指定的文件不变,但是会变成一个不包含 . 和 … 的绝对路径,且不包含连续多个 / 符号。如果一个路径以 / 结尾,那么它代表的一定是一个目录,正规化操作要去掉结尾的 /。若这个路径代表根目录,则正规化操作的结果是 /。若路径为空字符串,则正规化操作的结果是当前目录。

Input

第一行包含一个整数 P,表示需要进行正规化操作的路径个数。
第二行包含一个字符串,表示当前目录。
以下 P 行,每行包含一个字符串,表示需要进行正规化操作的路径。

Output

共 P 行,每行一个字符串,表示经过正规化操作后的路径,顺序与输入对应。

输入样例

7
/d2/d3
/d2/d4/f1
…/d4/f1
/d1/./f1
/d1///f1
/d1/
///
/d1/…/…/d2

输出样例

/d2/d4/f1
/d2/d4/f1
/d1/f1
/d1/f1
/d1
/
/d2

提示

1 ≤ P ≤ 10。
  文件和目录的名字只包含大小写字母、数字和小数点 .、减号 - 以及下划线 _。
  不会有文件或目录的名字是 . 或 … ,它们具有题目描述中给出的特殊含义。
  输入的所有路径每个长度不超过 1000 个字符。
  输入的当前目录保证是一个经过正规化操作后的路径。
  对于前 30% 的测试用例,需要正规化的路径的组成部分不包含 . 和 … 。
  对于前 60% 的测试用例,需要正规化的路径都是绝对路径。


分析

T3总是这么多字让人看到崩溃🤯

  • 题目分析&代码实现

1. 题目分析
我大概把题目读了两三遍之后才弄清楚了题目的要求。弄清楚之后发现题目并不复杂【T3总是如此】。

我们要解决的就是对一个字符串按要求进行转换,输出住转换结果就可以。

其中的要求可以整理为:

  • 转换结果为绝对路径:只包含“/ ”和名字,且第一个一定为“/ ”,最后个一定为名字。
  • 若字符串中包含“ / . / ”,则此处本应出现的名字和前一个名字相同,表明无变化。
  • 若字符串中包含“ / … / ”,则此处应该出现的名字和上上的名字相同。
  • 若字符串中包含“ // ",则只用保留一个“ / ”。
  • 若字符串末尾出现“ / ”,则直接去掉。
  • 若字符串首个不为“ / ”,则代表之后的目录都是在给出的当前目录之后,代表字符串应该直接接在题目给出的绝对路径之后。
  • 若字符串为空,则代表直接输出题目给的当前目录。

整理好以上信息之后,解决问题就很容易了,根据这个信息设计代码就可以。

2. 代码实现
实现过程可以简括为:

  • 字符串是否为空,空则直接输出当前目录
  • 若不为空,判断字符串首字符是否为“ / ”。若是,则不用改变头部,否则将当前目录加在头部。
  • 处理字符串中所有的“ // ”,利用string的find函数,每次定位第一个遇到的“ // ”中第一个字符的下标,删除前面一个“ / ”。直到不再存在“ // ”。
  • 处理字符串中所有“ /./ ”,利用string的find函数,每次定位第一个遇到的“ / . / ”中第一个字符的下标,删除前面的“ / . ”。直到不再存在“ / . / ”。
  • 处理字符串中所有“ / … / ”,利用string的find函数,每次定位第一个遇到的“ / … / ”中第一个字符的下标。
    • 若是在最顶部,直接删除“/ … ”即可。
    • 否则找到在它之前的第一个名字,将其和“/ … ”全部删除,整段只保留最后一个“ / ”
    • 直到不再存在“ /…/ ”。
  • 若字符串末尾存在“ / ”,直接删除
  • 输出处理后字符串。

  • 需要注意的问题

  • 如何获取空字符

这是个棘手的问题,也是这个题目的一个考点。

普通的cin并不会输入任何空格、提行等非字符符号。那么对于string,就只有通过getline这样的输入流操作来实现。

但是有个问题!如果对于普通的数据类型,如int使用cin,而紧接着对string使用getline,你会发现结果和你想的不一样。

原因是,cin从输入流中读取<<之后数据类型的数据,遇到非字符符号后停止,但是输入流中的提行、空格都并没有消失。getline会获取一整行数据,但前提是没有提行。

但是显然,这在这道题中出现了冲突。解决的办法是:利用cin.ignore()将其输入数据之后的提行给忽略,使得接下来的getline可以成功读入数据。

解决办法不止一种,百度即可👐🏼

  • 最开始的思路(80分)

这道题最后的ac的代码,是我在调试了很久最初的代码仍然无法拿到100分之后,上网观摩了各路大佬的代码,幡然醒悟之后重新写的。简洁明了,轻松满分【菜鸡跪地】

我最开始的思路就比起来很复杂了。大概是受到目录管理器那道题的影响【在这里就不指路了害怕同样被洗脑233】。

我最开始的想法是:

既然要输出绝对路径,那么关键其实只在于,最后他停在了哪个文件上,因此只需要找到最后的终点。

但很快我发现,这个天真的想法被题目所说的“不同目录存在相同文件名”这个约束给打灭了。因此就顺着这个思路,想到了记录路径。但是这一切都要建立在保存树结构的基础上。因此我就是在处理每个字符串的过程中逐渐完善和回溯目录树,并且忽略到特殊情况。最后得到了到达终点的唯一标识。

实现过程和目录管理器中类链式前向星的做法几乎相同。其实是很简单的,但是代码写起来就很长,和这个做法就比起来看着很蠢【233】。显然是想得太复杂了。

至于为什么是80分,我仍然是不明白,或许是遗漏了什么奇怪的判断条件,但是我实在想不出是什么条件。


附上辛辛苦苦写的80分代码吧⚠️:

//
//  main.cpp
//  lab1
//
//

#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#include <map>
#include <sstream>
using namespace std;

struct menu             //目录节点
{
    string name;
    map<string,int> child;
    int father = 0;
    
    void init(string n,int fa)
    {
        name = n;
        father = fa;
    }
};

char mark[2] = {'.','/'};
menu tree[1000];
vector<int> path;
int num = 0,cur = 0,tail = 0;    //标记目录数组下标,标记初始化的当前目录

int finding(char c)     //在符号数组中寻找
{
    for( int i = 0 ; i < 2 ; i++ )
    {
         if( c == mark[i] )
             return i;
    }
    return -1;
}

void change(string s)
{
    
    int before = cur;       //上一个到达的目录,记录该路径最终到达的目录
    string now_name;
    
    
    for( int i = 0 ; i < s.size() ; i++ )           //遍历整个字符串
    {
//        cout<<i<<" -------- i "<<endl;
        switch (finding(s[i]))              //确定当前字符类型
        {
            case -1:            //属于文件名字符
            {
                now_name.clear();
                
                int j = i;
                while( finding(s[j]) == -1 && j < s.size())  //记录所有非符号
                {
                    now_name.push_back(s[j]);
                    j++;
                }
                
                //如果当前目录在上一个目录的孩子中不存在,则新建目录
                if( tree[before].child.find(now_name) == tree[before].child.end()  )
                {
                    tree[++num].init(now_name,before);      //新建目录
                    tree[before].child[tree[num].name] = num;   //将该文件名放在父亲目录下
                    before = num;       //当前文件成为新的父目录
                }
                else        //否则直接更新当前目录
                    before = tree[before].child[now_name];
                
//                cout<<now_name<<" *** "<<i<<" -i- "<<j<<" -j- "<<endl;
                
                //如果是最后一个字符且是非符号,则最后更新的这个文件即为最终到达的文件
                if( j == s.size() )
                {
                    tail = before;
                    i = j;
                }
                else                //否则继续更新遍历索引
                    i = j - 1;
                
//                cout<<before<<" before "<<tree[before].father<<" father ===="<<endl;
//                cout<<endl;
                
                break;
            }
            case 0:         //"."
            {
                if( i + 1 < s.size() )
                {
                    if( finding(s[i + 1]) == 0 )    //如果下一个也是".",则当前到达目录为上一个目录的父目录
                    {
                        if( before > 0 )       //如果上一个到达的目录不是根目录,则上移
                            before = tree[before].father;
                        //否则仍然是根目录
//                        cout<<" ..... "<<before<<" before "<<endl;
                    }
                }
                else        //如果当前已经是最后一个字符,则上一个到达的文件即为最终到达的文件
                    tail = before;
                
                break;
            }
            case 1:     //"/"
            {
                if( i == 0 )        //如果是第一个遇到的/,说明当前目录从root开始
                    before = 0;
                //否则遇到"/"就直接跳过,直到不是"/"
                if( i == s.size() - 1 )
                    tail = before;
                
//                cout<<tail<<" ------ tail "<<endl;
            }
            default:
                break;
        }
    }
}

void handle(string s)       //确认当前目录
{
    string now_name;
    
    for( int i = 0 ; i < s.size() ; i++ )
    {
        if( finding(s[i]) == 1 )
        {
            if( i == 0 )    //第一个"/",将cur初始化为0,代表根目录
                cur = 0;
            else
                continue;
        }
        else
        {
            now_name.clear();
            
            int j = i;
            while( finding(s[j]) == -1 && j < s.size())  //记录所有非符号
            {
                now_name.push_back(s[j]);
                j++;
            }
            
            //如果上一个目录已经存在这个孩子目录,则直接更新当前目录,否则创建新目录
            if( tree[cur].child.empty() )
            {
                tree[++num].init(now_name,cur);      //新建目录
                tree[cur].child[tree[num].name] = num;   //将该文件名放在父亲目录下
            }
            
            cur = tree[cur].child[now_name];        //更新当前目录
            
            if( j == s.size() )
                i = j;
            else
                i = j - 1;
            
        }
    }
}

void output()           //输出正规路径
{
//    cout<<tail<<" ============= tail !! "<<endl;
    
    if( tail == 0 )         //如果最终到达根目录
    {
        cout<<"/"<<endl;        //直接输出“/”
    }
    else
    {
        for( int i = tail ; i != -1 ; i = tree[i].father )  //将整个路径反向记录
            path.push_back(i);
        
        for( int i = path.size() - 2 ; i >= 0 ; i-- )    //再正向输出(不用输出root)
            cout<<"/"<<tree[path[i]].name;
        
        cout<<endl;
    }
}

void initialize()         //初始化
{
    path.clear();
    tail = 0;
    tree[0].init("root", -1);
}

int main()
{
    int p = 0;
    cin>>p;
    cin.ignore();
    
    string now_menu,route;
    getline(cin,now_menu);
    
    handle(now_menu);       //确认当前目录
    
    for( int i = 0 ; i < p ; i++ )
    {
        initialize();
        
        getline(cin,route);
        
        if( !route.empty() )        //路径不空进行转换
            change(route);       //确认该路径最终到达的文件
        else                        //否则即为当前目录
            tail = cur;
        
        output();
    }
    
    
    
    return 0;
}


总结

  1. 还是老是把问题复杂化,最后写出来的代码调试起来就很困难,因为复杂化后就很难找到问题。
  2. string虽然很方便,但是真的有很多幺蛾子😑不过还是学习到了rfind这样的神奇操作
  3. 小细节往往是一道大题满分的关键!例如这个输入流的考点

代码

//
//
//  main.cpp
//  lab1
//
//

#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#include <map>
#include <sstream>
using namespace std;


int main()
{
    int p = 0,site = 0,site2 = 0;
    cin>>p;
    
    string now_menu,route,ans;
    cin>>now_menu;
    
    cin.ignore();
    
    
    for( int i = 0 ; i < p ; i++ )
    {
        getline(cin,route);
        
        ans.clear();
        
        if( !route.empty() )
        {
            if( route[0] != '/' )   //如果第一个不是/,说明从当前目录起始,将当前目录路径添加在前
                ans = now_menu + '/' + route;
            else                    //否则不变,从根目录起始
                ans = route;
            
            //删除“//”
            while( (site = ans.find("//")) != -1 )         //出现两个“/”就删除前一个
                ans.erase(site, 1);
            
            //删除“/./”
            while( (site = ans.find("/./")) != -1 )         //出现“/./”就只留最后一个“/”
                ans.erase(site, 2);
            
            //删除“/../"
            //出现“/../”就删除它前面的一个目录,因为返回到了该目录的上层
            while( (site = ans.find("/../")) != -1 )
            {
                if( site == 0 )         //说明在最前面,即依然留在根目录,保留一个“/”
                    ans.erase(site,3);
                else                    //否则找他前面的第一个目录
                {
                    //从“/../”向前反向找第一个“/”,这一定是它前面的第一个目录
                    site2 = ans.rfind("/",site - 1);
                    ans.erase(site2, site - site2 + 3 );    //删除直到只剩最后一个“/“
                }
            }
            
            //删除末尾的“/”
            if( ans.size() > 1 && ans[ans.size() - 1] == '/' )
                ans.erase(ans.size() - 1,1);
            
            cout<<ans<<endl;
        }
        else        //空路径直接输出当前目录
            cout<<now_menu<<endl;
        
    }
    
    
    
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

天翊藉君

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值