Category Archives: hihoCoder

hihoCoder week 49-1-欧拉路·一

hihoCoder week 49-1-欧拉路·一 题目1 : 欧拉路·一 时间限制:10000ms 单点时限:1000ms 内存限制:256MB 描述 小Hi和小Ho最近在玩一个解密类的游戏,他们需要控制角色在一片原始丛林里面探险,收集道具,并找到最后的宝藏。现在他们控制的角色来到了一个很大的湖边。湖上有N个小岛(编号1..N),以及连接小岛的M座木桥。每座木桥上各有一个宝箱,里面似乎装着什么道具。 湖边还有一个船夫,船夫告诉主角。他可以载着主角到任意一个岛上,并且可以从任意一个岛上再载着主角回到湖边,但是主角只有一次来回的机会。同时船夫告诉主角,连接岛屿之间的木桥很脆弱,走过一次之后就会断掉。 因为不知道宝箱内有什么道具,小Hi和小Ho觉得如果能把所有的道具收集齐肯定是最好的,那么对于当前岛屿和木桥的情况,能否将所有道具收集齐呢? 举个例子,比如一个由6个小岛和8座桥组成的地图: 主角可以先到达4号小岛,然后按照4->1->2->4->5->6->3->2->5的顺序到达5号小岛,然后船夫到5号小岛将主角接回湖边。这样主角就将所有桥上的道具都收集齐了。 提示:欧拉路的判定 输入 第1行:2个正整数,N,M。分别表示岛屿数量和木桥数量。1≤N≤10,000,1≤M≤50,000 第2..M+1行:每行2个整数,u,v。表示有一座木桥连接着编号为u和编号为v的岛屿,两个岛之间可能有多座桥。1≤u,v≤N 输出 第1行:1个字符串,如果能收集齐所有的道具输出“Full”,否则输出”Part”。 样例输入 6 8 1 2 1 4 2 4 2 5 2 3 3 6 4 5 5 6 样例输出 Full


本题考察无向图欧拉路的判断。 关于有向图和无向图欧拉(回)路的介绍,请看POJ Problem 1386: Play on Words。总结一下就是先用并查集判断无向图是否连通,然后判断奇数度的节点个数是否为0或2。完整代码如下: [cpp] #include<iostream> #include<cstdio> using namespace std; const int kMax = 10005; int n, m; int degree[kMax]; int father[kMax]; int Find(int x) { return x == father[x] ? x : (father[x] = Find(father[x])); } void Union(int x, int y) { father[Find(x)] = Find(y); } void Init() { for (int i = 1; i <= n; i++) father[i] = i; } bool IsConnected() { for (int i = 1; i <= n; i++) if (father[i] != father[1]) return false; return true; } bool IsEuler() { int odd = 0; for (int i = 1; i <= n; i++) if (degree[i] & 1) odd++; if (odd == 0 || odd == 2) return true; return false; } int main() { scanf("%d %d", &n, &m); int u, v; for (int i = 0; i < m; i++) { scanf("%d %d", &u, &v); degree[u]++; degree[v]++; Union(u, v); } if (IsConnected()) { if (IsEuler()) printf("Full\n"); else printf("Part\n"); } else printf("Part\n"); return 0; } [/cpp] 本代码提交AC,用时13MS,内存1MB。 总结: 判断图中是否存在环路详细介绍可看这里) 1. 无向图:依次删除图中度数为0或1的点及其关联的边,循环结束,如果还有未删除的点,则存在回路。 2. 有向图:拓扑排序,依次删除图中入度为0的点及其关联的出边,循环结束,如果还有未删除的点,则存在回路。 判断图是否连通 1. 无向图:使用并查集,最终如果所有节点的父亲都相同,则无向图是连通的。 2. 有向图:判断有向图是否强连通貌似很复杂,Kosaraju算法判断强连通图以及有向图强连通分量的Tarjan算法。]]>

hihoCoder 1093-最短路径·三:SPFA算法

hihoCoder 1093-最短路径·三:SPFA算法 #1093 : 最短路径·三:SPFA算法 时间限制:10000ms 单点时限:1000ms 内存限制:256MB 描述 万圣节的晚上,小Hi和小Ho在吃过晚饭之后,来到了一个巨大的鬼屋! 鬼屋中一共有N个地点,分别编号为1..N,这N个地点之间互相有一些道路连通,两个地点之间可能有多条道路连通,但是并不存在一条两端都是同一个地点的道路。 不过这个鬼屋虽然很大,但是其中的道路并不算多,所以小Hi还是希望能够知道从入口到出口的最短距离是多少? 提示:Super Programming Festival Algorithm。 输入 每个测试点(输入文件)有且仅有一组测试数据。 在一组测试数据中: 第1行为4个整数N、M、S、T,分别表示鬼屋中地点的个数和道路的条数,入口(也是一个地点)的编号,出口(同样也是一个地点)的编号。 接下来的M行,每行描述一条道路:其中的第i行为三个整数u_i, v_i, length_i,表明在编号为u_i的地点和编号为v_i的地点之间有一条长度为length_i的道路。 对于100%的数据,满足N<=10^5,M<=10^6, 1 <= length_i <= 10^3, 1 <= S, T <= N, 且S不等于T。 对于100%的数据,满足小Hi和小Ho总是有办法从入口通过地图上标注出来的道路到达出口。 输出 对于每组测试数据,输出一个整数Ans,表示那么小Hi和小Ho为了走出鬼屋至少要走的路程。 样例输入 5 10 3 5 1 2 997 2 3 505 3 4 118 4 5 54 3 5 480 3 4 796 5 2 794 2 5 146 5 4 604 2 5 63 样例输出 172


对于边稀疏的图,SPFA算法可以更高效的求解单源最短路径问题。 假设要求S和T的最短路径,该算法使用一个队列,最开始队列中只有(S,0)—表示当前处于点S,从点S到达该点的距离为0,然后每次从队首取出一个节点(i, L)——表示当前处于点i,从点S到达该点的距离为L,接下来遍历所有从这个节点出发的边(i, j, l)——表示i和j之间有一条长度为l的边,在将(j, L+l)加入到队列之前,先判断队列中是否存在点j,如果存在(j,l’),则比较L+l和l’的大小关系,如果L+l>=l’,那么(j,L+l)这条路就没必要继续搜索下去了,所以不将(j,L+l)加入队列;如果L+l<l’,那么原来的(j,l’)没必要继续搜索下去了,把(j,l’)替换成(j,L+l)即可。如果原队列中不存在j点,则直接把(j,L+l)加入队列。 当队列为空时,(T,ans)就是S到T的距离为ans。所以SPFA在某种程度上来说,就是BFS+剪枝。 SPFA的原理不难,写代码的时候要注意几点:1)N的范围是N<=10^5,如果用数组存储的话,内存够呛,所以建议用vector动态分配;2)每次从队列中弹出一个点时,记得将其在队列中的标记清空,即used[i]=0;3)处理好数据结构问题,因为数据输入中2点之间的距离可能有多个值,你可以在输入的时候只存储最小值,你也可以为了方便先把所有数据都存起来,统一在SPFA算法中进行大小判断,本代码使用第二种策略。
完整代码如下:


#include <iostream>
#include <cstdio>
#include <vector>
#include <queue>
using namespace std;
const int MAX_N = 1e5 + 10;
const int INF = 1e6 + 10;
int n, m, s, t;
//int path[MAX_N][MAX_N];//数组太大,改用vector动态分配
vector<int> path[MAX_N]; //path[i][j]表示第i个点和第path[i][j]个点有路相同,其中j是输入的顺序
vector<int> dist[MAX_N]; //dist[i][j]表示和第i个点相连的第j个点的距离是dist[i][j],path[i][j]和dist[i][j]是通过相同的j来共享数据
int used[MAX_N]; //used[i]表示第i个点是否在队列中
int s_dist[MAX_N]; //s_dist[i]表示第i个点和起始点s的距离
int spfa()
{
    int q1 = s;
    used[q1] = 1;
    for (int i = 1; i <= n; i++)
        s_dist[i] = INF;
    s_dist[s] = 0;
    queue<int> Q;
    Q.push(q1);
    while (!Q.empty()) {
        q1 = Q.front();
        Q.pop();
        used[q1] = 0; //当弹出队列时记得设置used[i]=0
        int qs = path[q1].size(); //和q1点相连的点的个数
        for (int i = 0; i < qs; i++) {
            int u = path[q1][i];
            if (s_dist[q1] + dist[q1][i] < s_dist[u]) //如果u是q1的父节点,肯定不满足这一条,所以不会回溯
            {
                s_dist[u] = s_dist[q1] + dist[q1][i];
                if (used[u] == 0) {
                    used[u] = 1;
                    Q.push(u);
                }
            }
        }
    }
    return s_dist[t];
}
int main()
{
    //freopen("input.txt","r",stdin);
    scanf("%d%d%d%d", &n, &m, &s, &t);
    int u, v, len;
    while (m–) {
        scanf("%d%d%d", &u, &v, &len); //记录了所有数据,此处还可以优化
        path[u].push_back(v);
        path[v].push_back(u);
        dist[u].push_back(len);
        dist[v].push_back(len);
    }
    printf("%d\n", spfa());
    return 0;
}

