算法竞赛入门经典(第2版)—第六章(数据结构基础)

文章目录

        • 零碎知识点整理
        • 题目
          • 210 - Concurrency Simulator
          • 514 - Rails
          • 442 - Matrix Chain Multiplication
          • 11988 - Broken Keyboard (a.k.a. Beiju Text)
          • 12657 - Boxes in a Line
          • 679 - Dropping Balls
          • 122 - Trees on the level
          • 548 - Tree
          • 839 - Not so Mobile
          • 699 - The Falling Leaves
          • 297 - Quadtrees
          • 572 - Oil Deposits
          • 1103 - Ancient Messages
          • 816 - Abbott's Revenge
          • 10305 - Ordering Tasks
          • 10129 - Play on Words
          • 10562 - Undraw the Trees
          • 12171 - Sculpture
          • 1572 - Self-Assembly
          • 1599 - Ideal Path
          • 506 - System Dependencies
          • 11853 - Paintball
          • 673 - Parentheses Balance
          • 712 - S-Trees
          • 536 - Tree Recovery
          • 439 - Knight Moves
          • 1600 - Patrol Robot
          • 12166 - Equilibrium Mobile
          • 10410 - Tree Reconstruction
          • 12118 - Inspector's Dilemma

零碎知识点整理

  • fill和memset函数。参考博文:【C++】fill函数,fill与memset函数的区别
    • fill的头文件为#include 。可以将数组或容器赋任何值。
      • 对于数组:fill(arr, arr + n, 要填入的内容);
      • 对于容器:fill(v.begin(), v.end(), 要填入的内容);
    • memset的头文件为#include 。只能为int类型数组的赋值为0或-1。
      • 对于数组:memset(a, 0, sizeof(a))。
    • 初始化赋值为非0的int数组或容器,推荐使用fill。
  • 欧拉回路:从图中一个点出发走出一条路,每条边恰好经过一次。判断欧拉道路的条件:
    • 无向图:1.无向图连通。2.最多有两个度数为奇数的点。
      • 当度数均为偶数时,为欧拉回路。
    • 有向图:1.忽略边的方向后,有向图连通。2.最多有两个入度不等于出度,其中一个多1,另一个少1。多1的为起始点,少1的为终止点。
      • 当入度均等于出度时,为欧拉回路。
  • freopen(“input.txt”,“r”,stdin);用来读取程序输入文件,使得文件的输入输出分开,更好调试。头文件为stdio.h。参考博文:
    freopen(“in.txt”,“r”,stdin) 函数的用法
#ifdef ONLINE_JUDGE
#else
    freopen("input.txt","r",stdin);
    freopen("output.txt","w",stdout);
#endif
  • 当图中结点之间可能有多个边相连时,必须使用邻接表来存储,而不能使用邻接矩阵。
  • find函数:参考
    • 头文件: #include

    • find函数的表示:

      • 一般表示:find(start,end,value)。start搜寻的起点,end搜寻的终点,要寻找的value值。
      • 容器表示:find(a.begin(),a.end(),value)。
      • 数组表示:find(a,a+length,val)。

      所有的返回,均是迭代器(容器)或指针(数组),而非是直观感觉上的索引下标。如果在查找范围内不存在,返回a.end(),这里需要注意的是,a.end()不在查找范围内。

    • 各个容器自己实现的find成员函数:

      • vector没有实现find函数,除此之外,常见容器都实现了自己的find函数。
      • String是这一种顺序存储结构,其find函数返回的是下标索引。set,map,multiset,multimap都不是顺序索引的数据结构,所以返回的是迭代器。
    • 判断元素是否存在。

      • 对于返回迭代器的查找,通过判断find(a.begin(),a.end(),value)==a.end(),来判断元素是否存在。
      • 对于string,通过a.find(val)==string::npos判断。

题目

210 - Concurrency Simulator

题目链接:210 - Concurrency Simulator
参考博文:uva 210 - Concurrency Simulator (并行程序模拟)

  • 题目大意:题目比较复杂。
  • 思路:大模拟题,细节比较多。详见代码。

代码:

#include
#include
#include
#include
#include
#include
using namespace std;

// 当前程序执行完,配额时间有剩余: 让下一个程序继续用
// 当配额时间用完,当前程序一句代码没执行完: 继续执行到完
// 多组输入
// 每组输出之间空一行
// 所有变量初值为0
// 最后一组的输出之后不要有空行

map<char, int> vars;
vector<deque<string> > group;
deque<int> stop;
deque<int> wait;

void fun(){
    int N,t1,t2,t3,t4,t5,Q;
    cin>>N>>t1>>t2>>t3>>t4>>t5>>Q;
    getchar(); // [注意] 接收空格

    //接收多组输入程序
    group = vector<deque<string> >(N);
    for(int i=0;i<N;i++){
        string str;
        while(str != "end"){
            getline(cin, str);
            group[i].push_back(str);
        }
        wait.push_back(i);
    }

    int index = 0; //当前第几个程序
    int count_live = N;//代表还没有结束的程序个数
    int lock = 0;  // 作为临界资源的记录型信号量,代表阻塞程序个数
    int part_time = 0; // 当前程序单位剩余时间

    while(count_live){
        int end_flag = 0; // 0:程序没结束, 1:程序end结束 -1:程序阻塞
        index = wait[0]; // 读取等待队列首
        wait.pop_front();

        part_time = Q;
        while(part_time>0){  // 一个单位时间剩余的时间
            string code = group[index][0]; // 读取程序队列首代码
            string judge = code.substr(0,2);

            if(judge == "pr"){ // print
                cout<<index+1<<": "<< vars[code[6]]<<endl;
                part_time -= t2;
            }else if(judge == "lo"){ // lock
                if(lock){  // 要阻塞,语句不执行,不记时
                    stop.push_back(index);
                    end_flag = -1;
                    part_time = 0; // 此次时间片废了
                    break; // 进入阻止队列,就不用进入等待队列了, 而且lock代码还需要执行
                }else{    // 如果还没有lock的程序,正常执行
                    lock++;
                    part_time -= t3;
                }
            }else if(judge == "un"){ // unlock
                int pro = stop[0];
                if(!stop.empty()){ // 若是最后一个解锁的,那么阻塞队列就是空
                    stop.pop_front();
                    wait.push_front(pro);
                }
                lock--;
                part_time -= t4;
            }else if(judge == "en"){ // end
                count_live --;
                part_time -= t5;
                end_flag = 1;
                break;  // 结束本程序,时间片给下一个
            }else{  // var
                int n;
                sscanf(code.substr(4).c_str(), "%d",&n);
                vars[judge[0]] = n;
                part_time -= t1;
            }
            group[index].pop_front();  // 执行成功,代码出队列
        }
        if(end_flag == 0)
            wait.push_back(index);  // 时间片完,只要程序没有end, 就会入wait队列
    }
}

int main()
{
    int T;
    cin>>T;
    for(int i=0;i<T;i++){
        if(i>0)
            cout<<endl;
        //初始化
        vars.clear();
        group.clear();
        stop.clear();
        wait.clear();
        fun();
    }
    return 0;
}
514 - Rails

题目链接:514 - Rails

  • 题目大意:火车按序进栈,给定火车出栈的顺序,问火车能否按照这个顺序出栈。
  • 思路:简单的栈应用,序号每次入栈后,均需要判断栈顶是否与当前出栈序号相同,相同则出栈,然后继续判断栈顶与下一个序号是否相同,如此循环,不相同则继续按序进栈。

代码:

#include 
#include 
#include 
#include 
using namespace std;

int main()
{
    stack<int> q;
    int a[5000], N, kase = 0;
    while(cin >> N && N)
    {
        while(cin >> a[1] && a[1])
        {
            //清空栈
            while(!q.empty()) q.pop();
            for(int i=2; i<=N; i++)
                cin >> a[i];
            int cnt = 1;
            for(int i=1; i<=N; i++)//按序进栈
            {
                q.push(i);
                while(!q.empty() && a[cnt]==q.top())//比较栈顶是否可以出栈
                    q.pop(), cnt++;//遍历下一个出栈序号
            }
            if(q.empty())//栈空,则为真
                printf("Yes\n");
            else
                printf("No\n");
        }
        printf("\n");
    }
    return 0;
}
442 - Matrix Chain Multiplication

题目链接:442 - Matrix Chain Multiplication

  • 题目大意:输入一些矩阵乘法式子,有括号。求矩阵相乘需要的加法步数。
  • 思路:简单栈模拟。先建立一个映射map,将字母与结构体(包括行和列)对应,然后遍历式子,遇到字母,将其对应的结构体入栈;遇到’)'将其栈顶的两个结构体计算一下,并计算出新的结构体,入栈,直到栈为空。

代码:

#include 
#include 
#include 
#include 
#include 
using namespace std;

typedef long long LL;
struct Node
{
    int r, c;
    Node(int r=-1, int c=-1):r(r), c(c){}
};
map<char, Node>m;
stack<Node> stc;
string s;

int main()
{
    int n, r, c;
    char ch;
    cin >> n;
    for(int i=0; i<n; i++)
    {
        cin >> ch >> r >> c;
        m[ch] = Node(r, c);
    }
    while(cin >> s)
    {
        while(!stc.empty()) stc.pop();
        LL sum = 0, flag = 1;
        for(int i=0; i<s.size(); i++)
        {
            if(isalpha(s[i]))//判断为字母
            {
                stc.push(m[s[i]]);
            }
            else if(s[i]==')')//计算
            {
                Node a = stc.top(); stc.pop();
                Node b = stc.top(); stc.pop();
                if(b.c!=a.r)//无法计算
                {
                    flag = 0;
                    break;
                }
                else//可以计算
                {
                    stc.push(Node(b.r, a.c));
                    sum += b.r*b.c*a.c;
                }
            }
        }
        if(flag) printf("%lld\n", sum);
        else     printf("error\n");
    }
    return 0;
}
11988 - Broken Keyboard (a.k.a. Beiju Text)

题目链接:11988 - Broken Keyboard (a.k.a. Beiju Text)

  • 题目大意:你有一个破损的键盘,键盘上所有的键都可以正常工作,但有时Home键和End键会自动按下,你不知道键盘存在这样的问题,当你输入完成以后屏幕上显示的是一串混乱的文本,你的任务是要输出这段混乱的文本是什么。
    多组输入,每组输入一行字符串,其中 ‘[ ’ 代表Home键,’ ]’代表End键
    每组数据输出一行,即在屏幕上显示的文本。
  • 思路:STL中list的使用。注意迭代器指向的是特定元素,要与数组下标区分开来,list.insert(it, ch)表示在it指向的元素前面添加元素。思路就是模拟题意即可。

代码:

#include 
#include 
#include 
#include 
#include 
using namespace std;

list<char> s;
char ch;
int main()
{
    int flag = 1, cnt = 0;
    list<char>::iterator it;
    while(ch=getchar())
    {
        if(ch==EOF) break;
        if(ch=='\n')
        {
            for(it=s.begin(); it!=s.end(); it++)
                printf("%c", *it);
            printf("\n");
            s.clear();
        }
        else if(ch==']') cnt = s.size();
        else if(ch=='[') cnt = 0;
        else
        {
            if(cnt==0)//当前面一个字符是'['
            {
                it = s.begin();//迭代器移到最前面
                s.push_front(ch);
            }
            else if(cnt==s.size())//当遇到[后或初始状态
            {
                s.push_back(ch);//在字符串后面添加
            }
            else
            {
                s.insert(it, ch);//在it指向的元素(原首部元素)的前方插入元素
            }
            cnt++;
        }
    }
    return 0;
}
12657 - Boxes in a Line

