Commit fe407970 authored by 赵彦君's avatar 赵彦君 💬
Browse files

Merge remote-tracking branch 'upstream/master'

parents c414f6be 0ac81fcb
Loading
Loading
Loading
Loading
+19.1 KiB
Loading image diff...
+19.3 KiB
Loading image diff...
+66 −0
Original line number Diff line number Diff line
锦标赛排序又被称为树形选择排序,是 [选择排序](/selection-sort.md) 的优化版本,在选择排序中用 $O(n)$ 的时间来选取一个元素,而锦标赛排序用 $O(\log n)$ 的时间选取一个元素,其总时间复杂度是 $O(n\log n)$ ,需要 $O(n)$ 的辅助空间。锦标赛排序是堆排序的一种变体。

锦标赛排序算法的名字来源于一种单败淘汰制的竞赛形式,在单败淘汰制中有许多选手参与比赛,两两比较胜出的选手进入下一轮比赛。这种淘汰方式能够决定最好的选手,但是在最后一轮比赛中被淘汰的选手不一定是第二好的,他可能不如先前被淘汰的选手。

## 算法流程

![tournament-sort1](./images/tournament-sort1.png)

如图所示,这是一棵 **最小锦标赛排序树** ,待排序数据是叶子节点显示的数据。红色边显示的是每一轮比较中较小的数据的胜出路径。显然完成一次"锦标赛"可以选出一组数据中最小的那一个。

每一轮对 $n$ 个数据进行比较并且得到 $\frac{n}{2}$ 个"优胜者",每一对中较小的数据进入下一轮比较。如果无法凑齐一对,数据那么这个数据直接进入下一轮的比较。

![tournament-sort2](./images/tournament-sort2.png)

完成一次"锦标赛"后需要将被选出的那个数据去除,不妨直接将其设置为 $\infty$ ,这个操作类似我们熟悉的 [堆排序](/heap-sort.md) ,然后我们就可以再进行一次"锦标赛"选出次小的那个数据了。一直重复这个这个操作,直至所有数据有序。

## 代码实现

```cpp
int n, a[maxn], tmp[maxn << 1];

int winner(int pos1, int pos2) {
  int u = pos1 >= n ? pos1 : tmp[pos1];
  int v = pos2 >= n ? pos2 : tmp[pos2];
  if (tmp[u] <= tmp[v]) return u;
  return v;
}

void creat_tree(int &value) {
  for (int i = 0; i < n; i++) tmp[n + i] = a[i];
  for (int i = 2 * n - 1; i > 1; i -= 2) {
    int j, k = i / 2;
    if (i % 2 == 0 && i < 2 * n - 1)
      j = i + 1;
    else if (i > 1)
      j = i - 1;
    tmp[k] = winner(i, j);
  }
  value = tmp[tmp[1]];
  tmp[tmp[1]] = INF;
}

void recreat(int &value) {
  int i = tmp[1];
  while (i > 1) {
    int j, k = i / 2;
    if (i % 2 == 0 && i < 2 * n - 1)
      j = i + 1;
    else if (i > 1)
      j = i - 1;
    tmp[k] = winner(i, j);
    i = k;
  }
  value = tmp[tmp[1]];
  tmp[tmp[1]] = INF;
}

void tournament_sort() {
  int value;
  creat_tree(value);
  for (int i = 0; i < n; i++) {
    a[i] = value;
    recreat(value);
  }
}
```
+1 −1
Original line number Diff line number Diff line
@@ -125,7 +125,7 @@ APIO 和 CTS 都以省为单位报名,一般按照 NOIP 的成绩排序来确

### 美国:USACO

官网地址: <https://www.usaco.org/> 
官网地址: <http://www.usaco.org/> 

USACO 或许是国内选手最熟悉的外国 OI 竞赛(可能也是中文题解最多的外国 OI 竞赛)。

+73 −5
Original line number Diff line number Diff line
@@ -2,12 +2,12 @@

树形 DP,即在树上进行的 DP。由于树固有的递归性质,树形 DP 一般都是递归进行的。

## 例题
## 基础

以下面这道题为例,介绍一下树形 DP 的一般过程。