本代码提交AC,用时208MS,内存19MB。

hihoCoder 1077-RMQ问题再临-线段树

hihoCoder 1077-RMQ问题再临-线段树 #1077 : RMQ问题再临-线段树 时间限制:10000ms 单点时限:1000ms 内存限制:256MB 描述 上回说到:小Hi给小Ho出了这样一道问题:假设整个货架上从左到右摆放了N种商品,并且依次标号为1到N,每次小Hi都给出一段区间[L, R],小Ho要做的是选出标号在这个区间内的所有商品重量最轻的一种,并且告诉小Hi这个商品的重量。但是在这个过程中,可能会因为其他人的各种行为,对某些位置上的商品的重量产生改变(如更换了其他种类的商品)。 小Ho提出了两种非常简单的方法,但是都不能完美的解决。那么这一次,面对更大的数据规模,小Ho将如何是好呢? 提示:其实只是比ST少计算了一些区间而已 输入 每个测试点(输入文件)有且仅有一组测试数据。 每组测试数据的第1行为一个整数N,意义如前文所述。 每组测试数据的第2行为N个整数,分别描述每种商品的重量,其中第i个整数表示标号为i的商品的重量weight_i。 每组测试数据的第3行为一个整数Q,表示小Hi总共询问的次数与商品的重量被更改的次数之和。 每组测试数据的第N+4~N+Q+3行,每行分别描述一次操作,每行的开头均为一个属于0或1的数字,分别表示该行描述一个询问和描述一次商品的重量的更改两种情况。对于第N+i+3行,如果该行描述一个询问,则接下来为两个整数Li, Ri,表示小Hi询问的一个区间[Li, Ri];如果该行描述一次商品的重量的更改,则接下来为两个整数Pi,Wi,表示位置编号为Pi的商品的重量变更为Wi 对于100%的数据,满足N<=10^6,Q<=10^6, 1<=Li<=Ri<=N,1<=Pi<=N, 0<weight_i, Wi<=10^4。 输出 对于每组测试数据,对于每个小Hi的询问,按照在输入中出现的顺序,各输出一行,表示查询的结果:标号在区间[Li, Ri]中的所有商品中重量最轻的商品的重量。 样例输入 10 3655 5246 8991 5933 7474 7603 6098 6654 2414 884 6 0 4 9 0 2 10 1 4 7009 0 5 6 1 3 7949 1 3 1227 样例输出 2414 884 7474


这一题是hihoCoder Problem 1070:RMQ问题再临的升级版,数据量提升到10^6,但提示还是用线段树来解决。我原本以为只要把之前的数组大小改为10^6就行了,没想到这次居然给我报CE错。 第一次遇到这种错误,看了半天没明白什么意思,后来多方查找才得知可能是数组太大了,直接编译就不通过,想想看10^6的二维数组:10^6*10^6=10^12,装的是int,则总大小为4*10^12B=3725G,这明显大大超出了内存范围,而之前的10^4二维数组只有4*10^8B=381M,按理说也超出了题目的256MB,不过还是险些AC了,但是这一次就没这么好运了,所以必须优化算法! 怎样优化内存空间呢?先把我们的线段树请出来看看: 上图是线段树的一个例子,每个节点保存了区间范围以及该区间的最小值。总的区间大小是[1,10],仔细看看这个区间树的节点个数只有19个;另外再画一个[1,6]区间上的区间树,节点个数只有11个。可以不加证明的得出一个n的区间长度的线段树的节点个数为2*n-1,这远远小于n*n,所以我们只需要O(n)的空间来存储,而不是O(n^2)。 反观之前hihoCoder Problem 1070:RMQ问题再临的解法,其实没有构造一个真正的线段树,所以浪费了很多空间,那么怎样来构造一个真正的线段树呢? 我们知道常规的树形结构是用链表来实现的,每一个节点都有指向其左右孩子节点的指针,这样就可以很容易的访问孩子节点,如果用数组的结构来表示链表的结果,是不是会简单很多呢?于是我们定义如下树的节点结构: [cpp] typedef struct node//线段树节点 { int l;//区间左端点 int r;//区间右端点 int minv;//区间最小值 }; [/cpp] 再定义一个表示树的数组node tree[2*MAX_N];很自然的tree[i]的左右孩子节点分别存储在tree[2i]和tree[2i+1],这样是不是也很容易访问孩子节点了呢。 不论是创建、查询、更新树,都是从树根开始递归往下,这个过程和之前的那个题目类似,这里就不再赘述了。完整的代码如下: [cpp] #include<iostream> #include<cstdio> using namespace std; const int MAX_N=1e6+2; int w[MAX_N]; int n,q; typedef struct node//线段树节点 { int l;//区间左端点 int r;//区间右端点 int minv;//区间最小值 }; node tree[2*MAX_N]; inline int get_min(int a,int b) { return a<b?a:b; } //创建树 void build(int l,int r,int pos) { tree[pos].l=l; tree[pos].r=r; if(l==r) tree[pos].minv=w[l]; else { int mid=(l+r)/2; build(l,mid,pos*2); build(mid+1,r,pos*2+1); tree[pos].minv=get_min(tree[pos*2].minv,tree[pos*2+1].minv); } } //查询树 int query(int l,int r,int pos) { if(l==tree[pos].l&&r==tree[pos].r) return tree[pos].minv; int mid=(tree[pos].l+tree[pos].r)/2; if(r<=mid) return query(l,r,pos*2); else if(l>mid) return query(l,r,pos*2+1); else { int left=query(l,mid,pos*2); int right=query(mid+1,r,pos*2+1); return get_min(left,right); } } //更新树 void update(int pi,int wi,int pos) { if(tree[pos].l==tree[pos].r&&tree[pos].l==pi) tree[pos].minv=wi; else { int mid=(tree[pos].l+tree[pos].r)/2; if(pi<=mid) update(pi,wi,pos*2); else update(pi,wi,pos*2+1); tree[pos].minv=get_min(tree[pos*2].minv,tree[pos*2+1].minv); } } int main() { //freopen("input.txt","r",stdin); scanf("%d",&n); for(int i=1;i<=n;i++) scanf("%d",&w[i]); build(1,n,1); scanf("%d",&q); int p,l,r; for(int i=0;i<q;i++) { scanf("%d%d%d",&p,&l,&r); if(p==0) printf("%d\n",query(l,r,1)); else update(l,r,1); } return 0; } [/cpp] 本代码提交AC,用时697MS,内存45MB。 P.S.之前用cin、cout超时,改成scanf、printf就好了,懒得取消同步什么的,以后就打算一直用scanf、printf了。唉,从上一题到这一题,优化是无止境的啊~~]]>

hihoCoder 1070-RMQ问题再临

hihoCoder 1070-RMQ问题再临 #1070 : RMQ问题再临 时间限制:10000ms 单点时限:1000ms 内存限制:256MB 描述 终于,小Hi和小Ho踏上了回国的旅程。在飞机上,望着采购来的特产——小Hi陷入了沉思:还记得在上上周他们去超市的时候,前前后后挑了那么多的东西,都幸运的没有任何其他人(售货员/其他顾客)来打搅他们的采购过程。但是如果发生了这样的事情,他们的采购又会变得如何呢? 于是小Hi便向小Ho提出了这个问题:假设整个货架上从左到右摆放了N种商品,并且依次标号为1到N,每次小Hi都给出一段区间[L, R],小Ho要做的是选出标号在这个区间内的所有商品重量最轻的一种,并且告诉小Hi这个商品的重量。但是在这个过程中,可能会因为其他人的各种行为,对某些位置上的商品的重量产生改变(如更换了其他种类的商品),面对这样一个问题,小Ho又该如何解决呢? 提示:平衡乃和谐之理 输入 每个测试点(输入文件)有且仅有一组测试数据。 每组测试数据的第1行为一个整数N,意义如前文所述。 每组测试数据的第2行为N个整数,分别描述每种商品的重量,其中第i个整数表示标号为i的商品的重量weight_i。 每组测试数据的第3行为一个整数Q,表示小Hi总共询问的次数与商品的重量被更改的次数之和。 每组测试数据的第N+4~N+Q+3行,每行分别描述一次操作,每行的开头均为一个属于0或1的数字,分别表示该行描述一个询问和描述一次商品的重量的更改两种情况。对于第N+i+3行,如果该行描述一个询问,则接下来为两个整数Li, Ri,表示小Hi询问的一个区间[Li, Ri];如果该行描述一次商品的重量的更改,则接下来为两个整数Pi,Wi,表示位置编号为Pi的商品的重量变更为Wi 对于100%的数据,满足N<=10^4,Q<=10^4, 1<=Li<=Ri<=N,1<=Pi<=N, 0<weight_i, Wi<=10^4。 输出 对于每组测试数据,对于每个小Hi的询问,按照在输入中出现的顺序,各输出一行,表示查询的结果:标号在区间[Li, Ri]中的所有商品中重量最轻的商品的重量。 样例输入 10 618 5122 1923 8934 2518 6024 5406 1020 8291 2647 6 0 3 6 1 2 2009 0 2 2 0 2 10 1 1 5284 0 2 5 样例输出 1923 2009 1020 1923