题目链接:12657 - Boxes in a Line12657 - Boxes in a Line

  • 题目大意:開始有n个盒子按1到n的顺序排列 对这些盒子进行m次操作 每次为把x移到y的左边 、右边、交换x,y 颠倒顺序中的一个。求操作完毕后全部奇数位原盒子序号的和。
  • 思路:一个链表模拟题,不能使用STL中的list,因为无法在O(1)复杂度下定位到某一个位置。本题目有一定的技巧,比如记录翻转的奇偶性。奇偶性不同操作不同。

代码:

#include 
#include 
using namespace std;

const int MAX = 100005;
int Right[MAX], Left[MAX];

//连接函数,方便操作
void link(int L, int R)
{
    Right[L] = R;
    Left[R] = L;
}

int main()
{
    int m, n, kase = 0;
    while(scanf("%d%d", &n, &m)!=EOF)
    {
        for(int i=1; i<=n; i++)
        {
            Left[i] = i-1;
            Right[i] = (i+1)%(n+1);
        }
        Right[0] = 1, Left[0] = n;
        int op, X, Y, inv = 0;
        while(m--)
        {
            scanf("%d", &op);
            if(op==4) inv = !inv;
            else
            {
                scanf("%d%d", &X, &Y);
                if(op==3 && Right[Y]==X) swap(X, Y);//方便后面推断翻转是否相邻,将相邻情况处理成一种
                if(op!=3 && inv)         op = 3-op;//对于操作1和2,对于翻转有对称性
                if(op==1 && X==Left[Y])  continue;
                if(op==2 && X==Right[Y]) continue;

                int LX = Left[X], RX = Right[X], LY = Left[Y], RY = Right[Y];
                if(op==1)
                {
                    link(LX, RX); link(LY, X); link(X, Y);
                }
                else if(op==2)
                {
                    link(LX, RX); link(Y, X); link(X, RY);
                }
                else if(op==3)
                {
                    if(Right[X]==Y)//相邻的情况需要单独处理
                    {
                        link(LX, Y); link(Y, X); link(X, RY);
                    }
                    else
                    {
                        link(LX, Y); link(Y, RX); link(LY, X); link(X, RY);
                    }
                }
            }
        }
        int b = 0;
        long long ans = 0;
        for(int i=1; i<=n; i++)
        {
            b = Right[b];
            if(i%2) ans += b;
        }
        //对于翻转次数为奇数,长度为偶数
        if(inv && n%2==0) ans = (long long)n*(n+1)/2-ans;
        printf("Case %d: %lld\n", ++kase, ans);
    }
    return 0;
}
679 - Dropping Balls

题目链接:679 - Dropping Balls

  • 题目大意:有一棵二叉树,最大深度为D,且所有叶子的深度都相同。所有结点从上到下从左到右编号为1, 2, 3,…, 2D-1。在结点1处放一个小球,它会往下落。每个内结点上都有一个开关,初始全部关闭,当每次有小球落到一个开关上时,状态都会改变。当小球到达一个内结点时,如果该结点上的开关关闭,则往左走,否则往右走,直到走到叶子结点,如图6-2所示。
    算法竞赛入门经典(第2版)—第六章(数据结构基础)_第1张图片
    一些小球从结点1处依次开始下落,最后一个小球将会落到哪里呢?输入叶子深度D和小球个数I,输出第I个小球最后所在的叶子编号。假设I不超过整棵树的叶子个数。D≤20。输入最多包含1000组数据。
  • 思路:本题目看似简单,可以快速使用模拟法模拟,但是数据量太大,导致模拟法超时。所以参考了书本上面的算法,对于到达当前节点的小球,如果第I个小球是第奇数个到达的,那么它应该向左走,同时也是第(I+1)/2个向左走的,如果是偶数个,应该向右走,同时也是第I/2个向右走的。然后当前节点k的左节点是2k,右节点是2k+1,不断循环直到循环深度为D为止即可。

直接模拟(超时)代码:

#include 
#include 
#include 
#include 
using namespace std;

const int MAX = 1<<21;
int flag[MAX];

void Init(int D)
{
    int m = 1<<D;
    memset(flag, 0, sizeof(flag));
}
int main()
{
    int T, D, I;
    while(scanf("%d", &T)==1 && T!=-1)
    {
        while(T--)
        {
            scanf("%d%d", &D, &I);
            Init(D);
            int cnt = 1, i = 1, ans;
            while(i<=I)
            {
                if(flag[cnt]==0)
                {
                    flag[cnt] = 1;
                    cnt = cnt*2;
                }
                else
                {
                    flag[cnt] = 0;
                    cnt = cnt*2+1;
                }
                if(cnt>=1<<(D-1))
                {
                    ans = cnt;
                    cnt = 1;
                    i++;
                }
            }
            printf("%d\n", ans);
        }
    }
    return 0;
}

AC代码:

#include 
#include 
#include 
#include 
using namespace std;

int main()
{
    int T, D, I;
    while(scanf("%d", &T)==1 && T!=-1)
    {
        while(T--)
        {
            scanf("%d%d", &D, &I);
            int k = 1;
            for(int i=0; i<D-1; i++)
            {
                if(I%2)//奇数
                {
                    k = k*2;//左子
                    I = (I+1)/2;//向左走的第I个小球
                }
                else
                {
                    k = k*2+1;//右子
                    I /= 2;//向右走的第I个小球
                }
            }
            printf("%d\n", k);
        }
    }
    return 0;
}
122 - Trees on the level

题目链接:122 - Trees on the level

  • 题目大意:输入一棵二叉树,你的任务是按从上到下、从左到右的顺序输出各个结点的值。每个结点都按照从根结点到它的移动序列给出(L表示左,R表示右)。在输入中,每个结点的左括号和右括号之间没有空格,相邻结点之间用一个空格隔开。每棵树的输入用一对空括号“()”结束(这对括号本身不代表一个结点),如图6-3所示。
    注意,如果从根到某个叶结点的路径上有的结点没有在输入中给出,或者给出超过一次,应当输出-1。结点个数不超过256。
    样例输入:
    (11,LL) (7,LLL) (8,R) (5,) (4,L) (13,RL) (2,LLR) (1,RRR) (4,RR) ()
    (3,L) (4,R) ()
    样例输出:
    5 4 8 11 13 4 7 2 1
    not complete

  • 思路:我使用了模拟法。对于"LR"的路径,"L"代表序号乘以2,"R"代表序号乘以2+1。然后使用map将序号与编号映射起来,在使用set存储所有序号。如果序号重复或父节点不存在,则表明该树不存在,否则按序号顺序输出编号即可。

代码:

#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;

const int MAX = 100000;
map<int, string> m;//树的序号与编号的映射
set<int> Set;//存储所有遍历到的树的序号
string s;
int flag;//标记是否是二叉树

//初始化函数
void Init()
{
    flag = 1;
    Set.clear();
    m.clear();
}
int main()
{
    Init();
    while(cin >> s)
    {
        if(s=="()")
        {
            set<int>::iterator it;
            if(flag)
            {
                //遍历判断是否是一个树
                for(it=Set.begin(); it!=Set.end(); it++)
                {
                    if(*it==1) continue;
                    if(m.count((*it)/2)==0)//判断其父节点是否存在
                    {
                        flag = 0;
                        break;
                    }
                }
            }
            if(!flag)
            {
                printf("not complete\n");
            }
            else
            {
                //打印输出结果
                for(it=Set.begin(); it!=Set.end(); it++)
                {
                    if(it!=Set.begin()) printf(" ");
                    printf("%s", m[*it].c_str());
                }
                printf("\n");
            }
            Init();
            continue;
        }
        
        int len = s.size();
        int k = s.find(',');
        //分割字符串
        string id = s.substr(1, k-1);
        string path = s.substr(k+1, len-k-1);
        int cnt = 1;
        for(int i=0; i<path.size(); i++)
        {
            if(path[i]=='L')      cnt = cnt*2;
            else if(path[i]=='R') cnt = cnt*2+1;
        }
        if(m.count(cnt)==0) m[cnt] = id;//存储结点
        else                flag = 0;//路径重复
        Set.insert(cnt);
    }
    return 0;
}

书中的代码:

#include 
#include 
#include 
#include 

using namespace std;

const int maxNum = 256 + 5;

// 二叉树节点类型
struct Node {
    // 是否被赋值
    bool isAssign;
    // 值
    int value;
    // 左子节点
    Node *left;
    // 右子节点
    Node *right;
    // 构造方法
    Node() : isAssign(false), left(NULL), right(NULL){ }
};
// 根节点
Node *root;

// 是否有错
bool failed;

// 创建一个新节点
Node * newNode() {
    return new Node();
}

// 递归释放二叉树
void removeTree(Node *p) {
    // 节点为空返回
    if(p == NULL) {
        return;
    }
    // 删除左子树
    removeTree(p->left);
    // 删除右子树
    removeTree(p->right);
    // 调用对象p的析构方法释放p节点占用的内存
    delete p;
}

// val为插入节点的值
// seq为插入节点的序列
void insertNode(int val, char* seq) {
    int n = strlen(seq);
    Node *p = root;
    // 循环读取插入结点序列
    // 包含了根节点的情况
    for(int i = 0; i < n; i++) {
        // 往左子树走
        if(seq[i] == 'L') {
            // 左子节点不存在
            if(p->left == NULL) {
                // 创建一个新节点,并挂在p的左子节点域上
                p->left = newNode();
            }
            // 指向该节点
            p = p->left;
        } else if(seq[i] == 'R') {
            if(p->right == NULL) {
                // 创建一个新节点,并挂在p的右子节点域上
                p->right = newNode();
            }
            // 指向该节点
            p = p->right;
        }
    }
    // 已经赋值过了,则表示输入有误
    if(p->isAssign) {
        failed = true;
    }
    p->value = val;
    // 标记已赋值
    p->isAssign = true;
}

char s[maxNum];
// 读取输入
bool readInput() {
    // 移除上一颗二叉树
    removeTree(root);
    // 初始化
    failed = false;
    root = newNode();
    while(true) {
        if(scanf("%s", s) != 1) {
            return false;
        }
        // ()退出输入节点
        if(!strcmp(s, "()")) {
            break;
        }
        // 字符串转数字
        int val;
        sscanf(&s[1], "%d", &val);
        // 插入节点
        insertNode(val, strchr(s, ',') + 1);
    }
    return true;
}

// 广度优先遍历
// 返回值为是否某一节点未被赋值
bool bfs(vector<int> &ans) {
    // 广度优先遍历所需要的队列
    queue<Node *> q;
    // 清除上一次的序列
    ans.clear();
    // 初始时只有一个根节点
    q.push(root);
    while(!q.empty()) {
        Node *p = q.front();
        q.pop();
        // 表示有节点未被赋值
        if(!p->isAssign) {
            return false;
        }
        // 将序列加入结果容器
        ans.push_back(p->value);
        if(p->left != NULL) {
            // 左子节点入栈
            q.push(p->left);
        }
        if(p->right != NULL) {
            // 右子节点入栈
            q.push(p->right);
        }
    }
    return true;
}