???+note " 例题[洛谷 P1352 没有上司的舞会](https://www.luogu.com.cn/problem/P1352)"
    某大学有 $n$ 个职员,编号为 $1\text{~} N$ 。他们之间有从属关系,也就是说他们的关系就像一棵以校长为根的树,父结点就是子结点的直接上司。现在有个周年庆宴会,宴会每邀请来一个职员都会增加一定的快乐指数 $a_i$ ,但是呢,如果某个职员的上司来参加舞会了,那么这个职员就无论如何也不肯来参加舞会了。所以,请你编程计算,邀请哪些职员可以使快乐指数最大,求最大的快乐指数。
    某大学有 $n$ 个职员,编号为 $1 \sim N$ 。他们之间有从属关系,也就是说他们的关系就像一棵以校长为根的树,父结点就是子结点的直接上司。现在有个周年庆宴会,宴会每邀请来一个职员都会增加一定的快乐指数 $a_i$ ,但是呢,如果某个职员的上司来参加舞会了,那么这个职员就无论如何也不肯来参加舞会了。所以,请你编程计算,邀请哪些职员可以使快乐指数最大,求最大的快乐指数。

我们可以定义 $f(i,0/1)$ 代表以 $i$ 为根的子树的最优解(第二维的值为 0 代表 $i$ 不参加舞会的情况,1 代表 $i$ 参加舞会的情况)。

@@ -61,10 +61,78 @@ int main() {
}
```

## 树上背包

树上的背包问题,简单来说就是背包问题与树形 DP 的结合。

???+note "例题[洛谷 P2014 CTSC1997 选课]"
    现在有 $n$ 门课程,第 $i$ 门课程的学分为 $a_i$ ,每门课程有零门或一门先修课,有先修课的课程需要先学完其先修课,才能学习该课程。
    
    一位学生要学习 $m$ 门课程,求其能获得的最多学分数。
    
     $n,m \leq 300$ 

每门课最多只有一门先修课的特点,与有根树中一个点最多只有一个父亲节点的特点类似。

因此可以想到根据这一性质建树,从而所有课程组成了一个森林的结构。为了方便起见,我们可以新增一门 $0$ 学分的课程(设这个课程的编号为 $0$ ),作为所有无先修课课程的先修课,这样我们就将森林变成了一棵以 $0$ 号课程为根的树。

我们设 $f(u,i,j)$ 表示以 $u$ 号点为根的子树中,已经遍历了 $u$ 号点的前 $i$ 棵子树,选了 $j$ 门课程的最大学分。

转移的过程结合了树形 DP 和背包 DP 的特点,我们枚举 $u$ 点的每个子节点 $v$ ,同时枚举以 $v$ 为根的子树选了几门课程,将子树的结果合并到 $u$ 上。

记点 $x$ 的儿子个数为 $s_x$ ,以 $x$ 为根的子树大小为 $\textit{siz_x}$ ,很容易写出下面的转移方程:

$$
f(u,i,j)=\max_{v,k \leq j,k \leq \textit{siz_v}} f(u,i-1,j-k)+f(v,s_v,k)
$$

注意上面转移方程中的几个限制条件,这些限制条件确保了一些无意义的状态不会被访问到。

 $f$ 的第二维可以很轻松地用滚动数组的方式省略掉,注意这时需要倒序枚举 $j$ 的值。

我们可以证明,该做法的时间复杂度为 $O(nm)$ [^note1]。

??? note "参考代码"
    ```cpp
    #include <algorithm>
    #include <cstdio>
    #include <vector>
    using namespace std;
    int f[305][305], s[305], n, m;
    vector<int> e[305];
    int dfs(int u) {
      int p = 1;
      f[u][1] = s[u];
      for (auto v : e[u]) {
        int siz = dfs(v);
        // 注意下面两重循环的上界和下界
        // 只考虑已经合并过的子树,以及选的课程数超过 m+1 的状态没有意义
        for (int i = min(p, m + 1); i; i--)
          for (int j = 1; j <= siz && i + j <= m + 1; j++)
            f[u][i + j] = max(f[u][i + j], f[u][i] + f[v][j]);
        p += siz;
      }
      return p;
    }
    int main() {
      scanf("%d%d", &n, &m);
      for (int i = 1; i <= n; i++) {
        int k;
        scanf("%d%d", &k, &s[i]);
        e[k].push_back(i);
      }
      dfs(0);
      printf("%d", f[0][m + 1]);
      return 0;
    }
    ```

## 习题

 [HDU 2196 Computer](http://acm.hdu.edu.cn/showproblem.php?pid=2196) 
-  [HDU 2196 Computer](http://acm.hdu.edu.cn/showproblem.php?pid=2196) 
-  [POJ 1463 Strategic game](http://poj.org/problem?id=1463) 
-  [POJ 3585 Accumulation Degree](http://poj.org/problem?id=3585) 

 [POJ 1463 Strategic game](http://poj.org/problem?id=1463) 
## 参考资料与注释

 [POJ 3585 Accumulation Degree](http://poj.org/problem?id=3585) 
[^note1]:  [子树合并背包类型的 dp 的复杂度证明 - LYD729 的 CSDN 博客](https://blog.csdn.net/lyd_7_29/article/details/79854245) 
Loading