本题的数据量比hihoCoder Problem 1068: RMQ-ST 算法这题要小,虽然
用O(N)的时间进行计算——扫描一遍整个区间找到最小值,然后对于每一个更改操作,使用O(1)的时间直接进行修改,这样也就是O(NQ)的总时间复杂度!在这种数据量下完全是可以通过的!
但是本题希望我们用线段树来解决。 我曾在《数据结构》的改造红黑树中看到过区间树,但是本题的线段树和书中的区间树有所区别,区间树由红黑树改造而来,结构更复杂些,这里只需要使用线段树即可。 常规的线段树如下图所示: 从图中可以看到构造线段树的过程就是一个二分的过程,不断将区间分成两半,直到只有一个元素。图中的线段树每一个节点是一个区间[l,r],本题我稍微改造了一下,改成了数组int seg_tree[left][length],比如seg_tree[i][j]表示从下标i开始,长度为j的这样一个区间上的最小值,这样就可以利用线段树来解决RMQ问题了。比如改造后的线段树就成了下面的样子: 因为树形这种特殊的结构,我们可以用一个DFS来对树实现二分构造,当DFS到某个节点长度为1时,其最小值就是w[i]本身,在回溯到父节点时,父节那个区间的最小值又是所有子节点最小值中的最小值。因为树的总节点数大约为2*n,所以复杂度O(n)。 当需要查询区间[l,r]的最小值时,只需对数组seg_tree二分搜索。具体来说,假设我们搜索到了节点[s_l,s_len],如果r<(s_l+s_len/2),说明区间[l,r]全在[s_l,s_len]的左边,我们递归在[s_l,s_len/2]区间找;如果l>=(s_l+s_len/2),说明区间[l,r]全在[s_l,s_len]的右边,我们递归在[s_l+s_len/2,s_len-s_len/2]区间找;如果以上两者都不是,说明[l,r]跨界了,而且中点下标一定是s_l+s_len/2,所以我们分别在二两半区间找,然后求这两者的最小值。复杂度O(lgn)。 当需要更新某个下标为pos的值为value时,也是DFS查找线段树,直到找到叶子seg_tree[pos][1],更新它的值,以及所有我们在查找过程经过的父节点的值。复杂度O(lgn)。 所以线段是的性质使得无论是构造、查询、更新操作,复杂度都只要O(lgn),这就是题目中所说的把总的复杂度平均分配到不同操作:平衡乃和谐之理。 完整代码如下: [cpp] #include<iostream> using namespace std; const int MAX_N=1e4+2; int w[MAX_N];//每个商品重量 int n,m; int seg_tree[MAX_N][MAX_N];//seg_tree[i][j]:起点为i,长度为j的区间的最小值 inline int get_min(int a,int b) { return a<b?a:b; } //深度优先遍历以构造线段树 void dfs(int left,int length) { if(length==1) { seg_tree[left][1]=w[left]; return; } dfs(left,length/2); dfs(left+length/2,length-length/2); seg_tree[left][length]=get_min(seg_tree[left][length/2],seg_tree[left+length/2][length-length/2]);//取最小值 } //在区间[s_left,s_len]搜索区间[left,length]的最小值 int search_min(int s_left,int s_len,int left,int length) { if((s_left==left)&&(s_len==length)) return seg_tree[s_left][s_len]; if((left+length-1)<(s_left+s_len/2))//全在左半部分 { return search_min(s_left,s_len/2,left,length); } else if(left>=(s_left+s_len/2))//全在右半部分 { return search_min(s_left+s_len/2,s_len-s_len/2,left,length); } else//左右分开搜索 { int left_len=s_left+s_len/2-left; int right_len=length-left_len; int min_left=search_min(s_left,s_len/2,left,left_len); int min_right=search_min(s_left+s_len/2,s_len-s_len/2,s_left+s_len/2,right_len); return get_min(min_left,min_right); } } //从区间[s_left,s_len]开始更新下标pos的值为value void update(int s_left,int s_len,int pos,int value) { if((s_left==pos)&&(s_len==1)) { seg_tree[s_left][1]=value; return ; } int mid=s_left+s_len/2; if(pos<mid) update(s_left,s_len/2,pos,value); else update(mid,s_len-s_len/2,pos,value); seg_tree[s_left][s_len]=get_min(seg_tree[s_left][s_len/2],seg_tree[mid][s_len-s_len/2]);//更新父节点 } int main() { //freopen("input.txt","r",stdin); cin>>n; for(int i=1;i<=n;i++) cin>>w[i]; dfs(1,n); cin>>m; int p,l,r; for(int i=0;i<m;i++) { cin>>p>>l>>r; if(p==0)//查询 { cout<<search_min(1,n,l,r-l+1)<<endl; } else//修改 { update(1,n,l,r); } } return 0; } [/cpp] 本代码提交AC,用时151MS,内存42MB。 ]]>

hihoCoder 1050-树中的最长路

hihoCoder 1050-树中的最长路 #1050 : 树中的最长路 时间限制:10000ms 单点时限:1000ms 内存限制:256MB 描述 上回说到,小Ho得到了一棵二叉树玩具,这个玩具是由小球和木棍连接起来的,而在拆拼它的过程中,小Ho发现他不仅仅可以拼凑成一棵二叉树!还可以拼凑成一棵多叉树——好吧,其实就是更为平常的树而已。 但是不管怎么说,小Ho喜爱的玩具又升级换代了,于是他更加爱不释手(其实说起来小球和木棍有什么好玩的是吧= =)。小Ho手中的这棵玩具树现在由N个小球和N-1根木棍拼凑而成,这N个小球都被小Ho标上了不同的数字,并且这些数字都是出于1..N的范围之内,每根木棍都连接着两个不同的小球,并且保证任意两个小球间都不存在两条不同的路径可以互相到达。总而言之,是一个相当好玩的玩具啦! 但是小Hi瞧见小Ho这个样子,觉得他这样沉迷其中并不是一件好事,于是寻思着再找点问题让他来思考思考——不过以小Hi的水准,自然是手到擒来啦! 于是这天食过早饭后,小Hi便对着又拿着树玩具玩的不亦乐乎的小Ho道:“你说你天天玩这个东西,我就问你一个问题,看看你可否知道?” “不好!”小Ho想都不想的拒绝了。 “那你就继续玩吧,一会回国的时候我不叫上你了~”小Hi严肃道。 “诶!别别别,你说你说,我听着呢。”一向习惯于开启跟随模式的小Ho忍不住了,马上喊道。 小Hi满意的点了点头,随即说道:“这才对嘛,我的问题很简单,就是——你这棵树中哪两个结点之间的距离最长?当然,这里的距离是指从一个结点走到另一个结点经过的木棍数。”。 “啊?”小Ho低头看了看手里的玩具树,困惑了。 提示一:路总有折点,路径也不例外! 输入 每个测试点(输入文件)有且仅有一组测试数据。 每组测试数据的第一行为一个整数N,意义如前文所述。 每组测试数据的第2~N行,每行分别描述一根木棍,其中第i+1行为两个整数Ai,Bi,表示第i根木棍连接的两个小球的编号。 对于20%的数据,满足N<=10。 对于50%的数据,满足N<=10^3。 对于100%的数据,满足N<=10^5,1<=Ai<=N, 1<=Bi<=N 小Hi的Tip:那些用数组存储树边的记得要开两倍大小哦! 输出 对于每组测试数据,输出一个整数Ans,表示给出的这棵树中距离最远的两个结点之间相隔的距离。 样例输入 8 1 2 1 3 1 4 4 5 3 6 6 7 7 8 样例输出 6


