STL 例题

UVa 10474(排序+查找)

原题地址

https://vjudge.net/problem/UVA-10474

题意:现有N个大理石,每个大理石上写了一个非负整数,首先把各数从小到大排序,然后回答Q个查询问题,查询整数x是否存在,若存在给出位置。排序后的大理石从左到右编号为1-N

解题思路

本题是《算法竞赛入门经典》的例题5-1,是一道排序的水题。

先排序,再查找即可。查找可以直接遍历,或者二分查找,在这里采用的是STL中的sort函数和针对有序序列的lower_bound函数。

lower_bound返回一个非递减序列[first, last)中的第一个大于等于值val的位置
upper_bound返回一个非递减序列[first, last)中第一个大于val的位置。
它们的本质也是二分查找,如图所示(http://www.cnblogs.com/cobbliu/archive/2012/05/21/2512249.html)
STL 例题_第1张图片
(注意返回值:对于数组而言返回的是相对于数组首部偏移后的地址,对于vector返回的是该位置的迭代器)

因此,先调用sort对输入数组做升序排序,再调用lower_bound,如果返回值有效则输出。

AC代码

#include 
#include 
using namespace std;

const int MAXN = 10000;

int main()
{
    int N, Q, a[MAXN], x;
    int kase = 0;
    while (cin >> N >> Q && N)
    {
        cout << "CASE# " << ++kase << ":\n";
        for (int i = 0; icin >> a[i];
        sort(a, a+N); //升序
        while (Q--)
        {
            cin >> x;
            int p = lower_bound(a, a+N, x) - a; //在非递减序列[First,Last)中找到第一个大于等于x的位置,注意用法
            if (a[p] == x) printf("%d found at %d\n", x, p+1); //找到了x的位置
            else printf("%d not found\n", x);
        }
    }
    return 0;
}

UVa 101 The Blocks Problem(模拟)

原题地址

https://vjudge.net/problem/UVA-101

题意:从左到右有n个木块,编号为0~n-1,要求模拟以下4种操作(下面的a和b均为编号)
move a onto b:把a和b上方的木块全部归到原位,然后把a放在b上面。
move a over b:把a上面的木块全部归位,然后把a放在b所在堆的顶部。
pile a onto b:把b上面的木块全部归位,然后把a及上面的木块整体堆在b的上面。
pile a over b:把a及上面的木块整体放在b所在堆的顶部。
遇到quit结束操作并输出堆的现状,a和b在同一堆时是非法指令,不操作。

解题思路

本题是《算法竞赛入门经典》的例题5-2,是一道vector+模拟的水题。

由于每个堆的高度不固定具有可变性,因此用vector数组(每个元素是一个vector)来表示每个堆是很合理的。对于vector,主要就是push_back,pop_back,size(访问大小),resize(调整大小)等函数的调用。

先找到给定数字a和b所在的堆及其高度,根据指令的含义分别操作。

这里比较巧妙的是,刘汝佳为了避免冗余的代码,把4种if判断和操作合并为3句话,统一用pile_upon函数实现移动,原因是对于a的堆,单纯move a等价于先把a之上的木块归位,再pile_upon一个a即可,否则移动多个;另一边,如果有onto要把b上面的归位,否则都是放在b堆的顶部。

AC代码

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

const int maxn = 30;
vector<int> pile[maxn]; //最多有maxn个堆,每个堆都是一个vector
int n;

void find_block(int num, int &p, int &h) //找到编号为num的堆p及其高度h,修改引用
{
    for (int i = 0; i < n; ++i)
        for (int index = 0; index < pile[i].size(); ++index)
        {
            if (pile[i][index] == num)
            {
                p = i; h = index;
                return;
            }
        }
}

void clear_above(int p, int h) //把第p堆在高度h之上的木块移回原位
{
    for (int i = h+1; i < pile[p].size(); ++i)
    {
        int x = pile[p][i];
        pile[x].push_back(x); //归位
    }
    pile[p].resize(h+1); //只保留下标0~h的元素
}

void pile_upon(int p1, int h1, int p2) //把p1堆中高度h1及其之上的木块整体移动到p2顶部
{
    for (int i = h1; i < pile[p1].size(); ++i) 
        pile[p2].push_back(pile[p1][i]);
    pile[p1].resize(h1); //只保留下标0~h-1的元素
}


void print_status() //打印输出
{
    for (int i = 0; iprintf("%d:", i);
        for (int j = 0; j < pile[i].size(); ++j)
            printf(" %d", pile[i][j]);
        printf("\n");
    }
}

int main()
{
    cin >> n;
    string s1,s2;
    int a, b;
    for (int i = 0; i//堆初始化
    while(cin >> s1 >> a >> s2 >> b)
    {

        int pa, pb, ha, hb;
        find_block(a, pa, ha); //找到数字a
        find_block(b, pb, hb); //找到数字b
        if (pa == pb) continue; //非法指令

        //合并后的3句话操作
        if (s1 == "move") //要move a一定会归位a上面的木块
            clear_above(pa, ha);
        if (s2 == "onto") //要onto b一定会归位b上面的木块
            clear_above(pb, hb); 
        pile_upon(pa, ha, pb); //把a及其之上的木块移到b堆上

        /*合并前
        if (s1 == "move" && s2 == "onto")
        {
            clear_above(pa, ha);
            clear_above(pb, hb);
            pile[pb].push_back(a);
            pile[pa].pop_back();
        }
        else if (s1 == "move" && s2 == "over")
        {
            clear_above(pa, ha);
            pile[pb].push_back(a);
            pile[pa].pop_back();
        }
        else if (s1 == "pile" && s2 == "onto")
        {
            clear_above(pb, hb);
            pile_upon(pa, ha, pb);
        }
        else if (s1 == "pile" && s2 == "over")
            pile_upon(pa, ha, pb);
        */
        //print_status();//观察每一次移动
    }
    print_status();
    return 0;
}

UVa 10815 Andy’s First Dictionary(set集合操作)

原题地址

https://vjudge.net/problem/UVA-10815

输入一个文本,找出所有不同的单词(不区分大小写),按字典序从小到大输出。

解题思路

本题是《算法竞赛入门经典》的例题5-3,不需要算法和技巧,主要是熟悉STL中集合对象set的操作。

由于要求每个单词只出现一次,而set容器内部由红黑树实现,插入删除查找的效率都非常高,而且是自动排序、去重,所以用set存放单词是非常合理的。

注意输入时将所有非字母的字符变为空格,用字符串流stringstream得到各个单词。

AC代码

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

set<string> dict; //string集合

int main()
{
    string s, buf;
    while(cin >> s)
    {
        for (int i = 0; iif (isalpha(s[i])) //统一为小写
                s[i] = tolower(s[i]);
            else s[i] = ' ';
        stringstream ss(s); //构造字符串流
        while (ss >> buf)
            dict.insert(buf); //新word加入词典
    }
    for (auto it = dict.begin(); it != dict.end(); ++it) //遍历输出
        cout << *it << endl;
    return 0;
}

UVa 156 Ananagrams (排序+map计数 )

原题地址

https://vjudge.net/problem/UVA-156

题意:给定一个文本,找出不能在这个文本里找到重组词的所有单词(重新对字符排序组合,比如NOTE的重组词可以是TONE,但是QUIZ没有重组词)

解题思路

本题是《算法竞赛入门经典》的例题5-4,是一道排序的基础题,简单复习一下map。

思路和之前的POJ 2159 Ancient Cipher(字符串排列替换)有些类似。

由于重组词的组合方式有很多种,没必要一个个列举出来(暴力列举也可以,STL里的函数next_permutation提供排列组合),只需要对文本里每个字符串排序并建立map计数,如果一个字符串排序后的出现次数至少2次,说明这个字符串存在重组词。

需要注意每次插入map的是小写化并排序后的单词,输出是最后无重组词的源字符串要按字典序输出。

AC代码

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

map<string, int> dict; //存放单词出现的次数,int默认为0
vector<string> input; //存放输入的源字符串

string trans(const string &s) //单词标准化
{
    string ret = s;
    for (int i = 0; i//小写化
        ret[i] = tolower(ret[i]);
    sort(ret.begin(), ret.end()); //string内部排序
    return ret;
}

int main()
{
    string s;
    while (cin >> s)
    {
        if (s == "#") break;
        input.push_back(s);
        dict[trans(s)]++; //标准化后的单词出现次数加一
    }
    vector<string> res;
    for (auto it = input.begin(); it != input.end(); ++it) //遍历输入字符串
    {
        if (dict[trans(*it)] == 1) //如果只出现一次,说明只有它本身,没有重组词
            res.push_back(*it);
    }
    sort(res.begin(), res.end()); //升序输出
    for (auto it = res.begin(); it != res.end(); ++it)
        cout << *it << endl;
    return 0;
}

UVa 12096 The SetStack Computer(双射+stack+map+vector综合应用)

原题地址

https://vjudge.net/problem/UVA-12096

题意:设计一个专门为集合运算使用的“集合栈”,栈中的每个元素都是栈,所有操作都对栈顶集合进行,支持以下操作:
PUSH:将空集 {} 入栈
DUP:将栈顶元素复制一份后入栈
UNION:出栈两个集合,将两者的并集入栈
INTERSECT:出栈两个集合,将两者的交集入栈
ADD:出栈两个集合,将先出栈的集合加入到后出栈的集合中,并把新集合入栈
每次操作后输出栈顶集合的基数大小(即元素个数),不考虑空栈、越界。

解题思路

本题是《算法竞赛入门经典》的例题5-5,是一道容器的综合应用题。

(由于本题的操作对象嵌套,自己实现起来有点难,下面都是对书上内容的总结,注意下面的“大”、“小”二字是相对的)

本题的集合并不是简单的整数集合或者字符串集合,而是小集合的大集合,为了方便起见,为每个不同的集合(大或小)分配一个唯一的ID,每个大集合都表示为它所包含小集合对应ID的大集合。所以用STL的set表示这个大集合(是不是很迷乱- -),整个栈就是一个stack(每个大集合也有个ID啊)。

建立双射关系:对任意集合x,map对象IDcache[x]就是x的ID,vector对象Setcache[IDcache[x]]就是集合x本身。

所有操作都对栈顶的大集合进行,push、dup、加操作需要用到ID,交、并操作需要用到集合本身。

需要注意的是

  • STL中set对象自带函数求并集(set_union),求交集(set_intersect),它们的前四个参数是两个集合的首尾迭代器,最后一个参数为插入到新集合的指定位置(有点复杂,还是看代码容易懂一些)。
  • 输出的不是栈的大小,而是栈顶集合(大集合)所含的小集合个数。

AC代码

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

#define ALL(x) x.begin(),x.end()
#define INS(x) inserter(x, x.begin())

typedef set<int> Set;
//map和vector形成双射
mapint> IDcache; //把集合映射为ID
vector Setcache; //下标ID对应一个集合

stack<int> s; //题目中要求的栈,每个元素为集合对应的ID

int getID(Set x) //返回集合x对应的ID
{
    if (IDcache.count(x) != 0) //已经分配了ID
        return IDcache[x];
    //插入新的集合
    Setcache.push_back(x);
    return IDcache[x] = Setcache.size()-1; //返回该集合对应的下标作为ID
}

int main()
{
    int kase;
    cin >> kase;
    while(kase--)
    {
        int n;
        cin >> n;
        for (int i = 0; istring op;
            cin >> op;
            if (op[0] == 'P') s.push( getID( Set() ) ); //插入空集
            else if (op[0] == 'D') s.push(s.top()); //复制栈顶集合
            else
            {
                //找到栈顶ID对应的两个集合
                Set x1 = Setcache[s.top()]; s.pop();
                Set x2 = Setcache[s.top()]; s.pop();
                Set tmp; //存放操作结果的集合,和宏定义INS配合
                if (op[0] == 'U')  //并集
                    set_union(ALL(x1), ALL(x2), INS(tmp)); //前四个参数为两个集合的首尾迭代器,最后一个参数为插入到新集合的指定位置
                else if (op[0] == 'I') //交集
                    set_intersection(ALL(x1), ALL(x2), INS(tmp));
                else if (op[0] == 'A') //把第一个集合作为ID插到第二个集合
                {
                    tmp = x2;
                    tmp.insert(getID(x1));
                }
                s.push(getID(tmp)); //新集合入栈
            }
            cout << Setcache[s.top()].size() << endl; //栈顶ID对应集合的基数
        }
        cout << "***" << endl;
    }
    return 0;
}


UVa 540 Team Queue(哈希+queue+map操作)

原题地址

https://vjudge.net/problem/UVA-540

题意:有team_cnt个团队的人要排队,每次新来一个人的时候,如果他的队友在排队,他就排到这个团队的后面,否则他排到整个队伍的队尾。输入团队数、每个团队中所有队员的编号,要求支持ENQUEUE, DEQUEUE, STOP三种操作,对于每个DEQUEUE指令,输出出队的人的编号。

解题思路

本题是《算法竞赛入门经典》的例题5-5,是一道哈希思想和queue+map的综合应用题。

最直观的想法就是建立队列的队列,即queue> q,每个正在q里排队的都是一个队列,然后每次入队时遍历q,如果q[i]的第一个人是队友就查到i的后面。

想法看起来很对,但是至少存在两个问题:

  • 队列不可使用迭代器遍历
  • 题目里说最多可能有二十万次操作,所以必须保证每次入队出队都是O(1)的复杂度,即不能有遍历!!

因此书上提出了一种利用哈希访问正在排队的团队的方法,即队列q本身不记录每个团队的排队情况,而是将团队代表的ID值入队或出队,由此访问团队为ID的排队情况q2[ID]。

map将队员映射到其所属团队编号,入出队的逻辑如下:

  • 当num入队时,先判断q2[ TeamID[num] ]是否为空,若为空说明队列q里无这个团队的身影,更新q2[ TeamID[num] ]并将TeamID[num]插入q的队尾;若不为空则直接插入q2[ TeamID[num] ]尾部即可
  • 当出队时,从q队首的团队q.front()里出一个人,如果q.front变为empty,就把这个团队的ID从q移除。

需要注意的是,由于每个case中map和q、q2都要重新初始化,所以不能放在全局。

AC代码

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

const int maxn = 1005;

int main()
{
    int team_cnt, x, cnt, kase = 0;
    while (cin >> team_cnt  && team_cnt)
    {
        printf("Scenario #%d\n", ++kase);

        //为每个人分配对应的团队ID
        map<int,int> TeamID;
        for (int i = 0; i < team_cnt; ++i)
        {
            cin >> cnt;
            while(cnt--)
            {
                cin >> x;
                TeamID[x] = i; //存放x所属的团队ID
            }
        }

        //q是团队编号的队列(横向),q2[i]是团队i(如果)排队的人员队列(纵向)
        queue<int> q, q2[maxn]; //不能放在全局

        //执行操作
        string op;
        while (cin >> op && op != "STOP")
        {
            int num;
            if (op[0] == 'E') //入队
            {
                cin >> num;
                int t = TeamID[num];
                if (q2[t].empty()) //如果团队t没人在排队
                    q.push(t); //团队t排进队列
                q2[t].push(num); //团队t加上这个人
            }
            else if (op[0] == 'D') //出队
            {
                int t = q.front();
                //队列首部的团队出一个人
                cout << q2[t].front() << endl;
                q2[t].pop();
                //队列首部的团队没人了,去除这个团队
                if (q2[t].empty()) q.pop();
            }
        }
        cout << endl;
    }
    return 0;
}

UVa 136 Ugly Numbers(素数+set+priority_queue)

原题地址

https://vjudge.net/problem/UVA-136

丑数是指不能被2,3,5以外的素数整除的数,即它的质因数只能是2,3,5,把所有丑数从小到大排序,求第1500个丑数。
如1,2,3,4,5,6,8,9,10,12,15…

解题思路

这道题是《算法竞赛入门经典》的例题5-7,看上去有关数论,但是其实没有那么复杂,也不用打素数表来遍历查表之类的。

如果不从遍历每个数字判断的角度,而是从产生规则的角度看,最小的丑数是1,而对于任何丑数x,2x、3x、5x也都是丑数,这样就可以用一个优先队列保存所有已经产生的丑数,每次取出最小的丑数衍生出3个丑数,加入优先队列(优先队列默认最大堆,因此要用greater参数声明最小堆优先队列

由于一个丑数可以有多种产生方式(比如12,15),所以用集合set表示一个数是否被产生过,只有没被产生过时才加入优先队列。

AC代码

#include 
#include 
#include 
#include 
#include 
using namespace std;
typedef long long LL;
const int base[] = {2,3,5};

int main()
{
    priority_queuevector, greater > pq;
    set s; //集合标记某个数字是否出现过
    s.insert(1);
    pq.push(1);
    int cnt = 0;
    while (1)
    {
        LL x = pq.top(); pq.pop(); //取出队首的数
        cnt++; //弹出一个数字+1,直到弹出第1500个
        if (cnt == 1500)
        {
            printf("The 1500'th ugly number is <%I64d>.", x);
            break;
        }
        LL tmp;
        for (int i = 0; i<3; ++i)
        {
            tmp = x*base[i]; //产生2x,3x,5x
            if (s.count(tmp) == 0) //没有出现过
            {
                s.insert(tmp); //标记tmp出现过
                pq.push(tmp); //tmp插入队列
            }
        }
    }
    return 0;
}

你可能感兴趣的:(算法与数据结构,算法竞赛入门经典-第2版,排序,哈希,整理笔记)