hihocoder - 最近公共祖先系列 15、16、17周

 

#离线Tarjan算法

时间限制:10000ms

单点时限:1000ms

内存限制:256MB

描述

上上回说到,小Hi和小Ho用非常拙劣——或者说粗糙的手段山寨出了一个神奇的网站,这个网站可以计算出某两个人的所有共同祖先中辈分最低的一个是谁。远在美国的他们利用了一些奇妙的技术获得了国内许多人的相关信息,并且搭建了一个小小的网站来应付来自四面八方的请求。

但正如我们所能想象到的……这样一个简单的算法并不能支撑住非常大的访问量,所以摆在小Hi和小Ho面前的无非两种选择:

其一是购买更为昂贵的服务器,通过提高计算机性能的方式来满足需求——但小Hi和小Ho并没有那么多的钱;其二则是改进他们的算法,通过提高计算机性能的利用率来满足需求——这个主意似乎听起来更加靠谱。

于是为了他们第一个在线产品的顺利运作,小Hi决定对小Ho进行紧急训练——好好的修改一番他们的算法。

而为了更好的向小Ho讲述这个问题,小Hi将这个问题抽象成了这个样子:假设现小Ho现在知道了N对父子关系——父亲和儿子的名字,并且这N对父子关系中涉及的所有人都拥有一个共同的祖先(这个祖先出现在这N对父子关系中),他需要对于小Hi的若干次提问——每次提问为两个人的名字(这两个人的名字在之前的父子关系中出现过),告诉小Hi这两个人的所有共同祖先中辈分最低的一个是谁?

提示一:老老实实分情况讨论就不会出错的啦!

提示二:并查集其实长得很像一棵树你们不觉得么?

输入

每个测试点(输入文件)有且仅有一组测试数据。

每组测试数据的第1行为一个整数N,意义如前文所述。

每组测试数据的第2~N+1行,每行分别描述一对父子关系,其中第i+1行为两个由大小写字母组成的字符串Father_i, Son_i,分别表示父亲的名字和儿子的名字。

每组测试数据的第N+2行为一个整数M,表示小Hi总共询问的次数。

每组测试数据的第N+3~N+M+2行,每行分别描述一个询问,其中第N+i+2行为两个由大小写字母组成的字符串Name1_i, Name2_i,分别表示小Hi询问中的两个名字。

对于100%的数据,满足N<=10^5M<=10^5,且数据中所有涉及的人物中不存在两个名字相同的人(即姓名唯一的确定了一个人),所有询问中出现过的名字均在之前所描述的N对父子关系中出现过,第一个出现的名字所确定的人是其他所有人的公共祖先

        

         这个算法的特点是:一次性收集大量查询,通过一次深搜遍历一次性解决全部查询

深搜时对每个节点进行标志染色。每个节点初始时默认白色,每进入一颗子树就将根节点标记为灰色离开子树时就将根节点标记黑色

1.进入当前节点a后,查询时若两个节点都为灰色,说明当前节点a在另一个节点b的子树中,最近公共祖先就是节点b。(如图中的节点A和B)

2.若节点b还是白色,说明还没遍历,暂不处理,等深搜到节点b时再进行处理。

         3.若节点b是黑色的,说明ab不在a当前所在的同一棵子树中,最近公共祖先则是b所在的子树和a所在的子树的共同根节点。(如图中的节点A和C)

         为了解决第3种情况的查询问题,引入并查集结构,离开子树时,将根节点并到其父节点所在集合中。加快查询a和b所在子树的公共根节点。。

 

#include <iostream>
#include <vector>
#include <map>
using namespace std;

#define maxn 100005

enum COLOR{WHITE, BLACK, GREY};

int union_set[maxn];    
int findPar(int x)
{
    if(union_set[x] == x)
        return x;
    else return union_set[x] = findPar(union_set[x]);
}

void unite(int x, int y)
{
    int xPar = findPar(x), yPar = findPar(y);
    if(xPar != yPar)
        union_set[yPar] = xPar;
}

vector<int> tree[maxn];   
map<string ,int> str_map; //记录每个名字在tree[]中下标
string name[maxn];       //保存下标的名字
int color[maxn];

struct q
{
    int to;   
    int index;  //记录是第几个查询
};
vector<q> query[maxn]; //保存所有查询
int ans[maxn];        //保存答案
int n, m, cnt = 0;

int getIndex(string str) //根据输入名字查找在tree[]中的下标,若不存在则新增一个
{
    int i;
    map<string, int>::iterator itr;

    itr = str_map.find(str);
    if(itr == str_map.end()){
        name[cnt] = str;
        i = cnt++;
        str_map.insert(pair<string, int>(str, i));
    }
    else i = itr->second;

    return i;
}