本题要求一棵树中相距最远两点之间的距离,也就是`树的直径`。 求树的直径有多种方法,我这里介绍两种。 第一种方法 第一种方法很好理解,我们随机的从某个节点s对树深度优先遍历(广度优先遍历也行),找到深度最大的叶子节点node1;然后再从node1点对树DFS,找到深度最大的叶子节点node2;则node1和node2即为这棵树中相距最远的两个点,它们之间的距离即为树的直径。 关于这种方法的简要证明,可以看这篇博客。 这种方法简单易懂,可以很快的写出代码,如下: [cpp] #include<iostream> #include<vector> using namespace std; const int MAX_N=1e5+2; int n;//节点数 int longest;//最远距离 int node1,node2;//node1,node2分别为为第一、二次DFS找到的最远点 vector<vector<int> > f_s(MAX_N);//father-son和son-father关系,存储双边 //参数分别为当前节点,深度,父亲节点 void dfs(int root,int depth,int father) { if(depth>longest)//更新 { longest=depth; node1=root; } int sons=f_s[root].size(); for(int i=0;i<sons;i++) { if(f_s[root][i]!=father)//不能回溯 dfs(f_s[root][i],depth+1,root); } } int main() { //freopen("input.txt","r",stdin); cin>>n; int a,b; for(int i=0;i<n-1;i++) { cin>>a>>b; f_s[a].push_back(b);//记录双边关系 f_s[b].push_back(a);//记录双边关系 } longest=0; dfs(1,0,-1);//随机选一个点(1)找到第一个最远点node1 longest=0; dfs(node1,0,-1);//以node1找第二个最远点node2,则node1到node2即为整棵树的直径 cout<<longest; return 0; } [/cpp] 本代码提交AC,用时255MS,内存9MB。 有一点需要提醒的是,因为不仅需要从s开始DFS,还需要从node1开始DFS,所以需要存储双向边,即不仅要存储(v,u),还需要存储(u,v);这样存了之后,为了防止DFS时回溯到父节点造成死循环,在DFS时要判断下一个节点是不是父节点,如果是则不继续DFS。 第二种方法 第一种方法虽然简单易懂,但不是hiho题目中给出的方法,那么hiho给出的又是什么方法呢?且看官方提示:
“没错,我枚举每一个点作为转折点t,求出以t为根节点的子树中的‘最长路’以及与‘最长路’不重合的‘次长路’,用这两条路的长度之和去更新答案,那么最终的答案就是这棵树的最长路长度了!”小Hi道。 “原来是这样,也就是说我只要以类似后序遍历的方式依次访问每个结点,从下往上依次计算每个结点的first值和second值,我就能够用O(N)的时间复杂度来解决这个问题咯!”
这提示的第一句话我倒是理解了,但是第二句一直不懂,为什么只要O(N)就可以解决问题呢。不管了,先看看只用第一句话能否AC吧。 我一开始的理解是这样的:尝试枚举每一个点为树的根节点t,然后从t开始DFS,找到以t为根的树的最长路径和次长路径(最长和次长路径不能有重复边),然后把他们加起来就是以t为根节点的树找到最长路径,而且这条最长路径肯定经过了根节点t。 比如样例输入,分别以节点1、2、3为根构成的树如下: 因为我们默认以t为根的树的最长路径一定经过节点t,所以上图最长路径分别为图中红色路线所示,长度分别为6,5,6(因为节点1和3都在最长路径上,所以他们求得的最长路径相等)。同理我们还可以求出以4,5…n为根(路径转折点)的最长路径,最后求这些最长路径中的最大值。 这个版本的代码如下: [cpp] #include<iostream> #include<vector> using namespace std; const int MAX_N=1e5+2; int n;//节点数 vector<vector<int> > f_s(MAX_N);//father-son和son-father关系,存储双边 vector<int> first,second;//first[i]和second[i]分别表示以i节点为根节点时(i节点以下构成的子树)的最大和次大长度 //参数分别为当前节点,父亲节点 void dfs(int root,int father) { int sons=f_s[root].size(); for(int i=0;i<sons;i++) { if(f_s[root][i]!=father)//不能回溯 { dfs(f_s[root][i],root); if(first[f_s[root][i]]+1>first[root]) { second[root]=first[root]; first[root]=first[f_s[root][i]]+1; } else if(first[f_s[root][i]]+1>second[root]) second[root]=first[f_s[root][i]]+1; } } } void init_first_second() { first.clear(); first.assign(n+1,0); second.clear(); second.assign(n+1,0); } int main() { freopen("input.txt","r",stdin); cin>>n; int a,b; for(int i=0;i<n-1;i++) { cin>>a>>b; f_s[a].push_back(b);//记录双边关系 f_s[b].push_back(a);//记录双边关系 } int ans=0; for(int i=1;i<=n;i++)//枚举每一个节点为根节点(转折点) { init_first_second(); dfs(i,-1); if(first[i]+second[i]>ans) ans=first[i]+second[i]; } cout<<ans; return 0; } [/cpp] 但是很遗憾,提交后TLE。 后来我仔细想了想,无论我们从哪个点s开始对树DFS,我们求得的first[i]和second[i]都是以点i为根构成的子树中它的最长和次长距离,那么first[i]+second[i]不就代表了从i开始DFS时得到的最长距离吗?比如图中以2为根的树中,我们在DFS的过程中肯定已经求到了first[1]和second[1],然后才能求first[2]和second[2],而这里的first[1]和second[2]和图中以1为根时求得的first[1]和second[1]是完全一样的。而对于以3为根的树,因为3正好在最长路径上,所以这里的first[1]和second[1]要小于前面两种情况,但是first[3]+second[3]和前面两种情况的first[1]+second[1]是相等的。 说了这么多,不知道同学们有没有发现,不管从哪个点开始DFS,只要一遍DFS结束,最长路径总会出现在某个first[i]+second[i]中。 至此,可以省略上一个版本中的for循环,只要一个DFS即可: [cpp] #include<iostream> #include<vector> using namespace std; const int MAX_N=1e5+2; int n;//节点数 vector<vector<int> > f_s(MAX_N);//father-son和son-father关系,存储双边 vector<int> first,second;//first[i]和second[i]分别表示以i节点为根节点时(i节点以下构成的子树)的最大和次大长度 int longest=0;//最终结果 //参数分别为当前节点,父亲节点 void dfs(int root,int father) { int sons=f_s[root].size(); for(int i=0;i<sons;i++) { if(f_s[root][i]!=father)//不能回溯 { dfs(f_s[root][i],root); if(first[f_s[root][i]]+1>first[root])//更新最大和次大 { second[root]=first[root]; first[root]=first[f_s[root][i]]+1; } else if(first[f_s[root][i]]+1>second[root])//只更新次大 second[root]=first[f_s[root][i]]+1; } } if(first[root]+second[root]>longest)//更新全局最大 longest=first[root]+second[root]; } void init_first_second() { first.assign(n+1,0); second.assign(n+1,0); } int main() { //freopen("input.txt","r",stdin); cin>>n; int a,b; for(int i=0;i<n-1;i++) { cin>>a>>b; f_s[a].push_back(b);//记录双边关系 f_s[b].push_back(a);//记录双边关系 } init_first_second(); dfs(1,-1);//使用任意一个节点DFS,结果都是一样的。 /*int ans=0; for(int i=1;i<=n;i++)//该循环可在DFS时完成 { if(first[i]+second[i]>ans) ans=first[i]+second[i]; }*/ cout<<longest; return 0; } [/cpp] 本代码提交AC,用时224MS,内存9MB。 第二种方法不太好理解,所以还是建议大家用第一种方法。 网上关于树的直径介绍了3种方法,可以参考这篇文章]]>

hihoCoder 1069-最近公共祖先·三

