楼主: charleszhou
跳转到指定楼层
上一主题 下一主题
收起左侧

[其他] 开个帖子记录自己刷挑战程序竞赛的历程

🔗
 楼主| charleszhou 2019-2-28 07:25:12 | 只看该作者
全局:
POJ 1182: 食物链

现在有N只动物分成3类: A, B, C.现在知道A吃B, B吃C, C吃A。现在有两种说:
(1) "1 X Y",表示X和Y是同类。  
(2) "2 X Y",表示X吃Y。

现在给K句话,这K句话有的是真的,有的是假的。求所有假话的个数。

思路:这真是个有趣的题目。具体做法是照抄书上的。如果关系仅仅只有是否同类的话那就是个简单的并查集问题了,不过怎么处理x吃y的关系呢?有一种脑洞大开的方案是我们对每只动物定义三种关系i-A表示i属于A类,i-B表示i属于B类,i-C表示i属于C类.这样如果两种动物i,j属于同一类,我们合并(i-A, j-A), (i-B, j-B), (i-C, j-C), 同样如果是i吃j,我们合并(i-A, j-B), (i-B, j-C), (i-C, j-A)。

然而这里的关键是如何判断冲突,也是我屡屡出错的一个地方。如果i,j属于同一类,那么一旦我们发现(i-A, j-B)是一类,或者(i-A, j-C)是一类,那么明显出现了冲突。注意没必要去判断(i-B, j-C)之类,因为它一定和(i-A, j-B)相同。但是(i-A, j-C)确是一个容易被遗漏的点。如果不判断这个那么我们无从判断1吃2,2吃3,3和1属于同一类这种错误!

同样,如果i吃j,我们发现(i-A, j-A)或者(i-A, j-C),那么也发生了冲突-前者对应i,j同一类,而后者对应j吃i。这里(i-A, j-C)也是非常容易被遗忘的corner case啊。

要点:并查集并不是只能处理A-B是否在同一集合的信息,也可以用来处理更加复杂的关系-比如这里用来处理 A吃B 这样。

POJ 2236: Wireless Network

一个由N个电脑组成的无线网络瘫痪了。我们可以挨个修复每个电脑。给出这N个电脑的位置,注意每个电脑只能和范围D内的电脑通信。在任何一个时刻,可以执行两种操作:维修一台电脑,或测试两台电脑是否能够通信。问每个测试结果是success还是fail.

思路:非常直截了当的思路啊, 无脑union-find即可。我尝试了两个优化:第一是把所有已经修复的电脑加入某vector,在检查附近的电脑的时候只从这个vector里面挑,实际测试性能影响不大。第二个优化是注意到 input query 的长度远远大于这个N,因此先预处理一下把所有电脑距离为D之内的电脑先存下来。这样虽然初始化的复杂度是O(N^2), 但是在unite的时候就块多了, 实际测试用时从2s降到了1.5s.

POJ 1703: Find them, Catch them

警方决定捣毁两大犯罪团伙:龙帮和蛇帮,显然一个帮派至少有一人。该城有N个罪犯,编号从1至N。将有M次操作。操作分两种:
(1) D a b 表示a、b是不同帮派
(2) A a b 询问a、b关系

对于每一个A操作,回答"In the same gang."或"In different gangs." 或"Not sure yet."

思路:看了挺难,不过显然和食物链是一样的。因为我们无法判断a, b到底是属于哪个帮派的所以要定义i-A表示i属于A帮派,用i-B表示i属于B帮派。如果来了D操作我们合并(a-A,  b-B) 以及 (a-B,  b-A)。询问a,b关系的时候我们检查,如果 (a-A,  b-A) 一类,则同一个帮派,如果 (a-A,  b-B) 一类,则不同帮派,否则则不确定。

Aizu 2170: Marked Ancestor

一棵树,N个节点,编号1-N, 两种操作:
(1) mark 某节点
(2) 打印某节点最近的被mark的祖先节点