void dfs(int par, int root)
{
    color[root] = GREY; 

    for(int i = 0; i < tree[root].size(); i++)
        dfs(root, tree[root][i]);

    for(int i = 0; i < query[root].size(); i++){
        q cur = query[root][i];    //节点为白色不处理
        if(color[cur.to] == GREY)  //两个节点都为灰
            ans[cur.index] = cur.to;
        else if(color[cur.to] == BLACK) //一个节点为黑
            ans[cur.index] = findPar(cur.to);
    }

    color[root] = BLACK;
    unite(par, root);
}

int main()
{
    fill(ans, ans+maxn, -1);
    for(int i = 0; i < maxn; i++) union_set[i] = i;

    cin >> n;

    string a, b;
    for(int i = 0; i < n; i++){
        cin >> a >> b;
        int aI = getIndex(a), bI = getIndex(b);
        tree[aI].push_back(bI);
    }

    cin >> m;
    for(int i = 0; i < m; i++){
        cin >> a >> b;
        int aI = str_map.find(a)->second,
            bI = str_map.find(b)->second;
        query[aI].push_back(q{bI,i});
        query[bI].push_back(q{aI,i});
    }

    dfs(0, 0);
    for(int i = 0; i < m; i++)
       cout << name[ans[i]] << endl;
    return 0;
}

 

 

 

 

#RMQ-ST算法

时间限制:10000ms

单点时限:1000ms

内存限制:256MB

描述

Hi和小Ho在美国旅行了相当长的一段时间之后,终于准备要回国啦!而在回国之前,他们准备去超市采购一些当地特产——比如汉堡(大雾)之类的回国。

但等到了超市之后,小Hi和小Ho发现者超市拥有的商品种类实在太多了——他们实在看不过来了!于是小Hi决定向小Ho委派一个任务:假设整个货架上从左到右拜访了N种商品,并且依次标号为1N,每次小Hi都给出一段区间[L, R],小Ho要做的是选出标号在这个区间内的所有商品重量最轻的一种,并且告诉小Hi这个商品的重量,于是他们就可以毫不费劲的买上一大堆东西了——多么可悲的选择困难症患者。

(虽然说每次给出的区间仍然要小Hi来进行决定——但是小Hi最终机智的选择了使用随机数生成这些区间!但是为什么小Hi不直接使用随机数生成购物清单呢?——问那么多做什么!)

提示一:二分法是宇宙至强之法!(真的么?)

提示二:线段树不也是二分法么?

 

         区间查询问题,对于每一个指定的区间都要快速查询到最值。(这里是针对数据没有改动的情况)

         对于一组数组,区间[l,r]长度为1时,每个区间的最值自然就是那个数本身。知道长度区间为1的最值,只需要比较两个长度1即可得到区间长度2的最值,同理很快可以得到长度为4,8…2^n的区间最值。对于非2^m的长度则没有必要计算,因为任意一个区间的最值,可以转化为两个长度2^t的区间(可以有重叠部分,不影响结果)最值的比较。比如区间[3,8],就是比较[3,6],[5,8]两个区间的最值即可。

         “pre_calc[L, Len]表示左边界为L,长度为Len的区间中的最小值——那么对于一个询问[Li, Ri],我只要找到小于这个区间长度的最大的2的非负整数次幂——T,那么这个区间中的最小值就是min{pre_calc[Li, T], pre_calc[Ri-T+1, T]}

 

 

#include <iostream>
#include <cmath>
#include <cstdio>
using namespace std;

#define maxn 1000005

int pre_calc[maxn][21];

int main()
{
    int n;
    scanf("%d", &n);

    for(int i = 0; i < n; i++)
        scanf("%d", &pre_calc[i][0]);

    for(int j = 1; (1<<j) <= n; j++)
    for(int i = 0; i < n; i++){
        if((1<<j) > n-i+1) break;   //长度超出范围,退出循环
        pre_calc[i][j] = min(pre_calc[i][j-1], pre_calc[i+(1<<(j-1))][j-1]);
    }

    int q, l, r;
    scanf("%d", &q);
    while(q--){
        scanf("%d%d", &l, &r);
        int maxLen = log(r-l+1)/log(2);  //找到小于这个区间长度的最大的2的非负整数次幂——T
        printf("%d\n", min(pre_calc[l-1][maxLen], pre_calc[r-(1<<maxLen)][maxLen]));
    }

    return 0;
}

 

 

 

 

 

 

#在线算法

时间限制:10000ms

单点时限:1000ms

内存限制:256MB

描述