hihoCoder 1069-最近公共祖先·三 #1069 : 最近公共祖先·三 时间限制:10000ms 单点时限:1000ms 内存限制:256MB 描述 上上回说到,小Hi和小Ho使用了Tarjan算法来优化了他们的“最近公共祖先”网站,但是很快这样一个离线算法就出现了问题:如果只有一个人提出了询问,那么小Hi和小Ho很难决定到底是针对这个询问就直接进行计算还是等待一定数量的询问一起计算。毕竟无论是一个询问还是很多个询问,使用离线算法都是只需要做一次深度优先搜索就可以了的。 那么问题就来了,如果每次计算都只针对一个询问进行的话,那么这样的算法事实上还不如使用最开始的朴素算法呢!但是如果每次要等上很多人一起的话,因为说不准什么时候才能够凑够人——所以事实上有可能要等上很久很久才能够进行一次计算,实际上也是很慢的! “那到底要怎么办呢?在等到10分钟,或者凑够一定数量的人两个条件满足一个时就进行运算?”小Ho想出了一个折衷的办法。 “哪有这么麻烦!别忘了和离线算法相对应的可是有一个叫做在线算法的东西呢!”小Hi笑道。 小Ho面临的问题还是和之前一样:假设现在小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^5,M<=10^5, 且数据中所有涉及的人物中不存在两个名字相同的人(即姓名唯一的确定了一个人),所有询问中出现过的名字均在之前所描述的N对父子关系中出现过,且每个输入文件中第一个出现的名字所确定的人是其他所有人的公共祖先。 输出 对于每组测试数据,对于每个小Hi的询问,按照在输入中出现的顺序,各输出一行,表示查询的结果:他们的所有共同祖先中辈分最低的一个人的名字。 样例输入 4 Adam Sam Sam Joey Sam Micheal Adam Kevin 3 Sam Sam Adam Sam Micheal Kevin 样例输出 Sam Adam Adam


这应该是最近公共祖先的最后一种算法了吧,hihoCoder还真是循序渐进啊,昨天刚刚学了RMQ-ST算法hihoCoder Problem 1068: RMQ-ST 算法,今天就用上了。 本题的解题思路也是非常的赞,首先把族谱树结构展开成一个数组B,当询问a,b节点的最近公共祖先时,在B中找到a,b最后出现的位置(也就是离开a,b节点的时候)an,bn,在区间[an,bn]上利用RMQ-ST算法求解区间深度最小的节点c,则c即为a,b的最近公共祖先,下面来具体讲解。 将族谱树展开成一个数组 为了利用RMQ-ST算法,需要将树形结构展开成一个数组,展开的方式是深度优先遍历树,将访问的节点顺序记录下来,不管是进入这个节点还是离开这个节点,都需要记录下来。比如下面一棵树: 是样例输入构成的一个家谱树,我们常规DFS这个树的序列是这样的:ASJMK。但是本题在进入和离开某个节点时都需要记录下来,而且每访问完一个子节点,要访问下一个子节点时,也需要记录当前父节点。比如访问完J准备访问M时,需要记录S,由此形成的序列为:ASJSMSAKA。 DFS生成的数组B长度大概为边数的两倍,所以复杂度级别只是O(n)的。 DFS之后就把树转换为了数组B[],因为使用RMQ-ST算法需要求某个区间的最小深度节点,所以在DFS的时候还需要记录每个节点的深度信息,而且还要记录每个节点最后访问的下标,这两点都不难,大家直接看代码即可。 RMQ-ST算法 得到数组B后就好办了,可以利用昨天学习的RMQ-ST算法求解2的幂区间长度间深度最小的节点,不过因为我们这题最终输出的是节点的名字而不是节点的深度,所以RMQ-ST比较的是深度,但存储的是节点名。 观察RMQ-ST算法的二重循环知道其复杂度为O(nlgn)。 在线求解 一切准备就绪后就可以求解了,首先根据字符串名字找到对应数字序号,然后查看两节点最后出现在树中的下标l,r,再讲[l,r]区间分成能覆盖[l,r]的2的幂的长度的半区间[l,t]和[t+1,r],最后求这两个区间最小深度的节点,输出其名称。 完整代码如下: [cpp] #include<iostream> #include<map> #include<string> #include<vector> #include<cmath> using namespace std; const int MAX_N=100010;//最大人数 map<string,int> name_index;//名字转换为数字 vector<string> index_name(MAX_N);//数字转换为名字 int n,m,k;//k全部记录将树转换成数组tree[]是的下标,转换结束时k即为tree[]的长度 vector<vector<int> > f_s(MAX_N);//f_s[i]表示第i个父亲的所有孩子 int tree[2*MAX_N];//DFS将树转换为的数组 int depth[MAX_N];//每个点在树中的深度 int rmq[MAX_N][30];//区间深度最低值 int last_show[MAX_N];//某个元素最后出现在tree中的位置(下标) //保存某个人的信息,并返回其下标 int store_name(string name) { map<string,int>::iterator it=name_index.find(name); if(it==name_index.end())//还不存在 { int curr_index=name_index.size();//用当前已有人数作为其下标,正好是递增的。 name_index.insert(make_pair(name,curr_index)); index_name[curr_index]=name;//记录 return curr_index;//返回下标 } else return it->second;//已经存在,直接返回 } //深度遍历树 void DFS(int root,int deep) { tree[k++]=root;//进入即记录 depth[root]=deep;//记录深度 int sons=f_s[root].size(); for(int i=0;i<sons;i++) { DFS(f_s[root][i],deep+1); tree[k++]=root;//每返回一次都要记录一次根节点 } last_show[root]=k-1;//root节点最后出现的位置 } //求a,b最小值 inline int get_min(int a,int b) { return a<b?a:b; } void init_rmq() { for(int j=0;j<k;j++) //rmq[j][0]=depth[tree[j]]; rmq[j][0]=tree[j];//保存的是这个节点,而不是它的深度,方便最后输出 int q=floor(log((double)k)/log(2.0)); for(int i=1;i<=q;i++) { for(int j=k-1;j>=0;j–) { rmq[j][i]=rmq[j][i-1]; if(j+(1<<(i-1))<k) { if(depth[rmq[j+(1<<(i-1))][i-1]]<depth[rmq[j][i]])//+优先级高于<<,所以改j+1<<(i-1)为j+(1<<(i-1)) rmq[j][i]=rmq[j+(1<<(i-1))][i-1]; } //rmq[j][i]=get_min(rmq[j][i],rmq[j+1<<(i-1)][i-1]); } } } int main() { //freopen("input.txt","r",stdin); cin>>n; string name1,name2; int index1,index2; while(n–) { cin>>name1>>name2; index1=store_name(name1); index2=store_name(name2); f_s[index1].push_back(index2); } k=0;//开始时树的下标为0,DFS完之后正好是tree[]数组的长度,可以作为rmq-st的n DFS(0,0); init_rmq(); cin>>m; int l,r,t; while(m–) { cin>>name1>>name2; if(name1==name2) { cout<<name1<<endl; continue; } index1=store_name(name1); index2=store_name(name2); l=last_show[index1]; r=last_show[index2]; if(l>r)//保证l<=r { int tmp=r; r=l; l=tmp; } t=floor(log(double(r-l+1))/log(2.0));//找到能使[l,r]分为两半的指数 cout<<index_name[get_min(rmq[l][t],rmq[r-(1<<t)+1][t])]<<endl;//r-(1<<t)+1,记得+1 } return 0; } [/cpp] 本代码提交AC,用时202MS,内存8MB。 相比于hihoCoder Problem 1067: 最近公共祖先·二,本题的的DFS+RMQ-ST算法是在线算法,也就是每来一个询问可以立即回答,而且复杂度为O(nlgn)还不错。 至此,hihoCoder的最近公共祖先问题完整结束,共使用三种算法,后两种算法都利用DFS,阅读时可以互相参照:最近公共祖先问题]]>

hihoCoder 1068-RMQ-ST算法

hihoCoder 1068-RMQ-ST算法 #1068 : RMQ-ST算法 时间限制:10000ms 单点时限:1000ms 内存限制:256MB 描述 小Hi和小Ho在美国旅行了相当长的一段时间之后,终于准备要回国啦!而在回国之前,他们准备去超市采购一些当地特产——比如汉堡(大雾)之类的回国。 但等到了超市之后,小Hi和小Ho发现者超市拥有的商品种类实在太多了——他们实在看不过来了!于是小Hi决定向小Ho委派一个任务:假设整个货架上从左到右拜访了N种商品,并且依次标号为1到N,每次小Hi都给出一段区间[L, R],小Ho要做的是选出标号在这个区间内的所有商品重量最轻的一种,并且告诉小Hi这个商品的重量,于是他们就可以毫不费劲的买上一大堆东西了——多么可悲的选择困难症患者。 (虽然说每次给出的区间仍然要小Hi来进行决定——但是小Hi最终机智的选择了使用随机数生成这些区间!但是为什么小Hi不直接使用随机数生成购物清单呢?——问那么多做什么!) 提示一:二分法是宇宙至强之法!(真的么?) 提示二:线段树不也是二分法么? 输入 每个测试点(输入文件)有且仅有一组测试数据。 每组测试数据的第1行为一个整数N,意义如前文所述。 每组测试数据的第2行为N个整数,分别描述每种商品的重量,其中第i个整数表示标号为i的商品的重量weight_i。 每组测试数据的第3行为一个整数Q,表示小Hi总共询问的次数。 每组测试数据的第N+4~N+Q+3行,每行分别描述一个询问,其中第N+i+3行为两个整数Li, Ri,表示小Hi询问的一个区间[Li, Ri]。 对于100%的数据,满足N<=10^6,Q<=10^6, 1<=Li<=Ri<=N,0<weight_i<=10^4。 输出 对于每组测试数据,对于每个小Hi的询问,按照在输入中出现的顺序,各输出一行,表示查询的结果:标号在区间[Li, Ri]中的所有商品中重量最轻的商品的重量。 样例输入 10 7334 1556 8286 1640 2699 4807 8068 981 4120 2179 5 3 4 2 8 2 4 6 8 7 10 样例输出 1640 981 1556 981 981