int main() {
    // 层次遍历序列
    vector<int> ans;
    while(readInput()) {
        // 通过广度优先遍历,也就是队列得到层次遍历序列
        if(!bfs(ans)) {
            failed = true;
        }
        if(failed) {
            printf("not complete\n");
        } else {
            for(int i = 0; i < ans.size(); i++) {
                if(i != 0) {
                    printf(" ");
                }
                printf("%d", ans[i]);
            }
            printf("\n");
        }
    }
    return 0;
}
548 - Tree

题目链接:548 - Tree

  • 题目大意:根据树的中序和后序遍历,求出其从路径到达叶子节点权值最小的叶子节点,当路径权值和相等时,取自身权值小的叶子节点。
  • 思路:根据中序和后序建立二叉树后,深度优先遍历即可。

代码:

#include 
#include 
#include 
#include 
using namespace std;

const int maxv = 10010;
int in_order[maxv], post_order[maxv], lch[maxv], rch[maxv];
int n, best, best_sum;

//处理读取的字符串
bool read_list(int *a)
{
    string line;
    if(!getline(cin, line)) return false;
    stringstream ss(line);
    n = 0;
    int x;
    while(ss >> x) a[n++] = x;
    return n>0;
}

//把in_order[L1..R1]和post_order[L2...R2]建成一棵二叉树
int build(int L1, int R1, int L2, int R2)
{
    if(L1>R1) return 0;
    int root = post_order[R2];//当前子树的根节点
    int p = L1;
    while(in_order[p]!=root) p++;//在中序中寻找根节点
    int cnt = p-L1;//子树的长度
    lch[root] = build(L1, p-1, L2, L2+cnt-1);
    rch[root] = build(p+1, R1, L2+cnt, R2-1);
    return root;
}

void dfs(int u, int sum)
{
    sum += u;
    if(!lch[u] && !rch[u])
    {
        if(sum<best_sum || (sum==best_sum && u<best))
            best_sum = sum, best = u;
    }
    if(lch[u]) dfs(lch[u], sum);
    if(rch[u]) dfs(rch[u], sum);
}

int main()
{
    while(read_list(in_order))
    {
        read_list(post_order);
        build(0, n-1, 0, n-1);
        best_sum = 1000000000;//赋初值
        dfs(post_order[n-1], 0);
        cout << best << endl;
    }
    return 0;
}
839 - Not so Mobile

题目链接:839 - Not so Mobile

  • 题目大意:
    算法竞赛入门经典(第2版)—第六章(数据结构基础)_第2张图片
  • 思路:该题目使用巧妙的递归,简化了大量代码。思路值得学习。

代码:

#include 
#include 
using namespace std;

bool solve(int &W)
{
    int W1, W2, D1, D2;
    bool b1 = true, b2 = true;
    cin >> W1 >> D1 >> W2 >> D2;
    if(!W1) b1 = solve(W1);//检查左子树是否平衡,并计算左子树的总重量
    if(!W2) b2 = solve(W2);//检查右子树是否平衡,并计算右子树的总重量
    W = W1+W2;//计算当前树的总重量,向上一层传值
    return b1 && b2 &&(W1*D1==W2*D2);//左右子树和当前树是否平衡
}
int main()
{
    int T, W;
    cin >> T;
    while(T--)
    {
        if(solve(W)) cout << "YES" << endl;
        else        cout << "NO" << endl;
        if(T) cout << endl;
    }
    return 0;
}
699 - The Falling Leaves

题目链接:699 - The Falling Leaves

  • 题目大意:给一棵二叉树,每个结点都有一个水平位置:左子结点在它左边1个单位,右子结点在右边1个单位。从左向右输出每个水平位置的所有结点的权值之和。如图6-7所示,从左到右的3个位置的权和分别为7,11,3。按照递归(先序)方式输入,用-1表示空树。
    算法竞赛入门经典(第2版)—第六章(数据结构基础)_第3张图片
    样例输入:

5 7 -1 6 -1 -1 3 -1 -1
8 2 9 -1 -1 6 5 -1 -1 12 -1 -1 3 7 -1 -1 -1
-1

样例输出:

Case 1:
7 11 3
Case 2:
9 7 21 15

  • 思路:采用了递归输入,技巧性应用。同时还需要选择出根节点的位置。

代码:

#include 
#include 
#include 
using namespace std;

const int MAX = 10010;
int ans[MAX], x;
void build(int cnt)
{
    cin >> x;
    if(x!=-1)
    {
        ans[cnt] += x;
        build(cnt-1);
        build(cnt+1);
    }
}

int main()
{
    int root, kase = 1;
    while(cin >> root && root!=-1)
    {
        memset(ans, 0, sizeof(ans));
        int pos = MAX/2;//根节点在位置中间
        ans[pos] = root;
        build(pos-1);
        build(pos+1);
        int p = pos;
        while(ans[p]!=0) p--;//寻找最左边的叶节点
        p = p+1;
        printf("Case %d:\n", kase++);
        while(ans[p+1])//输出
        {
            printf("%d ", ans[p++]);
        }
        printf("%d\n\n", ans[p]);
    }
    return 0;
}
297 - Quadtrees

题目链接:297 - Quadtrees

  • 题目大意:如图所示,可以用四分树来表示一个黑白图像,方法是用根结点表示整幅图像,然后把行列各分成两等分,按照图中的方式编号,从左到右对应4个子结点。如果某子结点对应的区域全黑或者全白,则直接用一个黑结点或者白结点表示;如果既有黑又有白,则用一个灰结点表示,并且为这个区域递归建树。
    算法竞赛入门经典(第2版)—第六章(数据结构基础)_第4张图片
    给出两棵四分树的先序遍历,求二者合并之后(黑色部分合并)黑色像素的个数。p表示中间结点,f表示黑色(full),e表示白色(empty)。

样例输入:

3
ppeeefpffeefe
pefepeefe
peeef
peefe
peeef
peepefefe

样例输出:

There are 640 black pixels.
There are 512 black pixels.

  • 思路:与上面两道题类似,只不过需要使用数组记录一下,方便累加。

代码:

#include 
#include 
#include 
#include 
using namespace std;

const int MAX = 500;
int buf[33][33], ans;
char ch;

//以(r,c)为左上角,w长度的区域内判断其颜色
void solve(int r, int c, int w)
{
    ch = getchar();//获得输入
    if(ch=='p')//四个编号
    {
        solve(r, c+w/2, w/2);
        solve(r, c, w/2);
        solve(r+w/2, c, w/2);
        solve(r+w/2, c+w/2, w/2);
    }
    else if(ch=='f')
    {
        //黑色赋值
        for(int i=r; i<r+w; i++)
        {
            for(int j=c; j<c+w; j++)
            {
                if(buf[i][j]==0)
                    buf[i][j] = 1, ans++;
            }
        }
    }
}

int main()
{
    int T;
    cin >> T;
    getchar();
    while(T--)
    {
        ans = 0;
        memset(buf, 0, sizeof(buf));
        //两个树累加
        solve(0, 0, 32);
        getchar();
        solve(0, 0, 32);
        getchar();
        printf("There are %d black pixels.\n", ans);
    }
    return 0;
}
572 - Oil Deposits

题目链接:572 - Oil Deposits

  • 题目大意: 在一个字符矩阵中,计算@连通块的数量。
  • 思路:dfs简单题,遍历+dfs。

代码:

#include 
#include 
#include 
using namespace std;

const int MAX = 101;
string f[MAX];
int m, n;
int dx[8] = {0, 1, 0, -1, 1, 1, -1, -1};
int dy[8] = {1, 0, -1, 0, 1, -1, 1, -1};

void dfs(int r, int c)
{
    f[r][c] = '*';
    for(int i=0; i<8; i++)
    {
        int x = r+dx[i], y = c+dy[i];
        if(x<0 || x>=m || y<0 || y>=n) continue;
        if(f[x][y]=='@') dfs(x, y);
    }
}

int main()
{
    while(cin >> m >> n && (n+m))
    {
        getchar();
        for(int i=0; i<m; i++)
            getline(cin, f[i]);
        int ans = 0;
        for(int i=0; i<m; i++)
        {
            for(int j=0; j<n; j++)
            {
                if(f[i][j]=='@')
                    dfs(i, j), ans++;
            }
        }
        printf("%d\n", ans);
    }
    return 0;
}
1103 - Ancient Messages

题目链接:1103 - Ancient Messages

  • 题目大意:输入一些十六进制的矩阵,将它转换为二进制后,根据01的形状寻找相应字符,具体细节看原题。
  • 思路:这道题目思路比较巧妙,不同字符的区别仅在于其中围起来圈圈的个数。但需要注意在遍历前需要为其外围再围上一圈’0’,防止最外围是’1’。而得到圈圈的方法是,遍历为每一个相邻相等的字符打上相同的编号,非0编号的’0’一定是围起来的’0’。通过遍历不同编号’1’周围’0’的编号种类数目,可以得到圈圈的个数。具体细节详见代码。

代码:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;

const int MAX = 300;
string t[MAX], s[MAX];//t是输入的十六进制,s是转换的二进制
int vis[MAX][MAX];//矩阵的编号,-1代表未遍历
vector<int> v;//记录二进制各个'1'的编号
set<int> Set[MAX];//记录圈圈,方便根据圈圈个数查找相应字符串
map<int, char> mp;//圈圈个数与相应字符的映射
vector<char> ans;//存储答案字符
int dx[5] = {0, 0, 1, -1};
int dy[5] = {1, -1, 0, 0};
int m, n, len;

//初始化各种容器
void Init()
{
    v.clear();
    ans.clear();
    for(int i=0; i<m+2; i++)
        s[i].clear();
    memset(vis, -1, sizeof(vis));
}
//将一个十六进制的数字转换为二进制字符串
void Transform(int n, int i)//n是十六进制数字,i是下标
{
    int a = n, b = n, len = 0;
    string cur = "";
    while(a)
    {
        b = a%2;
        cur = char(b+'0')+cur;
        a = a/2;
        len++;
    }
    while(len++<4)
        cur = '0'+cur;
    s[i] += cur;
}
//dfs为各个元素打上编号,相邻相等的元素的编号相同
void dfs(int r, int c, int cnt)
{
    vis[r][c] = cnt;
    for(int i=0; i<4; i++)
    {
        int x = r+dx[i], y = c+dy[i];
        if(x>=0 && x<m && y>=0 && y<=n && s[r][c]==s[x][y] && vis[x][y]==-1)
        {
            dfs(x, y, cnt);
        }
    }
}
int main()
{
    int kase = 1;
    mp[0] = 'W', mp[1] = 'A', mp[2] = 'K';
    mp[3] = 'J', mp[4] = 'S', mp[5] = 'D';
    while(cin >> m >> n && (m+n))
    {
        getchar();
        Init();
        for(int i=1; i<=m; i++)
        {
            getline(cin, t[i]);
            for(int j=0; j<n; j++)
            {
                if(isdigit(t[i][j]))
                    Transform(t[i][j]-'0', i);
                else
                    Transform(t[i][j]-'a'+10, i);
            }
        }

        //为s的上下左右各添加一行存储'0',方便遍历和判断
        len = s[1].size();
        for(int j=0; j<len; j++)
        {
            s[0]+='0';
            s[m+1]+='0';
        }
        m += 2;

        for(int i=0; i<m; i++)
        {
            s[i] += '0';
            s[i] = '0'+s[i];
        }
        n = len+2;

        //遍历,打上编号
        int cnt = 0;
        for(int i=0; i<m; i++)
        {
            for(int j=0; j<n; j++)
            {
                if(vis[i][j]==-1)//还未遍历过
                {
                    dfs(i, j, cnt++);
                    if(s[i][j]=='1')//'1'的不同编号
                        v.push_back(cnt-1);
                }
            }
        }

       //再次遍历,寻找圈圈
        for(int i=0; i<m; i++)
        {
            for(int j=0; j<n; j++)
            {
                if(s[i][j]=='1')
                {
                    for(int k=1; k<5; k++)
                    {
                        int x = i+dx[k], y = j+dy[k];
                        //'1'周围编号不为0的'0'一定是被围起来的
                        if(x>=0 && x<m && y>=0 && y<n && s[x][y]=='0' && vis[x][y]!=0)
                        {
                            Set[vis[i][j]].insert(vis[x][y]);//可以得到被相同编号的'1'围起来了多少种'0',即圈圈的个数
                        }
                    }
                }
            }
        }
        //根据不同的'1'围起来的圈圈的个数查找出现的字符
        for(int i=0; i<v.size(); i++)
        {
            int cnt = Set[v[i]].size();
            ans.push_back(mp[cnt]);
            Set[v[i]].clear();
        }
        //排序
        sort(ans.begin(), ans.end());
        //输出
        printf("Case %d: ", kase++);
        for(int i=0; i<ans.size(); i++)
        {
            printf("%c", ans[i]);
        }
        printf("\n");
    }
    return 0;
}
816 - Abbott’s Revenge