给Q个query, 返回所有打印出来的节点的编号总和。

思路: 这题实在是简单。最brute force的方法就能过,跟并查集都没什么关系。一个节省空间的优化是我们没必要用covered数组-我们在标记某个节点的时候,可以直接将其父亲设置为自己,相当于把这个子树给分离出来。这个思路实在是精妙

回复

使用道具 举报

🔗
amykk 2019-2-28 15:04:03 | 只看该作者
全局:
加油楼主,思路很清晰,长期关注~
回复

使用道具 举报

🔗
Neroldy 2019-2-28 18:40:39 | 只看该作者
全局:
个人认为这本书还是不错的,推荐读一读,起码到3.2为止。
回复

使用道具 举报

全局:
如果不是在校生,挑战成绩竞赛收益不大吧?而且竞赛级别的题目离工作应用差太多了
回复

使用道具 举报

🔗
 楼主| charleszhou 2019-3-1 04:19:47 | 只看该作者
全局:
amykk 发表于 2019-2-28 15:04
加油楼主,思路很清晰,长期关注~

多谢多谢~
回复

使用道具 举报

🔗
 楼主| charleszhou 2019-3-1 07:42:18 来自APP | 只看该作者
全局:
Neroldy 发表于 2019/02/28 18:40:39
个人认为这本书还是不错的,推荐读一读,起码到3.2为止。

是的。。网络流什么的就随他去吧。。
回复

使用道具 举报

🔗
5668157 2019-3-1 13:00:17 | 只看该作者
全局:
贴一个网盘link:
https://pan.baidu.com/s/1TSGXjHonNnn7ahTxZzA9mg
提取码:fkiu

补充内容 (2019-3-1 13:00):
那个中文版的挑战程序设计竞赛扫描版
回复

使用道具 举报

🔗
 楼主| charleszhou 2019-3-2 00:20:37 | 只看该作者
全局:
本帖最后由 14417335 于 2019-3-2 00:47 编辑

今天的重点在于复习各种图论算法。一般来说我不会在总结里面写code但是这一章是个例外,因为里面涉及到的算法我实在太不熟了。所以写下code以备随时查阅。我们统一输入, 假设给出某有向带权图d所有边vector<vector<int>> edges, 每个edge用一个vector表示, 包含[u, v, w]也就是起点终点和权重, 节点总数V(编号1->V)。假设起点为A, 我们用d[ u]来表示起点A到u的最短距离。

1. 最短路径问题

所有最短路径问题假设没有负环,不然很显然不存在最短路径。

1.1 bellman-ford