给出n个数字,连续q次问[l,r]区间的最小值是多少。如果每询问一次都用遍历的方式求最小值,则总的时间复杂度为O(nq)。 本题给出了一个新的算法–RMQ-ST算法,它的时间复杂度为O(nlogn+q),如果q很大,比如接近n时,RMQ-ST算法就有优势了。 RMQ-ST算法的基本思想是先计算某个小区间a的最小值,当需要计算大区间b的最小值时,把它分成两个小区间a1,a2,因为小区间的最小值之前已经计算出来了,也就是a1,a2已知,则b=min{a1,a2}。这样每次询问[l,r]区间的最小值时,我们也只需要把[l,r]分成两个[l,t]和[t+1,r]区间,取这两个区间最小值的最小值,这样的时间复杂度是O(1)。 所以关键就在于怎么求所有小区间的最小值。 根据提示,为了简便,我们只求2的幂的长度区间的最小值,设数组int pre_calc[i][j]为区间[i,i+2^j-1]的最小值(从i开始,长度为2^j的区间最小值),因为长度为1的区间的最小值就是其本身,所以有pre_calc[i][0]=weight[i]。 因为pre_calc[i][j]的长度为2^j,所以我们正好可以将其分成2个长度为2^(j-1)的区间,也就是说pre_calc[i][j]=min{pre_calc[i][j-1],pre[i+2^(j-1)][j-1]}。 这样我们就找到了pre_calc的递推公式(实质是DP里的状态转换方程),那么j的取值范围是怎样的呢?因为长度为n的数组其最大子区间长度就是n,所以j的最大值就是log(n),下面只需一个二重循环就可求解。 当来了一个询问[l,r]时,我们也需要将其分成两个长度为2的幂的子区间a和b,假设这个幂为t,为了使两个子区间完全覆盖[l,r],两个子区间甚至可以重叠,所以a的右边缘需要大于等于b的左边缘,即l+2^t>=r-2^t+1,得到t>=log(r-l+1)-1,所以我们可以取t=log(r-l+1)。 完整代码如下: [cpp] #include<stdio.h> #include<math.h> #define MAX_N 1000010 int n,q; int weight[MAX_N]; int pre_calc[MAX_N][30]; //求a,b最小值 int get_min(int a,int b) { return a<b?a:b; } //初始化 void init_pre() { for(int j=1;j<=n;j++) pre_calc[j][0]=weight[j]; int m=floor(log((double)n)/log(2.0));//指数最大值 for(int i=1;i<=m;i++) { for(int j=n;j>0;j–) { pre_calc[j][i]=pre_calc[j][i-1]; if(j+(1<<(i-1))<=n)//如果另一半也存在的话。//1<<(i-1)其实就是2^(i-1) pre_calc[j][i]=get_min(pre_calc[j][i],pre_calc[j+(1<<(i-1))][i-1]); } } } int main() { //freopen("input.txt","r",stdin); scanf("%d",&n); for(int i=1;i<=n;i++) scanf("%d",&weight[i]); init_pre(); scanf("%d",&q); int l,r,t; while(q–) { scanf("%d%d",&l,&r); t=floor(log(double(r-l+1))/log(2.0));//找到能使[l,r]分为两半的指数 printf("%d\n",get_min(pre_calc[l][t],pre_calc[r-(1<<t)+1][t])); } return 0; } [/cpp] 代码中求pre_calc时需要注意二重循环的顺序不要搞反了,因为我们是先求得小区间的最小值才能求大区间的最小值,所以第一重循环应该是区间的长度,而不是区间的起点。 本代码提交AC,用时875MS,内存138MB。(用cin,cout超时。。。) 这篇文章介绍了用RMQ-ST算法求解区间最大值和最小值的方法。]]>

hihoCoder 1067-最近公共祖先·二

hihoCoder 1067-最近公共祖先·二 #1067 : 最近公共祖先·二 时间限制: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^5,M<=10^5, 且数据中所有涉及的人物中不存在两个名字相同的人(即姓名唯一的确定了一个人),所有询问中出现过的名字均在之前所描述的N对父子关系中出现过,第一个出现的名字所确定的人是其他所有人的公共祖先。 输出 对于每组测试数据,对于每个小Hi的询问,按照在输入中出现的顺序,各输出一行,表示查询的结果:他们的所有共同祖先中辈分最低的一个人的名字。 样例输入 4 Adam Sam Sam Joey Sam Micheal Adam Kevin 3 Sam Sam Adam Sam Micheal Kevin 样例输出 Sam Adam Adam