题目链接:816 - Abbott’s Revenge

  • 题目大意:迷宫从起点走到终点,进入某点的朝向不同,可以出去的方向也不同,输出最短路经。
  • 思路:一个bfs的变体,思想不难,就是代码量比较大,使用了较多的数据结构。
    当代码量比较大时,一定要注意细节,伪代码要详细,思考要缜密,代码量虽然大,但要清清楚楚。
    代码:
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;

string name, signs;
int sx, sy, ex, ey, x, y;
char dirs;
string d = "NWSE";
map<char, int> m;//'NWSE’与数字的映射
map<int, vector<char> > s[10][10];//记录各个路口的标志方向
int vis[10][10][5];//标记是否已经遍历过,坐标和方向
struct Node
{
    int r, c, dir;
    Node(int r=-1, int c = -1, int dir=-1):r(r), c(c), dir(dir){}
    bool operator < (const Node &A) const
    {
        if(r==A.r)
        {
            if(c==A.c)
            {
                return dir<A.dir;
            }
            else return c<A.c;
        }
        else return r<A.r;
    }
};
queue<Node> q;//队列用于bfs遍历
stack<Node> stc;//用来使得路径正序
map<Node, Node> pre;//储存路径,用来寻路
//初始化
void Init()
{
    memset(vis, 0, sizeof(vis));
    pre.clear();
    for(int i=0; i<10; i++)
    {
        for(int j=0; j<10; j++)
            s[i][j].clear();
    }
}
void bfs(Node u)
{
    Node n;
    while(!q.empty()) q.pop();
    q.push(u);
    while(!q.empty())
    {
        Node v = q.front(); q.pop();
        vis[v.r][v.c][v.dir] = 1;
        if(v.r==ex && v.c==ey)//到达目的地
        {
            n = v;
            break;
        }
        if(s[v.r][v.c].count(v.dir)==0) continue;
        int len = s[v.r][v.c][v.dir].size();//通过坐标的朝向得到下一步的方向
        //遍历下一步
        for(int j=0; j<len; j++)
        {
            int dir_next = v.dir, r_next, c_next;
            if(s[v.r][v.c][v.dir][j]=='L')
            {
                dir_next = (v.dir+1+4)%4;
                if(v.dir==0)      r_next = v.r, c_next = v.c-1;
                else if(v.dir==1) r_next = v.r+1, c_next = v.c;
                else if(v.dir==2) r_next = v.r, c_next = v.c+1;
                else if(v.dir==3) r_next = v.r-1, c_next = v.c;
            }
            else if(s[v.r][v.c][v.dir][j]=='R')
            {
                dir_next = (v.dir-1+4)%4;
                if(v.dir==0)      r_next = v.r, c_next = v.c+1;
                else if(v.dir==1) r_next = v.r-1, c_next = v.c;
                else if(v.dir==2) r_next = v.r, c_next = v.c-1;
                else if(v.dir==3) r_next = v.r+1, c_next = v.c;
            }
            else
            {
                if(v.dir==0)      r_next = v.r-1, c_next = v.c;
                else if(v.dir==1) r_next = v.r, c_next = v.c-1;
                else if(v.dir==2) r_next = v.r+1, c_next = v.c;
                else if(v.dir==3) r_next = v.r, c_next = v.c+1;
            }
            if(r_next>=1 && r_next<=9 && c_next>=1 && c_next<=9 && vis[r_next][c_next][dir_next]==0)//判断是否可以作为下一步点
            {
                q.push(Node(r_next, c_next, dir_next));
                pre[Node(r_next, c_next, dir_next)] = v;//用来储存路径
            }
        }
    }
    if(n.r==-1)//表明没有到达目的地
    {
        printf("  No Solution Possible\n");
        return;
    }
    //用栈使得逆序路径正过来
    while(1)
    {
        stc.push(n);
        if(n.r==u.r && n.c==u.c && n.dir==u.dir) break;
        n = pre[n];
    }
    //按格式输出
    printf("  (%d,%d)", sx, sy);
    int cnt = 2;
    while(!stc.empty())
    {
        if((cnt-1)%10==0) printf(" ");
        printf(" (%d,%d)", stc.top().r, stc.top().c);
        if(cnt%10==0 && cnt>0) printf("\n");
        cnt++;
        stc.pop();
    }
    if((cnt-1)%10)
        printf("\n");
}
int main()
{
    m['N'] = 0, m['W'] = 1;
    m['S'] = 2, m['E'] = 3;
    while(cin >> name && name!="END")
    {
        Init();
        //处理输入
        cin >> sx >> sy >> dirs >> ex >> ey;
        while(cin >> x && x)
        {
            cin >> y;
            while(cin >> signs && signs!="*")
            {
                for(int i=1; i<signs.size(); i++)
                    s[x][y][m[signs[0]]].push_back(signs[i]);
            }
        }
        printf("%s\n", name.c_str());
        //从初始点得到下一个点
        Node u = Node(sx, sy, m[dirs]);
        if(u.dir==0)      u.r = u.r-1;
        else if(u.dir==1) u.c = u.c-1;
        else if(u.dir==2) u.r = u.r+1;
        else if(u.dir==3) u.c = u.c+1;
        if(u.r<1 || u.r>9 || u.c<1 || u.c>9)
        {
            printf("  No Solution Possible\n");
            continue;
        }
        bfs(u);
    }
    return 0;
}
10305 - Ordering Tasks

题目链接:10305 - Ordering Tasks
拓扑排序的两种方法:UVA - 10305 Ordering Tasks(拓扑排序模板题)

  • 题目大意: 给出n组二元数据,前面的的序号表示这个任务先于后面序号的任务,要求将任务先后排序(不一定有一种)。
  • 思路:典型的拓扑排序题目。拓扑排序思想是每一次选择入度为0的点,然后删去其所有出度边,这样不断迭代,直到所有点均被选择。本题使用了队列来存储入度为0的点,不断更新队列,直到将所有的点遍历。

代码:

#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;

const int MAX = 105;
int n, m, x, y;
int G[MAX][MAX], in_degree[MAX];
vector<int> ans;

bool toposort()
{
    queue<int> q;
    for(int i=1; i<=n; i++)//取所有入度为0的点
    {
        if(in_degree[i]==0)
            q.push(i);
    }
    while(!q.empty())//如果存在环的图,那么一定存在无论如何去除边均存在入度不为0的点
    {
        int u = q.front(); q.pop();
        ans.push_back(u);//存结果
        for(int i=1; i<=n; i++)
        {
            if(G[u][i])//存在出度的边
            {
                G[u][i] = 0;//删除该边
                in_degree[i]--;//入度减一
                if(in_degree[i]==0)
                    q.push(i);
            }
        }
    }
    if(ans.size()<n) return 0;
    else             return 1;
}
int main()
{
    while(scanf("%d%d", &n, &m)==2 && (n+m))
    {
        //初始化
        ans.clear();
        memset(G, 0, sizeof(G));
        memset(in_degree, 0, sizeof(in_degree));
        while(m--)
        {
            scanf("%d%d", &x, &y);
            G[x][y] = 1;
            in_degree[y]++;
        }
        if(toposort())
        {
            for(int i=0; i<ans.size(); i++)
            {
                if(i) printf(" ");
                printf("%d", ans[i]);
            }
            printf("\n");
        }
        else
            printf("No\n");
    }
    return 0;
}

使用dfs的拓扑排序:

#include
#include
const int maxn=100;
int n,m,u,v,t,topo[maxn],G[maxn][maxn],c[maxn];
//DFS
bool dfs(int u){
    c[u]=-1;//正在访问中
    for(int i=0;i<n;i++)if(G[u][i]){
        if(c[i]<0)return false;//存在环,退出
        if(!c[i]&&!dfs(i))return false;//i没访问过且访问后发现有环,退出 
    }
    c[u]=1;topo[--t]=u;//u顶点访问完毕,修改状态,加入拓扑序列首部
    return true; 
}
//topological ordering
bool toposort(){
    t=n;
    memset(c,0,sizeof(c));
    for(int i=0;i<n;i++)
        if(!dfs(i))return false;
    return true;
}
10129 - Play on Words

题目链接:10129 - Play on Words

  • 题目大意:输入一些单词,使得这些单词可以根据首尾字母连接成串,即一个单词的尾字母要与另一个单词的首字母相同。判断是否可以连接成串。
  • 思路:一个欧拉路径题目。题目可以转换成一个有向图的欧拉路径,因此需要存储每一个结点的出入度数,判断其是否满足欧拉路径的出入度条件。然后使用dfs判断是否可以遍历无向图的所有结点(使用set存储遍历到的结点然后与之前存储的结点比较)。

代码:

#include 
#include 
#include 
#include 
using namespace std;

int G[50][50];
int indeg[50], outdeg[50];
int cnt, vis[50];
void dfs(int u)
{
    vis[u] = 1;
    cnt++;//记录连通图中点的个数
    for(int i=0; i<26; i++)
    {
        if(!vis[i] && G[u][i])
        {
            dfs(i);
        }
    }
}
int main()
{
    int n, T;
    string s;
    cin >> T;
    while(T--)
    {
        cin >> n;
        memset(G, 0, sizeof(G));
        memset(indeg, 0, sizeof(indeg));
        memset(outdeg, 0, sizeof(outdeg));
        memset(vis, 0, sizeof(vis));
        set<int> S;
        for(int i=1; i<=n; i++)
        {
            cin >> s;
            int len = s.size();
            G[s[0]-'a'][s[len-1]-'a'] = 1;//是一个有向图
            indeg[s[len-1]-'a']++;
            outdeg[s[0]-'a']++;
            S.insert(s[len-1]-'a'), S.insert(s[0]-'a');
        }
        int dif = 0, in = 0, out = 0, st = *S.begin();//st一定取到没有入度的点,出发是个欧拉回路
        set<int>::iterator it;
        for(it=S.begin(); it!=S.end(); it++)
        {
            dif = indeg[*it]-outdeg[*it];
            if(dif>0)      in += dif;
            else if(dif<0) out += -dif;
            if(!indeg[*it]) st = *it;
        }
        if(in>1 || out>1 || in!=out)//欧拉道路的判断条件
        {
            cout << "The door cannot be opened." << endl;
            continue;
        }
        cnt = 0;//连通图中的结点数
        dfs(st);
        if(cnt<S.size()) cout << "The door cannot be opened." << endl;
        else             cout << "Ordering is possible." << endl;
    }
    return 0;
}
10562 - Undraw the Trees