上上回说到,小Hi和小Ho使用了Tarjan算法来优化了他们的最近公共祖先网站,但是很快这样一个离线算法就出现了问题:如果只有一个人提出了询问,那么小Hi和小Ho很难决定到底是针对这个询问就直接进行计算还是等待一定数量的询问一起计算。毕竟无论是一个询问还是很多个询问,使用离线算法都是只需要做一次深度优先搜索就可以了的。

那么问题就来了,如果每次计算都只针对一个询问进行的话,那么这样的算法事实上还不如使用最开始的朴素算法呢!但是如果每次要等上很多人一起的话,因为说不准什么时候才能够凑够人——所以事实上有可能要等上很久很久才能够进行一次计算,实际上也是很慢的!

那到底要怎么办呢?在等到10分钟,或者凑够一定数量的人两个条件满足一个时就进行运算?Ho想出了一个折衷的办法。

哪有这么麻烦!别忘了和离线算法相对应的可是有一个叫做在线算法的东西呢!Hi笑道。

Ho面临的问题还是和之前一样:假设现在小Ho现在知道了N对父子关系——父亲和儿子的名字,并且这N对父子关系中涉及的所有人都拥有一个共同的祖先(这个祖先出现在这N对父子关系中),他需要对于小Hi的若干次提问——每次提问为两个人的名字(这两个人的名字在之前的父子关系中出现过),告诉小Hi这两个人的所有共同祖先中辈分最低的一个是谁?

提示:最近公共祖先无非就是两点连通路径上高度最小的点嘛!

 

         这个算法的核心思路是:最近公共祖先是两个点连通路径上高度最小的点

 

         “从树的根节点开始进行深度优先搜索,每次经过某一个点——无论是从它的父亲节点进入这个点,还是从它的儿子节点返回这个点,都按顺序记录下来。这样,是不是就把一棵树转换成了一个数组?而找到树上两个节点的最近公共祖先,无非就是找到这两个节点最后一次出现在数组中的位置所囊括的一段区间中深度最小的那个点”

       查找区间深度最小的点,运用上面的RMQ-ST算法就可大大提高效率。

#include <iostream>
#include <map>
#include <vector>
#include <cstdio>
#include <cmath>
using namespace std;

#define maxn 100005

vector<int> tree[maxn];
map<string, int> str_map; //记录每一个节点对应tree[]中的下标
string name[maxn];  
int last[maxn];  //记录每一个节点在整个区间最后一次出现的位置
int pre_calc[2*maxn][20]; //pre_calc[i][j],记录从i开始长度为j的区间高度最小的节点的下标

int n, m, cnt = 0;

struct node
{
    int d;  //记录深度
    int id; //记录节点下标
}weight[2*maxn];


int getIndex(string str) //根据输入名字查找在tree[]中的下标,若不存在则新增一个
{
    int id;
    map<string, int>::iterator itr;
    itr = str_map.find(str);
    if(itr == str_map.end()){
        id = cnt;
        name[cnt++] = str;
        str_map.insert(pair<string, int>(str, id));
    }
    else id = itr->second;

    return id;
}


void dfs(int par, int root, int depth, int & cur)
{
    for(int i = 0; i < tree[root].size(); i++){
        int to = tree[root][i];
        weight[cur++] = node{depth+1, to};
        last[to] = cur-1;
        dfs(root, to, depth+1, cur);
    }

    if(par != -1){
        weight[cur++] = node{depth-1, par};
        last[par] = cur-1;
    }
}

int main()
{
    std::ios::sync_with_stdio(false);
    cin >> n;
    string a, b;
    for(int i = 0; i < n; i++){
        cin >> a >> b;
        int a_id = getIndex(a), b_id = getIndex(b);
        tree[a_id].push_back(b_id);
    }

    int cur = 0;
    dfs(-1, 0, 0, cur);

    for(int i = 0; i < 2*n; i++)
        pre_calc[i][0] = i;
    for(int j = 1; j < 20; j++)
    for(int i = 0; i < 2*n; i++){
        if((1<<j) > 2*n-i) break;
        int fir = pre_calc[i][j-1], sec = pre_calc[i+(1<<(j-1))][j-1];
        if(weight[fir].d < weight[sec].d)
            pre_calc[i][j] = fir;
        else pre_calc[i][j] = sec;
    }

    cin >> m;
    int ans;
    while(m--){
        cin >> a >> b;
        int a_id = getIndex(a), b_id = getIndex(b);
        int l = last[a_id], r = last[b_id];
        if(l > r) swap(l, r);
        
        int maxLen = log(r-l+1)/log(2);

        a_id = pre_calc[l][maxLen], b_id = pre_calc[r-(1<<maxLen)+1][maxLen];
        if(weight[a_id].d < weight[b_id].d)
            ans = weight[a_id].id;
        else ans = weight[b_id].id;

        cout << name[ans] << endl;
    }
    return 0;
}


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值