hihoCoder确实是个练习算法的好网站,之前我们写过朴素的最近公共祖先问题:hihoCoder Problem 1062: 最近公共祖先·一,但那是一个效率比较低的方法,这一次,hihoCoder详细给我们介绍了最近公共祖先的离线算法,而且讲解非常详细。 网站上的讲解详细,易于理解,我这里简单概括一下。所谓离线算法就是先把询问搜集起来,如果某个询问能先回答,则记录答案,直到把所有询问都解答了;与之相对的是在线算法,也就是每提出一个询问,我都要实时的马上回答。 离线算法综合了DFS和并查集。给定一个族谱(树形结构),我们先把每一个节点染成白色,并且在初始并查集中,每个点都是一个独立的集合,然后从树根开始深度优先遍历,当第一次经过某个节点a的时候,把它由白色改为灰色;如果第二次访问该节点的时候(也就是离开该点的时候),则把它由灰色改为黑色。 当a每次由白变灰之后,查看询问中是否出现过该节点,如果出现了,则查看询问中另一个节点b的颜色,如果b是灰色,则<a,b>的最近公共祖先就是b,因为b是灰色的,所以访问a之前肯定访问了b,所以它们的最近公共祖先是b;如果b是黑色,此时已经离开b了,并不能直接说明a和b的关系,但是<a,b>的最近公共祖先肯定在灰色链上,所以我们只要找出b向上的第一个灰色节点,就是<a,b>的最近公共祖先;如果b是白色,则说明b还没有访问,没有关于b的信息,所以我们暂时不管这个询问,等到访问到b时,a肯定不是白色,这是可以依据前两条规则得出它们的最近公共祖先。 当a每次由灰变黑之后,我们需要在并查集中将a和a的直接父节点合并UNION,因为当a由灰变黑时,a及其子树都访问结束了,但是a的直接父节点还没有访问结束,也就是说a的直接父节点还是灰色的,这样我们就记录了a向上的第一个灰色节点,这为上面的第二种情况判断做准备。 最近公共祖先的离线算法概括起来就是上面的内容,简单易懂也很强大。但是因为算法既包含DFS,又包含并查集,对初学者来说有一定的难度,而且这个题给出的数据是字符串形式的,数据量又很大,有很多细节需要注意。 我先把完整的代码贴出来,然后讲一下主要过程。 [cpp] #include<iostream> #include<string> #include<map> #include<vector> using namespace std; const int MAX_N=100010;//最大人数 map<string,int> name_index;//名字转换为数字 vector<string> index_name(MAX_N);//数字转换为名字 int color[MAX_N]={0};//0:白;1:灰;2:黑,初始颜色都是白色 int s_f[MAX_N];//son_father。s_f[i]表示第i个儿子的直接父亲 vector<vector<int> > f_s(MAX_N);//f_s[i]表示第i个父亲的所有孩子 vector<vector<int> > show_at(MAX_N);//show_at[i]表示第i个人在哪些询问里出现过 int q_l[MAX_N];//第i个询问的左边 int q_r[MAX_N];//第i个询问的右边 int father[MAX_N];//用于并查集,和s_f不一样 vector<string> ans;//最终结果 //保存某个人的信息,并返回其下标 int store_name(string name) { map<string,int>::iterator it=name_index.find(name); if(it==name_index.end())//还不存在 { int curr_index=name_index.size();//用当前已有人数作为其下标,正好是递增的。 name_index.insert(make_pair(name,curr_index)); index_name[curr_index]=name;//记录 father[curr_index]=curr_index;//初始的时候,并查集中每个元素都是一个独立的集合 return curr_index;//返回下标 } else return it->second;//已经存在,直接返回 } //并查集FIND操作 int find_father(int name) { return name==father[name]?name:(father[name]=find_father(father[name])); } //并查集UNION操作 void union_father(int name1,int name2) { father[find_father(name1)]=find_father(name2); } //深度遍历族谱树 void DFS(int root) { color[root]=1;//首先修改颜色为灰色 int show_time=show_at[root].size();//查看该元素是否在询问里出现 if(show_time!=0) { for(int i=0;i<show_time;i++)//判断所有询问 { int column=show_at[root][i]; int left=q_l[column];//找到该询问的左右元素 int right=q_r[column]; if(left==right)//如果是左右相同,则该自身即是其最近公共祖先 ans[column]=index_name[left]; else { if(left==root)//判断当前访问的是左边的还是右边的 { if(color[right]==1)//另一个点为灰色 ans[column]=index_name[right];//结果即为该灰色点 else if(color[right]=2)//另一个点为黑色 ans[column]=index_name[find_father(right)];//找其向上最近的灰色节点 } else { if(color[left]==1)//另一个点为灰色 ans[column]=index_name[left]; else if(color[left]=2)//另一个点为黑色 ans[column]=index_name[find_father(left)]; } } } } int sons=f_s[root].size();//查看是否有孩子节点 if(sons!=0) { for(int i=0;i<sons;i++) DFS(f_s[root][i]);//深度遍历其所有孩子 } color[root]=2;//改颜色为黑色 union_father(root,s_f[root]);//和直接父节点UNION } int main() { ios_base::sync_with_stdio(false);//取消同步,加速 cin.tie(0); //freopen("input.txt","r",stdin); int n,m; string name1,name2; int index1,index2; cin>>n; while(n–) { cin>>name1>>name2; index1=store_name(name1); index2=store_name(name2); f_s[index1].push_back(index2); s_f[index2]=index1; } cin>>m; ans.resize(m); for(int i=0;i<m;i++) { cin>>name1>>name2; index1=store_name(name1); index2=store_name(name2); q_l[i]=index1; q_r[i]=index2; show_at[index1].push_back(i); show_at[index2].push_back(i); } DFS(0); for(int i=0;i<m;i++) cout<<ans[i]<<endl; return 0; } [/cpp] 摆在我们面前的第一个问题就是数据的输入问题,题目给的每个人名都是字符串,当然我们可以直接用字符串构建一个树结构,然后进行查找、比较、赋值等操作,但是这样的时空效率都比较低,所以我们可以先用map<string,int> name_index和vector<string> index_name来记录人名字符串和人名数字下标,这样我们在DFS+并查集的时候可以只使用数字下标,到最后输出的时候再转换为字符串,提高效率。记录并返回人名数字下标的操作在函数int store_name(string name);中。 我们还需要记录父子关系,因为一个父亲可以有多个孩子,所以父亲-孩子关系需要用二维数组vector<vector<int> > f_s(MAX_N);来表示;而一个孩子只可能有一个父亲,所以孩子-父亲关系可以用一维数组int s_f[MAX_N];来表示。 还有两个数组需要初始化,每个节点的颜色初始化为白色,在并查集中每个节点单独为一个集合。 然后就是存储询问了。正如前面对LCA离线算法的描述,每当一个节点从白色变为灰色时,我们都要查看询问中是否出现过该节点。输入的询问中有很多节点,怎样来快速判断所有询问中是否有某个节点以及该询问中的另一个节点呢?如果这个问题处理不好,很可能就是TLE,反正我前两个版本都是因为这个原因TLE的。 这个问题可以通过两个步骤来解决,都是用空间来换时间。因为每一个询问都是由左右两个人名组成的,那么我们先把每次询问的左右两个人名记录在int q_l[MAX_N];和int q_r[MAX_N];中。然后还用一个二维数组vector<vector<int> > show_at(MAX_N);来记录某一个节点在哪些询问里出现过。这样当我们DFS到某个节点a时,首先查看show_at[a]这个数组是否为空,如果为空,说明a没有出现在任何一次询问中;如果不为空,则show_at[a]记录了a出现的所有询问的序号,根据这个序号,我们再去查q_l和q_r,这样就可以取出a出现时的那个询问的左右两个人名是什么了。所以整个过程只需要O(1)的时间,大大提高了时间效率。 因为题目中说明了
第一个出现的名字所确定的人是其他所有人的公共祖先。
所以我们以第一个人名下标0来DFS树结构。DFS的过程和上面的算法描述一致,我这里就不多说了,代码中都有详细的注释。 还有一个小问题需要注意,因为编译器不一样,在用vector声明二维数组的时候,我在VS2010下可以这样: [cpp] vector<vector<int>> a; //sth [/cpp] 但是提交之后会报Compile Error的错误:error: ‘>>’ should be ‘> >’ within a nested template argument list,也就是说这个编译器把>>看成了移位操作符,所以我们在两个箭头之间加一个空格> >这样就没问题了。 我上面的代码版本提交后AC,用时711MS,内存30MB。这是我迄今为止内存消耗最大的一次AC。 总的来说,这个题是好题,很考验选手的综合能力,特别是本题用到的各种数据结构较多,需要全盘考虑,不断优化。]]>

hihoCoder 1066-无间道之并查集

hihoCoder 1066-无间道之并查集 #1066 : 无间道之并查集 时间限制:20000ms 单点时限:1000ms 内存限制:256MB 描述 这天天气晴朗、阳光明媚、鸟语花香,空气中弥漫着春天的气息……额,说远了,总之,小Hi和小Ho决定趁着这朗朗春光出去玩。 但是刚刚离开居住的宾馆不久,抄近道不小心走入了一条偏僻小道的小Hi和小Ho就发现自己的前方走来了几个彪形大汉,定睛一看还都是地地道道的黑人兄弟!小Hi和小Ho这下就慌了神,捡肥皂事小,这一身百把来斤别一不小心葬身他乡可就没处说去了。 就在两人正举足无措之时,为首的黑叔叔从怀里掏出了一件东西——两张花花绿绿的纸,分别递给了小Hi和小Ho。 小Hi和小Ho接过来,只见上面写道(译为中文):“本地最大的帮派——青龙帮,诚邀您的加入!”下面还详细的列出了加入青龙帮的种种好处。 于是两人略感心安,在同黑叔叔们交谈一番之后,已是均感相见恨晚。同时,在小Hi和小Ho表示自己不日便将回国之后,黑叔叔们也没有再提加入帮派之事,但是那为首的黑叔叔思索一会,开口道(译为中文):“我现在有一个难题,思索了很久也没法子解决,既然你们俩都是高材生,不如来帮我看看。” 小Hi和小Ho点了点头表示没问题,于是黑叔叔继续说道:“这个问题是这样的,我们帮派最近混进了许多警察的卧底,但是在我们的调查过程中只能够知道诸如‘某人和另一个人是同阵营的’这样的信息,虽然没有办法知道他们具体是哪个阵营的,但是这样的信息也是很重要的,因为我们经常会想要知道某两个人究竟是不是同一阵营的。” 小Hi和小Ho赞同的点了点头,毕竟无间道也都是他们看过的。 黑叔叔接着说道:“于是现在问题就来了,我希望你们能写出这样一个程序,我会有两种操作,一种是告诉它哪两个人是同一阵营的,而另一种是询问某两个人是不是同一阵营的……既然你们就要回国了,不如现在就去我们帮派的总部写好这个程序再走把。” 为了生命安全与……小Hi和小Ho都不得不解决这个问题!那么他们究竟从何下手呢? 提示:说起来其实就是不断的合并集合嘛~ 输入 每个测试点(输入文件)有且仅有一组测试数据。 每组测试数据的第1行为一个整数N,表示黑叔叔总共进行的操作次数。 每组测试数据的第2~N+1行,每行分别描述黑叔叔的一次操作,其中第i+1行为一个整数op_i和两个由大小写字母组成的字符串Name1_i, Name2_i,其中op_i只可能为0或1,当op_i=0时,表示黑叔叔判定Name1_i和Name2_i是同一阵营的,当op_i=1时,表示黑叔叔希望知道Name1_i和Name2_i是否为同一阵营的。 对于100%的数据,满足N<=10^5, 且数据中所有涉及的人物中不存在两个名字相同的人(即姓名唯一的确定了一个人),对于所有的i,满足Name1_i和Name2_i是不同的两个人。 输出 对于每组测试数据,对于黑叔叔每次op_i=1的操作,输出一行,表示查询的结果:如果根据已知信息(即这次操作之前的所有op_i=0的操作),可以判定询问中的两个人是同一阵营的,则输出yes,否则输出no。 样例输入 10 0 Steven David 0 Lcch Dzx 1 Lcch Dzx 1 David Dzx 0 Lcch David 0 Frank Dzx 1 Steven Dzx 1 Frank David 0 Steven Dzx 0 Dzx Frank 样例输出 yes no yes yes