题目链接:10562 - Undraw the Trees

  • 题目大意:输入一个多叉树,输出其括号表示法。
  • 思路:使用dfs的思想,对每一个子树由左到右深度优先递归遍历即可,我的代码量比较大,大多数用来寻找子树的范围了(即’-'的范围)。

我的代码:

#include 
#include 
#include 
#include 
#include 
using namespace std;

set<char> Set;
char ch;
string ans, s[300];
int n;

//ch:该子树的根字符,m是当前字符结点行,st是该行起始点,ed是该行终止点,二者用于划分子树范围
void solve(char ch, int m, int st, int ed)
{
    ans = ans+ch+'(';//添加字符
    int len = s[m].size()-1;
    ed = min(ed, len);//防止子树范围大于该行字符的大小
    for(int i=st; i<=ed; i++)
    {
        int x1 = 0, x2 = s[m+2].size()-1;//用于标记下一个子树范围
        if(Set.count(s[m][i])==0)//判断是字符结点
        {
            if(m<n && s[m+1][i]=='|')//表明该结点可以作为下一个子树的根节点
            {
                //寻找下一个子树的范围x1-x2
                for(int j=i; j>=0; j--)
                {
                    if(s[m+2][j]!='-')
                    {
                        x1 = j+1;
                        break;
                    }
                }
                for(int j=i; j<s[m+2].size(); j++)
                {
                    if(s[m+2][j]!='-')
                    {
                        x2 = j-1;
                        break;
                    }
                }
                solve(s[m][i], m+3, x1, x2);
            }
            else//该结点下没有子树
            {
                ans = ans+s[m][i]+'('+')';
            }
        }
    }
    ans += ')';
}
int main()
{
    int T, mid;
    cin >> T;
    //操作字符
    Set.insert('-'), Set.insert('|');
    Set.insert(' '), Set.insert('#');
    getchar();
    while(T--)
    {
        n = 0;//代表最大行下标
        ans = "(";
        while(getline(cin, s[n++]))
        {
            if(s[n-1]=="#") break;
        }
        n -= 2;
        if(n<0)//表明没有字符结点
        {
            printf("()\n");
            continue;
        }
        //寻找根节点及其位置
        for(int i=0; i<s[0].size(); i++)
        {
            if(Set.count(s[0][i])==0)
            {
                ch = s[0][i];
                mid = i;
                break;
            }
        }
        if(n==0)
        {
            printf("(%c())\n", ch);
            continue;
        }
        //寻找第一个子树的范围
        int x1 = 0, x2 = s[2].size()-1;
        for(int i=mid; i>=0; i--)
        {
            if(s[2][i]!='-')
            {
                x1 = i+1;
                break;
            }
        }
        for(int i=mid; i<s[2].size(); i++)
        {
            if(s[2][i]!='-')
            {
                x2 = i-1;
                break;
            }
        }
        solve(ch, 3, x1, x2);
        ans += ')';
        printf("%s\n", ans.c_str());
    }
    return 0;
}

书上的代码:

#include 
#include 
#include 
#include 
using namespace std;
const int maxn=210;
int n;
char a[maxn][maxn];
//r表示行下标,c表示列下标
void dfs(int r,int c)
{
    printf("%c(",a[r][c]);
    if(r+1<n&&a[r+1][c]=='|')//可以递归子树
    {
        int i=c;
        while(i-1>=0&&a[r+2][i-1]=='-')//找到下一个指数的列的起始点
            i--;
        while(a[r+2][i]=='-'&&a[r+3][i]!='\0')//r+2行的'-'范围和r+3行的字符串长度范围均需要满足
        {
            if(!isspace(a[r+3][i]))
                dfs(r+3,i);
            i++;
        }
    }
    printf(")");
}
void solve()
{
    n=0;//表示有效字符行数
    while(1)
    {
        gets(a[n]);
        if(a[n][0]=='#')
            break;
        else
            n++;
    }
    printf("(");
    if(n)
    {
        for(int i=0;i<=strlen(a[0]);i++)//寻找根节点字符
        {
            if(a[0][i]!=' ')
            {
                dfs(0,i);
                break;
            }
        }
    }
    printf(")\n");
}
int main()
{
    int T;
    scanf("%d",&T);
    getchar();
    while(T--)
    {
        solve();
    }
}
12171 - Sculpture

题目链接:12171 - Sculpture
参考博文:Sculpture (UVa 12171) bfs + 离散化

  • 题目大意:求有多个长方体组成的不规则立方体的体积和外表面积。
  • 思路:比较难的题目。先采用了坐标离散化处理(同时在周围加上空气坐标,最大最小的坐标一定不为建筑物),将区域使用坐标表示,然后使用bfs来从空气出发遍历,求得空气内表面积(建筑物外表面积)和空气体积(总体积-建筑物体积)。书上的代码采用了结构体函数,比较精妙,值得学习。

代码:

#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;

const int maxn = 50+5;
const int maxl = 1000+1;

int xl[maxn],yl[maxn],zl[maxn];//最小的x,y,z坐标
int xr[maxn],yr[maxn],zr[maxn];//最大的x,y,z坐标

int gra[maxn<<1][maxn<<1][maxn<<1];//离散化后的数组,下标是x,y,z坐标
int xx[maxn<<1],yy[maxn<<1],zz[maxn<<1];//存储一个长方体最大和最小的x,y,z坐标,用于离散化区域
int xcnt,ycnt,zcnt;
//表示三个维度的增减方向
int dx[6] = {1,-1,0,0,0,0};
int dy[6] = {0,0,1,-1,0,0};
int dz[6] = {0,0,0,0,1,-1};

struct Point{
    int x,y,z;
    Point(int x = 0,int y = 0,int z = 0) :
        x(x),y(y),z(z) {}
    //判断坐标是否在范围内
    bool valid()const{
        if(0<=x && 0<=y && 0<=z && x<xcnt-1 && y<ycnt-1 && z<zcnt-1) return true;
        else return false;
    }
    //判断是否是建筑物区域
    bool solid()const{
        return gra[x][y][z] == 1;
    }
    //将区域设置为已经遍历过
    void Setvis()const{
        gra[x][y][z] = 2;
    }
    //判断区域是否已经遍历过
    bool Getvis()const{
        return gra[x][y][z] ==2;
    }
    //根据方向i获取邻居区域
    Point neighbour(int i)const{
        return Point(x+dx[i],y+dy[i],z+dz[i]);
    }
    //获取该区域的体积
    int volume()const{
        return (xx[x+1]-xx[x])*(yy[y+1]-yy[y])*(zz[z+1]-zz[z]);
    }
    //获取该区域的面积
    int area(int dir)const{
        if(dx[dir] != 0) return (yy[y+1]-yy[y])*(zz[z+1]-zz[z]);
        else if(dy[dir] != 0) return (xx[x+1]-xx[x])*(zz[z+1]-zz[z]);
        else return (xx[x+1]-xx[x])*(yy[y+1]-yy[y]);
    }
};
//寻找数组num中,tar的下标
int ID_find(int *num,int len,int tar){
    return lower_bound(num,num+len,tar)-num;
}

void floodfill(int &V,int &S){
    Point c;
    c.Setvis();//标记已经遍历过
    queue<Point> que;
    que.push(c);
    while(!que.empty()){
        Point first = que.front();
        que.pop();
        V += first.volume();//累加该区域的体积
        for(int i = 0;i < 6;i++){
            Point Next = first.neighbour(i);//获取邻居区域
            if(Next.valid()){//该区域在规定范围内
                if(Next.solid()){//如果该区域是建筑物内
                    S += first.area(i);//累加该区域的外表面积
                }
                else if(!Next.Getvis()){//没有遍历到的空气区域
                    Next.Setvis();
                    que.push(Next);
                }
            }
        }
    }
    V = maxl*maxl*maxl-V;
}
//用于去重操作
void Unique(int *num,int cnt,int &ccnt){
    sort(num,num+cnt);
    ccnt = unique(num,num+cnt)-num;
}

int main()
{
#ifdef ONLINE_JUDGE
#else
    freopen("input.txt","r",stdin);
#endif // ONLINE_JUDGE
    int iCase;
    scanf("%d",&iCase);
    while(iCase--){
        int n;
        scanf("%d",&n);
        int cnt = 2;
        //相当于在建筑物周围加入空气,0一定最小,maxl一定最大,建筑物的坐标一定在二者之间
        xx[0] = yy[0] = zz[0] = 0;
        xx[1] = yy[1] = zz[1] = maxl;
        for(int i = 0;i < n;i++){
            scanf("%d%d%d%d%d%d",&xl[i],&yl[i],&zl[i],&xr[i],&yr[i],&zr[i]);
            xr[i] += xl[i],yr[i] += yl[i],zr[i] += zl[i];
            xx[cnt] = xl[i],yy[cnt] = yl[i],zz[cnt++] = zl[i];
            xx[cnt] = xr[i],yy[cnt] = yr[i],zz[cnt++] = zr[i];
        }
        //去重操作,xx变为去重后的坐标数组,xcnt变为去重后的数组大小
        Unique(xx,cnt,xcnt);
        Unique(yy,cnt,ycnt);
        Unique(zz,cnt,zcnt);
        memset(gra,0,sizeof(gra));
        for(int i = 0;i < n;i++){
            //X1代表x1[i]在xx中的下标,说明将区域离散化为了点
            int X1 = ID_find(xx,xcnt,xl[i]),X2 = ID_find(xx,xcnt,xr[i]);
            int Y1 = ID_find(yy,ycnt,yl[i]),Y2 = ID_find(yy,ycnt,yr[i]);
            int Z1 = ID_find(zz,zcnt,zl[i]),Z2 = ID_find(zz,zcnt,zr[i]);
            for(int X = X1;X < X2;X++){            //这里一定是 < ,因为是以点代体,如果点到了边界,体就出去了
                for(int Y = Y1;Y < Y2;Y++){
                    for(int Z = Z1;Z < Z2;Z++){
                        gra[X][Y][Z] = 1;//将建筑物中的区域全赋值为1,以标记
                    }
                }
            }
        }
        int V = 0,S = 0;
        floodfill(V,S);
        printf("%d %d\n",S,V);
    }
    return 0;
}
1572 - Self-Assembly

题目链接:1572 - Self-Assembly
参考博文:UVA ~ 1572 ~ Self-Assembly (拓扑排序)

  • 题目大意:有n(n≤40000)种边上带标号的正方形。每条边上的标号要么为一个大写字母后面跟着一个加号或减号,要么为数字00。当且仅当两条边的字母相同且符号相反是,两条边能够拼到一起(00不能和任何边拼在一起,包括另一条标号为00的边)。
    假设输入的每种正方形都有无穷多种,而且可以翻转和旋转,你的任务是判断能否组成一个无限大的结构。每条边要么悬空(不喝任何边相邻),要么和一个上述可拼接的边相邻。如图6-17(a)所示是3个正方形,图6-17(b)是他们组成的一个合法结构(但大小有限)。
  • 思路:本题看上去很难下手,但不难发现“可以旋转和翻转”是一个很有意思的条件,值得推敲。“无限大结构”并不一定能铺满整个平面,只需要能连出一条无限长的“通路”即可。借助旋转和翻转,可以让这条“通路”总是往右和往下延伸,因此永远不会自交。这样一来,只需要某个正方形为起点开始“铺路”,一旦可以拼上一块和起点一样的正方形,无限重复下去就能得到一个无限大的结构。
    可惜这样的分析仍然不够,因为正方形的数目n很大。进一步分析发现:实际上不需要正方形本身重复,而只需要边上的标号重复即可。这样问题就转化为:把标号看成点(一共A+ ~ Z+,A- ~ Z- 这52种,因为00不能作为拼接点),正方形看作边,得到一个有向图。则当且仅当图中存在有向环时有解。只需要做一次拓扑排序即可。
    所以本题思考的重点是如何建立有向图。如果将正方形看做边,对于A+B-C+00来说,A+实际上可以与B+,C-相连,B-可以与A-,C-相连,C+可以与A-,B+相连,所以可以根据这个特点在输入序列中将有向图建立起来,然后拓扑排序,有环则表示可以无限大,否则不可以。我的拓扑排序使用的是队列模拟法。参考博文里面使用的是dfs的拓扑排序法。

代码:

#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;

const int bias = 26;
int a[10], G[100][100], in_degree[100];
set<int> Set;
//使用队列的拓扑排序
bool toposort()
{
    queue<int> q;
    set<int> ans;
    set<int>::iterator it;
    for(it=Set.begin(); it!=Set.end(); it++)
    {
        if(in_degree[*it]==0)
        {
            q.push(*it);
        }
    }
    while(!q.empty())
    {
        int u = q.front(); q.pop();
        ans.insert(u);
        for(it=Set.begin(); it!=Set.end(); it++)
        {
            if(G[u][*it])
            {
                G[u][*it] = 0;
                in_degree[*it]--;
                if(in_degree[*it]==0)
                    q.push(*it);
            }
        }
    }
    if(ans.size()==Set.size()) return 1;
    else                       return 0;
}
void Init()
{
    Set.clear();
    memset(G, 0, sizeof(G));
    memset(in_degree, 0, sizeof(in_degree));
}
int main()
{
    int n;
    char s[10];
    while(scanf("%d", &n)!=EOF)
    {
        Init();
        for(int i=0; i<n; i++)
        {
            memset(a, -1, sizeof(a));
            scanf("%s", s);
            //提取出编号
            for(int i=1; i<=7; i+=2)
            {
                if(s[i]=='-')
                    a[i/2] = s[i-1]-'A';
                else if(s[i]=='+')
                    a[i/2] = s[i-1]-'A'+bias;
            }
            //建立有向图
            for(int i=0; i<4; i++)
            {
                if(a[i]<0) continue;//表明该位置是00
                for(int j=0; j<4; j++)
                {
                    if(a[j]<0 || i==j) continue;
                    int ajb = (a[j]+bias)%(2*bias);//a[j]可以相连的字符编号
                    if(G[a[i]][ajb]) continue;
                    G[a[i]][ajb] = 1;
                    in_degree[ajb]++;
                    Set.insert(a[i]), Set.insert(ajb);//存储所有出现的结点
                }
            }
        }
        if(toposort())
            printf("bounded\n");
        else
            printf("unbounded\n");
    }
    return 0;
}
1599 - Ideal Path

题目链接:1599 - Ideal Path

  • 题目大意:算法竞赛入门经典(第2版)—第六章(数据结构基础)_第5张图片
  • 思路:首先从终点开始进行BFS,同时记录从终点开始到每个节点的最短路径的长度,这样一直到起点,然后从起点开始进行新的一轮的BFS,在新的一轮的BFS中,我们要记录所走路径的颜色信息,每次选择颜色最小的那一条路来走,同时要注意,如果从当前的节点开始有多条颜色最小的路径,那么我们要将相应节点的编号都要存到队列当中,在下一轮的时候依次出队列分别进行下一轮的比较,保证所走的路径的颜色值是字典序最小的。

代码:

#include 
#include
#include
#include
#include
#include
#include
using namespace std;
#define MAXN 100000+5
#define MAXN2 1000000000+5;

// 因为两个结点之间可能不止一条边,因此就不能以普通的邻接矩阵来存储图
// 使用vector将所有结点的邻接点和相应的连接边存起来,下标是一一对应的
vector<int> g[MAXN];
vector<int> c[MAXN];

// d数组保存每个结点到终点的距离
// ans数组保存最短路径上的每条边的权值
int d[MAXN],ans[MAXN];
bool vis[MAXN];

// 第一遍BFS,从终点开始到起点结束
// 为每个点标记它离终点的距离
// 这样就能够保证从起点开始再来一次BFS时
// 所选择的点都是处在最短路径上的
void bfs(int x)
{
    queue<int> q;
    q.push(x);
    while(q.size())
    {
        int u=q.front();
        q.pop();
        int len=g[u].size();
        for(int i=0;i<len;i++)
        {
            int v=g[u][i];
            if(v==1)
            {
                d[v]=d[u]+1;
                return;
            }
            else if(d[v]>d[u]+1)// 当前点的所有邻接点,到终点的距离都要+1
            {
                d[v]=d[u]+1;
                q.push(v);
            }
        }
    }
}

// 第二遍BFS,从起点开始
// BFS中,要在某结点被加入进队列之后就把该结点置为已访问
// 如果在出队时将该结点置为已访问,就可能把已经在队列里的邻接点又加入队列,导致超时
void bfs2()
{
    queue<int> q;
    q.push(1);
    while(q.size())
    {
        int u=q.front();
        q.pop();
        if(d[u]==0) return;
        int len=c[u].size();
        int min_color=MAXN2;
         // 对于当前扩展到的每个结点(即出队结点)
        // 遍历其邻接点,且邻接点需比当前点离终点的距离小1,以确保它在最短路径上
        // 找出连接边权值的最小值
        for(int i=0;i<len;i++)
        {
            int v=g[u][i];
            if(d[v]==d[u]-1)
            {
                min_color=min(min_color,c[u][i]);
            }
        }

        // t即为保存最短边权值序列的数组的索引
        int t=d[1]-d[u];
        ans[t]=min(ans[t],min_color); //更新

         // 将当前点所有满足三个条件的邻接点加入队列
        // 条件为:未被访问过,离终点距离小1,连接边权值等于上面找到的最小值
        for(int i=0;i<len;i++)
        {
            int v=g[u][i];
            if(vis[v]==false && d[v]==d[u]-1 && c[u][i]==min_color) //这个也是细节
            {
                q.push(v);
                vis[v]=true;
            }
        }
    }
    return ;
}
int main()
{
    int n,m;
    while(scanf("%d%d",&n,&m)!=EOF)
    {
        for(int i=0;i<MAXN;i++)
        {
            g[i].clear();
            c[i].clear();
        }
        for(int i=1;i<=m;i++)
        {
            int a,b,c1;
            scanf("%d%d%d",&a,&b,&c1);
            g[a].push_back(b);
            g[b].push_back(a);
            c[a].push_back(c1);   //从a出发颜色可以是c1,从b出发的颜色也可以是c1
            c[b].push_back(c1);
        }
        for(int i=0;i<=n;i++)
        {
            d[i]=MAXN;
            ans[i]=MAXN2;
            vis[i]=false;
        }
        d[n]=0;
        bfs(n);
        bfs2();
        printf("%d\n", d[1]);
        printf("%d",ans[0]);
        for(int i=1;i<d[1];i++)
        {
            printf(" %d",ans[i]);
        }
        printf("\n");
    }
    return 0;
}
506 - System Dependencies

题目链接:506 - System Dependencies
参考博文:System Dependencies

  • 题目大意:详见原题。
  • 思路:一个细节比较多的模拟题(拓扑思想)。主要考虑到有显式安装和隐式安装,还需要维护依赖关系和安装组件的数组,细节详见博文和代码,特点是将组件名称转换为ID数字,方便操作。

代码:

#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;

const int maxn = 10000010;
//depend[x]和be_depended[x]分别表示组件x所依赖的组件列表和依赖于x的组件列表
vector<int> depend[maxn], be_depended[maxn];
vector<int> installed;
map<string, int> item;
string name[maxn];
int status[maxn];//0表示组件x未安装,1表示显示安装,2表示隐式安装
int cnt;

//首先,维护一个组建的名字列表——可以把输入中的组件名全部转化为整数编号。
//此处可以使用一个map记录出现过的组件名及其编号,使用字符串数组name方便记录编号对应的组件名。
int ID(string It)
{
    if(item.count(It)) return item[It];
    item[It] = ++cnt;
    name[cnt] = It;//根据编号找对应组件的名字
    return cnt;
}

void install(int it, bool toplevel)//toplevel区分显、隐式安装
{
    if(!status[it])//it还没有安装
    {
        for(int i = 0; i < depend[it].size(); i++)
        {
            install(depend[it][i], false); //隐式安装
        }
        cout << "   Installing " << name[it] << endl;
        //赋值显式安装和隐式安装
        status[it] = toplevel ? 1 : 2;
        installed.push_back(it);
    }
}

bool needed(int it)
{
    for(int i = 0; i < be_depended[it].size(); i++)
    {
        if(status[be_depended[it][i]]) return true; //组件被其他组件依赖,且该组件还在
    }
    return false;
}

void Remove(int it, bool toplevel)
{
    if((toplevel || status[it] == 2) && !needed(it))//(显式指定删除||隐式安装)&&不被依赖
    {
        status[it] = 0;//卸载
        installed.erase(find(installed.begin(), installed.end(), it));//从已安装名单中移除
        cout << "   Removing " << name[it] << endl;
        for(int i = 0; i < depend[it].size(); i++)//卸载其依赖包
        {
            Remove(depend[it][i], false);
        }
    }
}

int main()
{
    cnt = 0;
    string com, item1, other;
    getline(cin, com);
    while(1)
    {
        cout << com << endl;
        istringstream line(com);
        line >> com;
        if(com == "END") break;
        if(com == "DEPEND")
        {
            line >> item1;
            int id1 = ID(item1), id2;
            while(line >> other)
            {
                id2 = ID(other);
                depend[id1].push_back(id2);//id1依赖id2
                be_depended[id2].push_back(id1);//id2被id1依赖
            }
        }
        else if(com == "INSTALL")
        {
            line >> item1;
            int id = ID(item1);
            if(status[id])//组件已经安装
                cout << "   " << name[id] << " is already installed." << endl;
            else
                install(id, true); //显式安装
        }
        else if(com == "REMOVE")
        {
            line >> item1;
            int id = ID(item1);
            if(!status[id])//没有安装
                cout << "   " << name[id] << " is not installed." << endl;
            else if(needed(ID(item1)))//存在其他包依赖它
                cout << "   " << name[id] << " is still needed." << endl;
            else
                Remove(id, true);  //显式卸载
        }
        else if(com == "LIST")
        {
            for(int i = 0; i < installed.size(); i++)
                cout << "   " << name[installed[i]] << endl;
        }
        getline(cin, com);
    }
    return 0;
}
11853 - Paintball

题目链接:11853 - Paintball
参考博文:UVA 11853 Paintball

  • 题目大意:在1000*1000的区域内,一堆人玩水弹(也许是吧),知道他们的坐标和攻击范围,问你从西边到东边能否不被水弹砸。找出西边进入的最北位置和东边出去的最北位置。
  • 思路:这道题的思路和巧妙,分为两个部分。第一个部分是判断能否安全出入,第二个部分是计算出进入和出去的位置。对于第一个部分可以这样想,把整个方形区域看作水面,把禁区看作小岛,如果可以从上边沿着这些小岛走到下边的话,就说明方形区域被禁区整个隔断了,那么就不能从左边走到右边,这样一来DFS即可。对于第二个部分,求东西最北的出入位置,只需要在DFS的时候计算当前禁区与左右边缘的下交点,不断更新坐标值即可。
    算法竞赛入门经典(第2版)—第六章(数据结构基础)_第6张图片
    代码:
#include 
#include 
#include 
#include 
#include 
using namespace std;

struct Node
{
    int x, y, r;
    Node(int x=-1, int y=-1, int r=-1):x(x), y(y), r(r){}
    bool operator < (const Node &A) const
    {
        if(y==A.y)
            return x<A.x;
        else
            return y>A.y;
    }
};
set<Node> Set;
int vis[1005][1005];
double Wn, En;

//判断连个区域是否相连
bool check2(Node u, Node v)
{
    double dis = sqrt((u.x-v.x)*(u.x-v.x)+(u.y-v.y)*(u.y-v.y));
    double dis2 = (double)(u.r+v.r);
    return dis <= dis2;
}
bool dfs(Node u)
{
    vis[u.x][u.y] = 1;
    if(u.x-u.r<=0)//更新西边进入的位置
    {
        double d = (double)u.y-sqrt(u.r*u.r-u.x*u.x);
        Wn = min(Wn, d);
    }
    if(u.x+u.r>=1000)//更新东边出的位置
    {
        double d = (double)u.y-sqrt(u.r*u.r-(1000-u.x)*(1000-u.x));
        En = min(En, d);
    }
    if(u.y-u.r<=0) return 1;//到达了最下面
    set<Node>:: iterator it;
    for(it=Set.begin(); it!=Set.end(); it++)//寻找相交的区域,递归遍历
    {
        int x = (*it).x, y = (*it).y, r = (*it).r;
        if(!vis[x][y] && check2(u, *it))
        {
            if(dfs(*it)) return 1;
        }
    }
    return 0;
}
int main()
{
    int T, x, y, r;
    while(cin >> T)
    {
        Set.clear();
        while(T--)
        {
            cin >> x >> y >> r;
            Set.insert(Node(x, y, r));
        }
        memset(vis, 0, sizeof(vis));
        int flag = 1;
        Wn = 1000, En = 1000;//西面最北的进入位置,东面最北的进入位置
        set<Node>::iterator it;
        for(it=Set.begin(); it!=Set.end(); it++)
        {
            int x = (*it).x, y = (*it).y, r = (*it).r;
            if(y+r>=1000 && !vis[x][y])//选择包含最上界的未遍历的区域作为出发点
            {
                if(dfs(*it))
                {
                    flag = 0;
                }
            }
        }
        if(!flag) printf("IMPOSSIBLE\n");
        else      printf("%.2f %.2f %.2f %.2f\n", 0.0, Wn, 1000.0, En);
    }
    return 0;
}
673 - Parentheses Balance

题目链接:673 - Parentheses Balance

  • 题目大意:输入一行包含()和[]的字符串,检查每对括号是否匹配得上,能就Yes否则No。同时空字符串也看成是符合情况的。
  • 思路:简单题栈题目,但是输入需要使用getline,因为可能输入空字符串,cin无法读入。题目中没有说是否考虑空格但是后面的测试样例中有空格,并且还把含空格的样例判断成了正确的,但是我测试后发现无论将含空格的样例判断成正确还是错误,代码均通过。因此说明后台数据没有含空格的数据,但是根据题目意思,确实应该将含空格的样例判断为错误。

代码:

#include 
#include 
#include 
#include 
using namespace std;

int main()
{
    stack<char> stc;
    string s;
    int T;
    cin >> T;
    getchar();
    while(T--)
    {
        getline(cin, s);//因为有空格和空字符串,所以按行输入
        int len = s.size();
        if(len==0)//空字符串
        {
            cout << "Yes" << endl;
            continue;
        }
        int flag = 1;
        //按栈来找对应的括号
        for(int i=0; i<len; i++)
        {
            if(s[i]=='(' || s[i]=='[')
                stc.push(s[i]);
            else if(s[i]==')')
            {
                if(stc.size()==0 || stc.top()!='(')
                {
                    flag = 0;
                    break;
                }
                stc.pop();
            }
            else if(s[i]==']')
            {
                if(stc.size()==0 || stc.top()!='[')
                {
                    flag = 0;
                    break;
                }
                stc.pop();
            }
            else//其余的输入字符,均为错误
            {
                flag = 0;
                break;
            }
        }
        if(flag && stc.empty())//正常完成且括号完全匹配
            cout << "Yes" << endl;
        else
            cout << "No" << endl;
        while(!stc.empty()) stc.pop();//清空栈
    }
    return 0;
}
712 - S-Trees

题目链接:712 - S-Trees

  • 题目大意:算法竞赛入门经典(第2版)—第六章(数据结构基础)_第7张图片
  • 思路:一个简单的关于树的技巧题。需要观察规律,发现其和二进制完全等价,级该二进制(问题字符串)代表的值是叶节点字符串的下标,而且该二进制需要与 x i x_i xi对应,即需要根据 x i x_i xi的顺序重新排列二进制字符串得到新的串,该串对应的二进制的值就是输入的叶节点字符串的下标。

代码:

#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;

int n, m;
string f, s, ans, vva;
vector<int> v;//存储每层的xi顺序
int main()
{
    int kase = 1;
    while(cin >> n && n)
    {
        v.clear();
        ans = "";
        for(int i=0; i<n; i++)
        {
            cin >> f;
            int l = 0;
            for(int j=1; j<f.size(); j++)
                l = l*10+f[j]-'1';
            v.push_back(l);//将xi转换为下标存储,与问题输入的下标对应起来
        }
        cin >> s;
        cin >> m;
        for(int i=0; i<m; i++)
        {
            cin >> vva;
            int a = vva.size()-1;
            int len = pow(2, a), id = 0;//len是当前子树的叶节点数量的一半
            for(int j=0; j<v.size(); j++)//与二进制等效
            {
                if(vva[v[j]]=='1')//当为1时,加一半子树叶子节点
                    id += len;
                else//当为0时,下标不变
                    id += 0;
                len /= 2;//子树变小,叶节点数目减半
            }
            ans += s[id];
        }
        printf("S-Tree #%d:\n", kase++);
        printf("%s\n\n", ans.c_str());
    }
    return 0;
}
536 - Tree Recovery

题目链接:536 - Tree Recovery
参考博文:acm之旅–树

  • 题目大意:给出树的前序和中序,求其后序。
  • 思路:可以根据前序找子树的根节点,然后在中序中划分左右子树,然后就可以根据关系重建树,或者直接得到后序关系。

代码:

#include 
#include 
#include 
#include 
using namespace std;

string in, pre, post;
int pos;

//根据前序和中序得出后序的函数
void rec(int l, int r)
{
    if(l>=r) return;
    char v = pre[pos++];
    int m = in.find(v);
    rec(l, m);
    rec(m+1, r);
    post += v;//后序
}
int main()
{
    while(cin >> pre)
    {
        cin >> in;
        post = "", pos = 0;
        rec(0, pre.size());
        printf("%s\n", post.c_str());
    }
    return 0;
}
439 - Knight Moves

题目链接:439 - Knight Moves

  • 题目大意:一个8*8的国际象棋,马从一个位置调到另一个位置,求其最短步数。
  • 思路:裸的bfs求最短路径长度。

代码:

#include 
#include 
#include 
#include 
using namespace std;

char st[5], ed[5];
struct Node
{
    int r, c, step;//step记录从起始点到当前位置的步数
    Node(int r=-1, int c=-1, int step=-1):r(r), c(c), step(step){}
};
Node s, e;
int vis[15][15];
int dx[10] = {1, -1, 2, -2, 1, -1, 2, -2};
int dy[10] = {2, 2, 1, 1, -2, -2, -1, -1};

//bfs模板
int bfs(Node u)
{
    memset(vis, 0, sizeof(vis));
    queue<Node> q;
    q.push(u);
    vis[u.r][u.c] = 1;
    while(!q.empty())
    {
        Node v = q.front(); q.pop();
        if(v.r==e.r && v.c==e.c)
        {
            return v.step;
        }
        for(int i=0; i<8; i++)
        {
            int r = v.r+dx[i], c = v.c+dy[i];
            if(r>=0 && r<8 && c>=0 && c<8 && !vis[r][c])
            {
                vis[r][c] = 1;
                q.push(Node(r, c, v.step+1));
            }
        }
    }
}

int main()
{
    while(scanf("%s%s", st, ed)!=EOF)
    {
        s = Node(st[1]-'1', st[0]-'a', 0);//起始点
        e = Node(ed[1]-'1', ed[0]-'a');//终止点
        int steps = bfs(s);
        printf("To get from %s to %s takes %d knight moves.\n", st, ed, steps);
    }
    return 0;
}
1600 - Patrol Robot

题目链接:1600 - Patrol Robot
参考博文:uva1600 Patrol Robot(不同的BFS最短路)
UVA1600-Patrol Robot(BFS进阶)

  • 题目大意:在bfs基础上可以最多连续穿越k个障碍,求最短路径长度。

方法一:

  • 思路:这道题看似简单,让我使劲浑身解数也没有做出来。重点在于,如何处理连续穿越k个障碍,有可能物体从两条路到达一个位置,虽然有一条路长度较长,但是可以穿越更多障碍,而另一条虽然先到达,但是因为可以穿越的障碍少,使得无法到达目的地。一种处理方法是:增加一个数组用来存储当前结点还能跨过几个障碍。可以知道,在到达同一个位置时,在步数比之前步数要大的情况下,如果还可以跨越的障碍物没有之前的要多,那么就一点优势就没有了,就可以把这种情况减枝掉。因此每到一个结点,判断当前走法所剩的k值,如果小于当前结点的值(即之前走法有更大的k),就舍弃。

代码:

#include 
#include 
#include 
#include 
#include 
using namespace std;

int m, n, k;
int leave[25][25];//记录当前到达位置还可以越过最大的障碍数
int vis[25][25];//记录地图

struct Node
{
    int r, c, step, leave;//step记录从起始点到当前位置的步数,leave记录还可以越过多少个障碍
    Node(int r=-1, int c=-1, int step=-1, int leave=-1):r(r), c(c), step(step), leave(leave){}
};
//右、左、下、上
int dr[5] = {0, 0, 1, -1};
int dc[5] = {1, -1, 0, 0};

int bfs(Node u)
{
    memset(leave, -1, sizeof(leave));
    queue<Node> q;
    q.push(u);
    vis[u.r][u.c] = -1;
    while(!q.empty())
    {
        Node v = q.front(); q.pop();
        if(v.r==m && v.c==n) return v.step;
        for(int i=0; i<4; i++)
        {
            int r = v.r+dr[i], c = v.c+dc[i];
            if(r<1 || r>m || c<1 || c>n) continue;//出界
            if(leave[r][c]>=v.leave) continue;//可以跨越的障碍物越多越好
            else                     leave[r][c] = v.leave;
            if(vis[r][c]==0)//0值
                q.push(Node(r, c, v.step+1, k));
            else if(v.leave)//1值并且还可以跨越障碍
                q.push(Node(r, c, v.step+1, v.leave-1));
        }
    }
    return -1;
}

int main()
{
    int T;
    scanf("%d", &T);
    while(T--)
    {
        scanf("%d%d", &m, &n);
        scanf("%d", &k);
        for(int i=1; i<=m; i++)
        {
            for(int j=1; j<=n; j++)
            {
                cin >> vis[i][j];
            }
        }
        int steps = bfs(Node(1, 1, 0, k));
        printf("%d\n", steps);
    }
    return 0;
}

方法二:

  • 思路:这种解决方案是将标记vis数组增加一维,表示穿越了几个障碍物到达这里,这样再去判重减枝。这种方法仅减去了在穿越同样障碍物是到达同一个位置步数比较大的情况,没有减去在穿越更多的障碍物得到同一个位置步数比较大的情况。因此,我的改进方法是如果当前是第一个到达该位置的,那么就将经过更多(包括与当前情况一样的)障碍物的vis数组全部赋值为1,即以后只能在穿越更少的障碍物的情况下访问该位置(因为走的步数更多)

代码:

#include 
#include 
#include 
#include 
#include 
using namespace std;

struct Point{
    int x,y,time;
    int layer;//连续走过的障碍数目
    Point(int x = 0,int y = 0,int time = 0,int layer = 0) :
        x(x),y(y),time(time),layer(layer) {}
};

const int maxn = 22;
int gra[maxn][maxn];
int vis[maxn][maxn][maxn];
int n,m,k;
int dx[4] = {1,-1,0,0};
int dy[4] = {0,0,-1,1};

int bfs(){
    queue<Point> que;
    que.push(Point(1,1,0,0));
    memset(vis,0,sizeof(vis));
    vis[1][1][0] = 1;
    while(!que.empty()){
        Point first = que.front();que.pop();
        if(first.x==n && first.y==m) return first.time;//到达目的地
        int xx,yy;
        for(int i = 0;i < 4;i++){
            xx = first.x+dx[i],yy = first.y+dy[i];
            if(1<=xx && 1<=yy && xx<=n && yy<=m){//在界内
                int layer = first.layer;
                if(gra[xx][yy]) layer++;//当前位置时障碍
                else layer = 0;//当前位置不是障碍
                if(layer<=k && !vis[xx][yy][layer]){//在连续穿越了layer个障碍到达xx,yy位置
                	//vis[xx][yy][layer] = 1;//原代码
                	//改进代码
                    for(int j=layer; j<=k; j++)
                        vis[xx][yy][j] = 1;
                    que.push(Point(xx,yy,first.time+1,layer));
                }
            }
        }
    }
    return -1;
}

int main()
{
    //freopen("input.txt","r",stdin);
    int iCase;
    scanf("%d",&iCase);
    while(iCase--){
        scanf("%d%d",&n,&m);
        scanf("%d",&k);
        for(int i = 1;i <= n;i++){
            for(int j = 1;j <= m;j++){
                scanf("%d",&gra[i][j]);
            }
        }
        printf("%d\n",bfs());
    }
    return 0;
}
12166 - Equilibrium Mobile

题目链接:12166 - Equilibrium Mobile

  • 题目大意:题目大意:给出一棵二叉树,整个树是天平,每个结点有一个砝码或一个天平,对于任意一个天平,绳子都在中点,每个砝码都有重量,求最少修改多少个砝码的重量使得整个天平平衡。
  • 思路:本题的难点一方面在与一个想法和题目数据范围。由于要求修改最少的砝码,所以必定会以一个砝码作为参照,或者说该砝码质量不变。即先求出砝码质量的众数,但是要考虑不同深度的砝码,所以需要将因为深度为1的砝码质量为4和深度为2的砝码质量为2本质上是一种情况,所以需要将砝码质量除以pow(2,(树的最大深度-该砝码的深度)),然后求砝码质量的众数,最后砝码总数量减去众数的个数即可。

代码:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;

string s;
stack<char> stc;//记录当前结点的深度
set<int> Set;//记录叶子数值,是m的键
map<int, int> m;//记录叶子数值对应的个数
void solve(int len)
{
    Set.clear();
    m.clear();
    while(!stc.empty()) stc.pop();
    int sum = 0, layer = 0;
    //求出树的最大深度layer
    for(int i=0; i<len; i++)
    {
        if(s[i]=='[')
            stc.push(s[i]);
        else if(s[i]==']')
            stc.pop();
        if(stc.size()>layer) layer = stc.size();
    }
    while(!stc.empty()) stc.pop();
    //记录所有砝码质量的个数
    for(int i=0; i<len; i++)
    {
        if(s[i]=='[')
            stc.push(s[i]);
        else if(s[i]==']')
            stc.pop();
        else if(isdigit(s[i]))
        {
            sum++;//记录有多少个数字结点
            int n = s[i]-'0';
            int t = i+1;
            while(t<len && isdigit(s[t]))
                n = n*10+s[t++]-'0';
            i = t-1;
            int d = stc.size();
            n = n/pow(2, layer-d);//该数字结点对应的叶子结点的数值
            if(Set.count(n)) m[n] = m[n]+1;
            else             m[n] = 1, Set.insert(n);
        }
    }
    int Max = 0;
    //求最多的叶子数值个数
    set<int>::iterator it;
    for(it=Set.begin(); it!=Set.end(); it++)
    {
        int t = *it;
        if(Max<m[t]) Max = m[t];
    }
    printf("%d\n", sum-Max);
}
int main()
{
#ifdef ONLINE_JUDGE
#else
    freopen("input.txt","r",stdin);
    freopen("output.txt","w",stdout);
#endif
    int T;
    cin >> T;
    while(T--)
    {
        cin >> s;
        solve(s.size());
    }
}
10410 - Tree Reconstruction

题目链接:10410 - Tree Reconstruction
参考博文:随缘&不屈UVA10410-Tree Reconstruction(BFS序和DFS序的性质)
UVA10410 TreeReconstruction 树重建 (dfs,bfs序的一些性质,以及用栈处理递归 )
uva 10410 - Tree Reconstruction(栈)

  • 题目大意:给出一颗树的BFS和DFS遍历序列,求每一个结点的子结点。
  • 思路:一道好题,并且还是比较难。经过思考,首先需要明确:
    • 1.在BFS序中,与点a相邻的下一个点可能有三种身份:(1)节点a的孩子 (2)节点a的第一后兄弟 (3)节点a的兄弟的孩子
    • 2.在DFS序中,与点a相邻的下一个点可能有三种身份:(1)节点a的孩子(2)节点a的第一后兄弟(3)啥也不是(意思是说直接回到父辈及以上了)
    • 当满足DFS序列中相邻(即dfs(v)=dfs(u)+1),如果v和u在BFS中不相邻,且bfs(v)>bfs(u)+1,则表明v是u的子节点,如果v和u在BFS中相邻,且bfs(v)=bfs(u)+1,v
    • 当在DFS序列中不相邻(即dfs(v)>dfs(u)+1),则v可能是u的子节点(u有多个子节点),或u的兄弟节点(u有多个兄弟节点),或什么也不是。则此时情况其实跟上一种一样,满足上面的条件,u一定是v的父节点,并且u在DFS中的位置一定比v靠前。
    • 还要注意一个细节就是栈顶元素时root时,直接加孩子,入栈。

代码:

#include 
using namespace std;

const int MAX = 1005;
vector<int> G[MAX];
int pos[MAX];//

int main()
{
    int n, t;
    while(scanf("%d", &n)!=EOF)
    {
        int x;
        //BFS
        for(int i=1; i<=n; i++)
        {
            scanf("%d", &t);
            pos[t] = i;//记录bfs中每一个元素对应的位置
            G[i].clear();
        }
        int root;
        scanf("%d", &root);
        stack<int> stc;
        stc.push(root);
        for(int i=1; i<n; i++)
        {
            scanf("%d", &t);
            while(1)
            {
                int u = stc.top();
                if(pos[u]+1<pos[t] || (pos[u]+1 == pos[t] && u > t) || u==root)//子节点||根节点
                {
                    G[u].push_back(t);
                    stc.push(t);
                    break;
                }
                else stc.pop();
            }
        }
        for(int i=1; i<=n; i++)
        {
            printf("%d:", i);
            for(int j=0; j<G[i].size(); j++)
            {
                if(j) printf(" ");
                printf(" %d", G[i][j]);
            }
            printf("\n");
        }
    }
    return 0;
}
12118 - Inspector’s Dilemma

题目链接:12118 - Inspector’s Dilemma
参考博文:UVa 12118 - Inspector’s Dilemma

  • 题目大意: 一个有v个顶点的完全图,找一条经过m条指定边的最短路径。
  • 思路:欧拉回路的变形题。在每一个连通块中计算奇数端点个数x,然后(x-1)/2就是额外需要走的道路,再加上每个连通块之间要走的道路与必须要走的道路就是走的最少的道路。

代码:

#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;

const int MAX = 1005;
vector<int> G[MAX];//存选定的路径
set<int> Set;//存选定路径包括的点
//vis表示遍历数组,degree表示结点的度的数组,sum_deg表示每个连通块中需要额外走的路径个数之和,sum_num表示连通块个数
int vis[MAX], degree[MAX], sum_deg, sum_num;

//bfs统计连通图中的奇数断定个数
void bfs(int u)
{
    int cnt = 0;
    queue<int> q;
    q.push(u);
    vis[u] = 1;
    while(!q.empty())
    {
        int v = q.front(); q.pop();
        if(degree[v]%2) cnt++;
        for(int i=0; i<G[v].size(); i++)
        {
            if(!vis[G[v][i]])
            {
                vis[G[v][i]] = 1;
                q.push(G[v][i]);
            }
        }
    }
    if(cnt>0)
        sum_deg += (cnt-1)/2;
}
void Init()
{
    memset(vis, 0, sizeof(vis));
    memset(degree, 0, sizeof(degree));
    Set.clear();
}
int main()
{
#ifdef ONLINE_JUDGE
#else
    freopen("input.txt","r",stdin);
    freopen("output.txt","w",stdout);
#endif

    int V, E, T, a, b, kase = 1;
    while(scanf("%d%d%d", &V, &E, &T) && (V+E+T))
    {
        if(E==0)//注意数据范围
        {
            printf("Case %d: %d\n", kase++, 0);
            continue;
        }
        Init();
        for(int i=0; i<E; i++)
        {
            scanf("%d%d", &a, &b);
            G[a].push_back(b);
            G[b].push_back(a);
            Set.insert(a);
            Set.insert(b);
            degree[a]++;
            degree[b]++;
        }
        sum_deg = 0, sum_num = 0;
        set<int>::iterator it;
        for(it=Set.begin(); it!=Set.end(); it++)
        {
            if(!vis[*it]) bfs(*it), sum_num++;
        }
        printf("Case %d: %d\n", kase++, (sum_deg+E+sum_num-1)*T);
        for(it=Set.begin(); it!=Set.end(); it++)
        {
            G[*it].clear();
        }
    }
    return 0;
}

你可能感兴趣的:(算法竞赛入门经典(第2版),算法竞赛入门经典)