解决单源,有负边图中的最短路径问题。其核心思想设d[ i]是起点A到i的最短路径,则有d[j] = min(d[ i] + w[ij]),其中i是所有和j相连的点。反过来推断,我们可以先求出从A点出发1条边能到达的最短路径,然后求2条边能到达的最短路径,直到求出 V-1 条边能到达的最短路径。因此实际上我们循环 V-1 次,对所有边进行松弛操作即可, 复杂度 O(VE)。这里所谓的松弛操作是指我们利用边 u->v 来优化 d[v]的过程 (d[v] = min(d[v], d[ u] + w[uv]).

为了判断是否存在负环,我们只需要进行V次路径松弛。如果发现最后一次某d[ i]又被更新了,那么证明存在负环(因为如果没有负环,最短路径不可能经过一个点两次,换句话说最短路径最多只有V-1个点)。

代码:

int bellman_ford() {
        int d[V+1];
        int u, v, w;
        for (int i = 1; i <= V; i++) {
            d[ i] = INT_MAX;
        }
        d[A]= 0;
        for (int i = 0; i < V-1; i++)
            for (vector<int> edge: edges) {
                u = edge[0];
                v = edge[1];
                w = edge[2];
                if (d[ u] != INT_MAX && d[v] > d[ u] + w)
                    d[v] = d[ u] + w;
            }
}

最后,注意bellman-ford的一个特例:如果给出的图是DAG的话那么我们可以先计算图的拓扑排序,然后按照拓扑排序的顺序依次进行路径松弛即可。这样的复杂度仅仅为O(E).

1.2 Dijstra 

解决单源,无负边图中的最短路径问题。仔细观察一下bellman-ford算法,可以发现其中存在一些时间浪费。比如如果d[ i]不是最短路径,自然d[ i] + w[ij]也不可能是A到j的最短路径。再比如如果d[ i]没有被更新(d[ i]已经是找到的最短路径了),下次循环我们还是会依次更新所有和i相邻的边。

为了优化,我们可以考虑假设d[ i]已经是A能出发所达到的最短路径,那么我们可以把和i相邻的边依次松弛一遍,然后就不用再考虑i了。因为在没有负边的情况下,d[ i]不可能在以后的更新过程中会变得更小。我们可以简单的在bellman-ford的基础上修改一下。我们首先需要用used数组来记住那些最短路径还没有被确定的点,然后每次松弛的时候都找 used[ i] == False 中d[ i]最小的点i。随后我们只需要松弛点i的邻居节点即可。

代码如下:

int dijstra_v1() {
        int d[V+1];
        bool used[V+1];
        int u, v, w;
        fill(used, used+N+1, false);
        fill(d, d+N+1, INT_MAX);
        d[A] = 0;
        int cost[V+1][V+1];
        for (int i = 1; i <= V; i++)
            for (int j = 1; j <= V; j++) {
                if (i == j)
                    cost[ i][j] = 0;
                else
                    cost[ i][j] = INT_MAX;
        }
        for (vector<int> edge: edges) {
            int u = edge[0];
            int v = edge[1];
            int w = edge[2];
            cost[ u][v] = w;
        }
        for (int i = 0; i < A-1; i++) {
            int v = -1;
            for (int j = 1; j <= A; j++) {
                if (!used[j] && (v == -1 || d[j] < d[v]))
                    v = j;
            }
            for (int j = 1; j <= A; j++) {
                if (cost[v][j] != INT_MAX && d[j] > d[v] + cost[v][j]) {
                    d[j] = d[v] + cost[v][j];
                }
                    
            }
            used[v] = 1;
        }
}

看似很复杂,主要是我们需要先初始化这个cost矩阵,其中cost[ i][j]记录着i到j的路径长度,INF表示i和j不直接相连。这样复杂度是O(V*V)。

注意到这里最麻烦的一个操作是找d[ i]中的最小值。为了优化这个过程我们可以把d[ i]加入最小堆中,这样的复杂度就是O(ElogV)了。这里的E是因为我们对每个边都进行了一次松弛操作。然后logV是因为堆中总元素不会超过定点总个数。

int dijstra_v2() {
        typedef pair<int, int> P;
        int d[V+1];
        fill(d, d+V+1, INT_MAX);
        d[A] = 0;
        vector<P> graph[V+1];
        for (vector<int> edge : edges) {
            graph[edge[0]].push_back(make_pair(edge[1], edge[2]));
        }
        priority_queue<P, vector<P>, greater<P> > pq;
        pq.push(make_pair(0, A));
        P node;
        while (!pq.empty()) {
            node = pq.top();
            pq.pop();
            int u = node.second;
            if (d[ u] < node.first) continue;
            res = max(res, node.first);
            cnt += 1;
            for (P neigh: graph[ u]){
                int v = neigh.first;
                int w = neigh.second;
                if (d[v] > d[ u] + w) {
                    d[v] = d[ u] + w;
                    pq.push(make_pair(d[v], v));
                }
            }
        }
}

注意一些坑,比如某个点可能不止一次被加入并且从pq中pop出来,这是因为对某点v, 我们可能找到经过u1->v的最短路径以及经过u2->v的最短路径,但是只有一条是从起点A->v的最短路径。为了解决这个问题我们在pop出某点v的时候计算下此时的最短距离是否比d[ u]要小,如果是的话忽略即可。

此外,如果有负边的话,dijstra算法是不行的。最简单的例子 A->B:1, A->C:2,C->B:-3. A->B的实际最短路径是-1。可是根据dijstra算法,第一次找到A->B的最短路径后就不会再去考虑它了,这样会得到错误的结果。

dijstra的一个特例是所有边没有权重或者所有边权重相同的情况,这种情况下显然bfs就可以了。复杂度O(E)。

1.3 Floyd-Warshall算法

多源最短路径问题,求所有点到所有点的最短路径。可以处理存在负边的情况。假设D[i,j,k]是从i到j经过1->k号中间节点的最短路径的长度。则考虑两种情况:最短路径不经过节点k,则D[i,j,k] = D[i,j,k-1], 或者最短路径经过节点k,则 D[i,j,k] = D[i,k,k-1] + D[k,j,k-1]。综合起来也就是

D[i,j,k] = min(D[i,j,k-1], D[i,k,k-1] + D[k,j,k-1])

注意k只依赖于k-1的情况,因此空间复杂度可以优化到 O(V^2),时间复杂度显然是O(V^3)。代码极其简单:

int floyd() {
        int dp[V+1][V+1];
        for (int i = 1; i <= V; i++)
            for (int j = 1; j <= V; j++)
                dp[ i][j] = i == j ? 0:INT_MAX;
        for (vector<int> edge:edges) {
            dp[edge[0]][edge[1]] = edge[2];
        }
        for (int k = 1; k <= V; k++)
            for (int i = 1; i <= V; i++)
                for (int j = 1; j <= V; j++) {
                    if (dp[ i][k] != INT_MAX && dp[k][j] != INT_MAX)
                        dp[ i][j] = min(dp[ i][j], dp[ i][k] + dp[k][j]);
                }
}

因为代码过于简单,在输入规模很小的情况下也可以考虑用 Floyd 算法来计算单源最短距离。


补充内容 (2019-3-3 06:47):
dijstra实现的坑
1. 注意初始化,节点编号1->V,初始化V+1个元素
2. if d[u ] <= node.first 则会出错,因为 u的邻居节点还没有被relax过
回复

使用道具 举报

🔗
 楼主| charleszhou 2019-3-2 00:21:55 | 只看该作者
全局:
14417335 发表于 2019-2-21 21:57
很抱歉给你带来这多麻烦。还请以后paste之前做最后的search and replace应该不花时间但是仍然是个麻烦。 ...

版主你好~不知道我又触发了什么神奇的关键字我刚发的回复突然全部被加上了下划线。。可否麻烦版主帮忙处理一下?非常感谢!
回复

使用道具 举报

🔗
 楼主| charleszhou 2019-3-2 00:22:24 | 只看该作者
全局:
5668157 发表于 2019-3-1 13:00
贴一个网盘link:
https://pan.baidu.com/s/1TSGXjHonNnn7ahTxZzA9mg
提取码:fkiu

想请教一下你们都是怎么添加补充内容的。。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册账号
隐私提醒:
  • ☑ 禁止发布广告,拉群,贴个人联系方式:找人请去🔗同学同事飞友,拉群请去🔗拉群结伴,广告请去🔗跳蚤市场,和 🔗租房广告|找室友
  • ☑ 论坛内容在发帖 30 分钟内可以编辑,过后则不能删帖。为防止被骚扰甚至人肉,不要公开留微信等联系方式,如有需求请以论坛私信方式发送。
  • ☑ 干货版块可免费使用 🔗超级匿名:面经(美国面经、中国面经、数科面经、PM面经),抖包袱(美国、中国)和录取汇报、定位选校版
  • ☑ 查阅全站 🔗各种匿名方法

本版积分规则

>
快速回复 返回顶部 返回列表