这是一道考察并查集的好题目。并查集也叫做不相交集。它主要有两个操作:FIND和UNION。 假设有两个不相交集分别为A,B,用各自集合内的元素a和b分别代表A和B,也就是说A中所有元素(包括a自己)的祖先都是a,B中所有元素(包括b自己)的祖先都是b。FIND操作就是给定一个元素c,要找到它的祖先。常规思路就是递归的顺着族谱往上找,但是为了后续查找的高效,可以路径压缩一下,也就是把查找过程中的节点的祖先都赋为c的祖先,这样下次查找其他元素的祖先时就快得多。 UNION操作就是已知元素c和d,要把这两个元素所在的集合合并起来,自然就是先用FIND找到c和d的祖先,然后把其中一个祖先当做另一个祖先的祖先,这样就把两个集合合并起来了。 至于这道题,题目中的提示已经把并查集的FIND函数写出来了。 如果op_i=0,说明这两个人是同一个集合的,即他们有共同的祖先,则首先判断这两个人在不在map中,如果不在,则以自身为祖先加入到map中,然后执行UNION操作把他们合并为一个集合。 如果op_i=1,要判断两个人是否在一个集合中,想当然的FIND他们的祖先,如果祖先相同,说明他们是一个集合的。 理清了上述关系,马上写出代码: [cpp] #include<iostream> #include<string> #include<map> using namespace std; map<string,string> represent; //并查集FIND操作 string find_represent(string name) { if(name==represent[name]) return name; else { represent[name]=find_represent(represent[name]);//把经过的节点全部链接到根节点 return represent[name]; } } int main() { //freopen("input.txt","r",stdin); int n; int op; string name1,name2; cin>>n; while(n–) { cin>>op>>name1>>name2; if(op==0) { if(represent.find(name1)==represent.end()) represent[name1]=name1; if(represent.find(name2)==represent.end()) represent[name2]=name2; represent[find_represent(name1)]=find_represent(name2);//UNION操作 } else { //**********也需要先判断是否在map里********* if(represent.find(name1)==represent.end()) represent[name1]=name1; if(represent.find(name2)==represent.end()) represent[name2]=name2; //****************************************** if(find_represent(name1)==find_represent(name2)) cout<<"yes"<<endl; else cout<<"no"<<endl; } } return 0; } [/cpp] 本代码提交AC,用时243MS,内存1MB。 需要注意的是,由于本题使用MAP实现,在执行FIND操作时有这么一句: [cpp] if(name==represent[name]) [/cpp] 对于STL的map,如果本来map中不存在a这个元素,执行了if(map[a]==”sth”)之后,也会自动把a加入map,最后就是map[a]=””,为空。虽然就某一次来说,这样判断的结果和下面修正的结果是一样的,但是如果下一次op_i=0需要加入a时发现map中已经有a了,就直接执行UNION操作了,但是这个时候a的祖先却是空””,而本来a的祖先应该是它自己a的。所以这就会对后面的结果产生影响。所以当我们op_i=1时,如果某个人不在map中,我们也要把它加入map并使其祖先为自身。 关于这道题,这篇题解写得还可以,它把并查集的UNION和FIND分开来写,条理清晰,值得参考,不过它没有路径压缩。 之前一直对并查集心存恐惧,刷过这道题之后,发现并查集也就这么回事,代码简洁,也不难理解。继续坚持!]]>

hihoCoder 1037-数字三角形

hihoCoder 1037-数字三角形 #1037 : 数字三角形 时间限制:10000ms 单点时限:1000ms 内存限制:256MB 问题描述 小Hi和小Ho在经历了螃蟹先生的任务之后被奖励了一次出国旅游的机会,于是他们来到了大洋彼岸的美国。美国人民的生活非常有意思,经常会有形形色色、奇奇怪怪的活动举办,这不,小Hi和小Ho刚刚下飞机,就赶上了当地的迷宫节活动。迷宫节里展览出来的迷宫都特别的有意思,但是小Ho却相中了一个其实并不怎么像迷宫的迷宫——因为这个迷宫的奖励非常丰富~ 于是小Ho找到了小Hi,让小Hi帮助他获取尽可能多的奖品,小Hi把手一伸道:“迷宫的介绍拿来!” 小Ho选择的迷宫是一个被称为“数字三角形”的n(n不超过200)层迷宫,这个迷宫的第i层有i个房间,分别编号为1..i。除去最后一层的房间,每一个房间都会有一些通往下一层的房间的楼梯,用符号来表示的话,就是从第i层的编号为j的房间出发会有两条路,一条通向第i+1层的编号为j的房间,另一条会通向第i+1层的编号为j+1的房间,而最后一层的所有房间都只有一条离开迷宫的道路。这样的道路都是单向的,也就是说当沿着这些道路前往下一层的房间或者离开迷宫之后,小Ho没有办法再次回到这个房间。迷宫里同时只会有一个参与者,而在每个参与者进入这个迷宫的时候,每个房间里都会生成一定数量的奖券,这些奖券可以在通过迷宫之后兑换各种奖品。小Ho的起点在第1层的编号为1的房间,现在小Ho悄悄向其他参与者弄清楚了每个房间里的奖券数量,希望小Hi帮他计算出他最多能获得多少奖券。 提示一:盲目贪心不可取,搜索计算太耗时 提示二:记忆深搜逞神威,宽度优先解难题 提示三:总结归纳提公式,减少冗余是真理 输入 每个测试点(输入文件)有且仅有一组测试数据。 每组测试数据的第一行为一个正整数n,表示这个迷宫的层数。 接下来的n行描述这个迷宫中每个房间的奖券数,其中第i行的第j个数代表着迷宫第i层的编号为j的房间中的奖券数量。 测试数据保证,有100%的数据满足n不超过100 对于100%的数据,迷宫的层数n不超过100 对于100%的数据,每个房间中的奖券数不超过1000 对于50%的数据,迷宫的层数不超过15(小Ho表示2^15才3万多呢,也就是说……) 对于10%的数据,迷宫的层数不超过1(小Hi很好奇你的边界情况处理的如何?~) 对于10%的数据,迷宫的构造满足:对于90%以上的结点,左边道路通向的房间中的奖券数比右边道路通向的房间中的奖券数要多。 对于10%的数据,迷宫的构造满足:对于90%以上的结点,左边道路通向的房间中的奖券数比右边道路通向的房间中的奖券数要少。 输出 对于每组测试数据,输出一个整数Ans,表示小Ho可以获得的最多奖券数。 样例输入 5 2 6 4 1 2 8 4 0 9 6 6 5 5 3 6 样例输出 28


简单的动态规划问题,根据题目的描述:
从第i层的编号为j的房间出发会有两条路,一条通向第i+1层的编号为j的房间,另一条会通向第i+1层的编号为j+1的房间
假设best[i][j]为到达第i层第j个房间时能获得的最大奖券数,则有状态转换方程: $$best[i][j]=max \{ best[i-1][j],best[i-1][j-1] \} +reward[i][j] $$ 然后求最后一层的最大值,就是最终能够获取到的最大奖券数。 完整代码如下: [cpp] #include<iostream> using namespace std; int n; int reward[101][101];//reward[i][j]表示第i层第j个房间的奖券数 int best[101];//best[j]表示到达第i层第j个房间时能获取到的最大奖券数 int main() { //freopen("input.txt","r",stdin); cin>>n; for(int i=0;i<n;i++) for(int j=0;j<=i;j++) cin>>reward[i][j]; best[0]=reward[0][0]; for(int i=1;i<n;i++) { for(int j=i;j>=0;j–)//从右往左填表,这样只需要一行空间 { if(j==0) best[j]+=reward[i][j]; else { best[j]=(best[j]>best[j-1]?best[j]:best[j-1])+reward[i][j]; } } } int ans=0; for(int i=0;i<n;i++) if(best[i]>ans)//求最大值 ans=best[i]; cout<<ans; return 0; } [/cpp] 本代码提交AC,用时1MS,内存0MB。]]>