APIO2016 题解

今年的题比去年的不知道高到哪里去了。。。
Orz 蛤省Cu观光团

从左至右:Menci Fancy KZ gcx miku XTT cstio dydxh mzx Satoshi lxt 图片来源于Fancy博客

Orz cstdio
Orz 韩国出题人

A. 划艇

先吐槽:这题不是叫划艇么,怎么就赛艇了?

BZOJ4584:[Apio2016]赛艇

题目

【问题描述】

在首尔城中,汉江横贯东西。在汉江的北岸,从西向东星星点点地分布着 N 个划艇学校,编号依次为1 N 。每个学校都拥有若干艘划艇。同一所学校的所有划艇颜色相同,不同的学校的划艇颜色互不相同。颜色相同的划艇被认为是一样的。每个学校可以选择派出一些划艇参加节日的庆典,也可以选择不派出任何划艇参加。如果编号为i的学校选择派出划艇参加庆典,那么,派出的划艇数量可以在 ai bi 之间任意选择( aibi )。

值得注意的是,编号为 i 的学校如果选择派出划艇参加庆典,那么它派出的划艇数量必须大于任意一所编号小于它的学校派出的划艇数量。

【任务描述】

输入所有学校的ai bi 的值,求出参加庆典的划艇有多少种可能的情况,必须有至少一艘划艇参加庆典。两种情况不同当且仅当有参加庆典的某种颜色的划艇数量不同。

【输入格式】

第一行包括一个整数 N ,表示学校的数量。

接下来N行,每行包括两个正整数,用来描述一所学校。其中第 i 行包括的两个正整数分别表示ai, bi 1aibi109 )。

【输出格式】

输出一行,一个整数,表示所有可能的派出划艇的方案数除以 1,000,000,007 得到的余数。
【样例输入】

2
1 2
2 3

【样例输出】

7

【样例说明】

在只有一所学校派出划艇的情况下有4种方案,两所学校都派出划艇的情况下有3种方案,所以答案为7。

【子任务】

子任务 1(9 分): 1N500 且对于所有的 1iN ,保证 ai=bi

子任务 2(22 分): 1N500 1iN(biai)106

子任务 3(27 分): 1N100

子任务 4(42 分): 1N500

9分算法

子任务1中一定满足 ai=bi ,因此我们这样DP:

f[i] 为最后一个取到的位置为 i 的方案数,定义f[0]=0,则

f[i]=0j<i,a[j]<a[i]f[j]

最后输出 ni=1f[i] 即可。

时间复杂度 O(n2) ,期望得分9分。

100分算法

中间的子任务没什么意思。。。估计是照顾乱搞党?不管了直接说标算。

这种输入很少,但数字范围很大的问题,往往可以离散化处理。为了方便处理 ai=bi 的情况,我们把区间初始化成左闭右开区间 [ai,bi+1) ,然后把所有的输入数据进行离散化。

比如说,输入中的所有数据排序去重后,得到一个序列 {x1,x2,,xcnt} ,那么我们把所有的 ai 值换做它在上述数组中的位置, bi 同理,这里二分查找或者map都是可以的。

然后,所有的值就都不超过 cnt 了。我们记 f[i][j] 为最后取到第 i 个学校,且在这里的取值在区间[xj,xj+1)的方案数,显然如果这个学校对应的区间不包括 [xj,xj+1) 则答案为0,同时令 f[0][0]=1,f[0][k]=0(k>0)

然后考虑怎么转移。当前要计算 f[i][j] ,那么之前取的一个有两种情况:

· 之前取的数不在区间 [xj,xj+1) 内,这显然是没有什么问题的,随便取都合法,因此这一部分给 f[i][j] 带来的贡献为

(xj+1xj)×i=0i1j=0j1f[i][j]

· 之前取的数在区间 [xj,xj+1) 内。这就很麻烦了,因为这里都是区间而不是点,因此统计方案不能直接相加,否则会出现大量不合要求的方案。解决的办法是组合学方法。

先看一个很简单的问题:在区间 [xj,xj+1) 任取两个数,使得前一个小于后一个,有几种取法?

小学数学题吧,答案是 C2xj+1xj

那如果取3个数 a,b,c 满足 a<b<c 呢?废话, C3xj+1xj .

为了方便,我们把 xj+1xj 记作 l 好了。现在考虑更难一些的问题:如果给你3个相同的区间,每个区间你可以选择选或不选,在你选出的所有区间中,每个取出一个数,并且取出的数严格递增,有几种取法?

分情况讨论一下。

· 一个区间都不选,那么有C03×C0l=1种取法。

· 选一个区间,那么有 C13×C1l=3l 种取法。

· 选两个区间,那么有 C23×C2l=l(l1)6 种取法。

· 选三个区间,那么有 C33×C3l=l(l1)(l2)6 种取法。

因此总共有 1+3l+l(l1)6+l(l+1)(l2)6 种取法。这什么玩意啊。。。有毛线规律。。。

还是用组合形式表述吧。取法总数为 C03×C0l+C13×C1l+C23×C2l+C33×C3l 种取法。根据 Ckn=Cnkn 的原则,这个式子又可以写为

C33×C0l+C23×C1l+C13×C2l+C03×C3l

是不是似曾相识?有没有在高中课本上见过?它明显是等于 C3l+3 的。这就好像在 l+3 个人(其中恰有 3 个神犇)里面选择3个,那么按照取到的神犇数量分情况计算,就得到上面的式子,不难验证等价性。

因此,上述问题稍加扩展,就是:

给你x个相同的区间( xl ),每个区间你可以选择选或不选,在你选出的所有区间中,每个取出一个数,并且取出的数严格递增,取法总数为 Cxl+x

好了,这个重要结论得到之后,我们回到问题。不是要求 f[i][j] 吗?我们枚举在之前第一次取到区间 [xj,xj+1) 的学校 k ,为了保证不重不漏,这个学校之前的学校都不能取到这个区间,它们的取法总数为

(xj+1xj)×i=0k1j=0j1f[i][j]

然后在 k i的这些学校里,要取某个学校,那么它的取值必然也在这个区间,这就转化为了上面的结论,但是还有一点不同,就是学校 k 和学校i必须选。其实这样会使得式子变成(在这里为了方便,定义 Cmn=0(n<m)

C0ik1×C0l+C1ik1×C1l++Clik1×Cll

只要稍作代换,不难发现它的值等于 Cik1ik+l1 。这样,通过枚举 k ,就可以计算出所要求的结果。归纳一下,这一部分造成的贡献是

i=1i1Cii1ii+l1×k=1i1j=1j1f[k][j]

似乎很完美?但一看这式子就知道复杂度是 O(n5) 的。。。

不难发现,凡是出现二维前缀和的地方,都是矩形左上角一块的所有数之和,那么再维护一下二维前缀和就好了,时间复杂度降至 O(n3)

还有一个问题,就是组合数的计算。显然直接按照定义是不行的,最好根据杨辉三角什么的先预处理出来组合数表,当然也可以利用递推式 Cm1n1=Cmn×mn 在操作中计算,但是这样还要预处理乘法逆元。

当然还有别的优化(据说可以FFT?蒟蒻表示并看不出来),不过到这里已经足够了,时间复杂度 O(n3) ,期望得分100分。

#include <cstdio>
#include <set>
#include <algorithm>
using namespace std;
#define Menci 1000000007
//Let's Orz Menci Together
int n;
int inv[501];
int a[501], b[501];
set<int> nums;
int cnt, get[1002];
int l[1002];
int ans[501][1002];
int main()
{
    scanf("%d", &n);
    inv[1] = 1;
    for (int i = 2; i <= 500; i++)
        inv[i] = (long long)(Menci - Menci / i) * inv[Menci % i] % Menci;
    for (int i = 1; i <= n; i++)
    {
        scanf("%d%d", &a[i], &b[i]);
        if (!nums.count(a[i]))
            nums.insert(a[i]);
        if (!nums.count(b[i] + 1))
            nums.insert(b[i] + 1);
    }
    for (set<int>::iterator it = nums.begin(); it != nums.end(); it++)
        get[cnt++] = *it;
    for (int i = 1; i < cnt; i++)
        l[i] = get[i] - get[i - 1];
    for (int i = 1; i <= n; i++)
    {
        a[i] = upper_bound(get, get + cnt, a[i]) - get;
        b[i] = upper_bound(get, get + cnt, b[i]) - get;
    }
    for (int i = 0; i < cnt; i++)
        ans[0][i] = 1;
    for (int i = 1; i <= n; i++)
    {
        ans[i][0] = 1;
        for (int j = a[i]; j <= b[i]; j++)
        {
            ans[i][j] = ((long long)ans[i - 1][j - 1] * l[j]) % Menci;
            int now = 1, c = l[j] - 1;
            for (int k = i - 1; k; k--)
            {
                if (a[k] <= j && j <= b[k])
                {
                    now++;
                    c = (long long)c * (l[j] + now - 2) % Menci * inv[now] % Menci;
                    if (!c)
                        break;
                    ans[i][j] = (ans[i][j] + (long long)ans[k - 1][j - 1] * c) % Menci;
                }
            }
        }
        for (int j = 1; j < cnt; j++)
            ans[i][j] = ((long long)ans[i][j] + ans[i - 1][j] + ans[i][j - 1] - ans[i - 1][j - 1] + Menci) % Menci;
    }
    printf("%d\n", (ans[n][cnt - 1] - 1 + Menci) % Menci);
    return 0;
}

B. 烟花表演

题目

【问题描述】

烟花表演是最引人注目的节日活动之一。在表演中,所有的烟花必须同时爆炸。为了确保安全,烟花被安置在远离开关的位置上,通过一些导火索与开关相连。导火索的连接方式形成一棵树,烟花是树叶,如[图1]所示。火花从开关出发,沿导火索移动。每当火花抵达一个分叉点时,它会扩散到与之相连的所有导火索,继续燃烧。导火索燃烧的速度是一个固定常数。[图1]展示了六枚烟花{ E1,E2,,E6 }的连线布局,以及每根导火索的长度。图中还标注了当在时刻 0 从开关点燃火花时,每一发烟花的爆炸时间。


图1

Hyunmin为烟花表演设计了导火索的连线布局。不幸的是,在他设计的布局中,烟花不一定同时爆炸。我们希望修改一些导火索的长度,让所有烟花在同一时刻爆炸。例如,为了让[图1]中的所有烟花在时刻13爆炸,我们可以像[图2]中左边那样调整导火索长度。类似地,为了让[图1]中的所有烟花在时刻 14 爆炸,我们可以像[图2]中右边那样调整长度。


土2

修改导火索长度的代价等于修改前后长度之差的绝对值。例如,将[图1]中布局修改为[图2]左边布局的总代价为 6 ,而将[图1]中布局修改为[图2]右边布局的总代价为5.

导火索的长度可以被减为 0 ,同时保持连通性不变。

给定一个导火索的连线布局,你需要编写一个程序,去调整导火索长度,让所有的烟花在同一时刻爆炸,并使得代价最小。

【输入格式】

所有的输入均为正整数。令N代表分叉点的数量, M 代表烟花的数量。分叉点从1 N 编号,编号为1的分叉点是开关。烟花从 N+1 N+M 编号。

输入格式如下:

N M
P2 C2
P3 C3

PN CN
PN+1 CN+1

PN+M CN+M

其中 Pi 满足 1Pi<i ,代表和分叉点或烟花 i 相连的分叉点。Ci代表连接它们的导火索长度( 1Ci109 )。除开关外,每个分叉点和多于 1 条导火索相连,而每发烟花恰好与1条导火索相连。

【输出格式】

输出调整导火索长度,让所有烟花同时爆炸,所需要的最小代价。

【样例输入】

4 6
1 5
2 5
2 8
3 3
3 2
3 3
2 9
4 4
4 3

【样例输出】

5

【子任务】

子任务 1(7 分): N=1,1M100

子任务 2(19 分): 1N+M300 且开关到任意烟花的距离不超过 300

子任务 3(29 分): 1N+M5,000

子任务 4(45 分): 1N+M300,000

cstdio神犇的PPT

点击这里查看

7分算法

只有一个分叉点的话,显然改动分叉点到根结点的路没有卵用,问题转化为把一坨东西改成一样大所需的最小代价,求一下中位数就行了。

时间复杂度 O(M) ,期望得分7分。

26分算法

数据规模那么小,不妨这样表示状态: f[i][j] 表示以 i 为根结点的子树中,所有叶子结点到i的距离均为 j 的最小代价。这样进行树形DP就好了,转移当然是枚举子树根结点到此结点的边怎么更改咯。

当然预处理要把不合法的东西搞得很大很大。

时间复杂度O(nmax{dis}),根据题意 max{dis}89700 ,能够拿到第二个子任务的分数。

结合算法一获得26分。

100分算法

这个建模真是巧妙,最好去看上面的课件,下面的内容纯属口胡。

这个费用啊,明显是和绝对值函数有点关系,最后肯定是一个分段函数。设函数 F(x) 表示最终每个叶结点到根结点距离均为 x 时的最小费用。它满足如下性质:

  1. F(0)等于树上所有边权和。

    • x 足够大时,F(x)的值为一常数,它的值等于根结点的儿子个数。
    • 这样,只要知道所有拐点(当然这里强制规定所有拐点导致斜率增加1), 这个函数就可以画出来,再求最值就是小学问题了。

      问题就是这些拐点怎么求。当然,根据问题的性质,上述性质对任意一棵子树都是满足的,这就启示我们要进行合并。

      合并之前,因为子树要连上来,所以要增加一条边,显然当 x 足够大的时候,x每增加一点。只改这一条边都是最优方法,因此这时最右端斜率肯定是1。所以,把原来的拐点里面,导致之后斜率非负的都删去,记录下原来的拐点中斜率为0一段的左右端点,把它们往右平移就行。这里比较抽象。。。大家看样例画个图理解一下。。。。

      合并就是很简单的并在一起,注意可重。显然对于删点、加点、合并的操作,可并堆是坠吼的数据结构,打一个就好。

      时间复杂度 O((N+M)log(N+M)) ,期望得分100分。

      #include <cstdio>
      #include <cctype>
      #include <algorithm>
      #include <vector>
      using namespace std;
      #define MAXN 300010
      inline int readint()
      {
          int x = 0;
          char c = getchar();
          while (isdigit(c))
          {
              x = x * 10 + c - '0';
              c = getchar();
          }
          return x;
      }
      struct node
      {
          long long data, dis;
          node *son[2];
          node(long long d = 0)
          {
              data = d;
              dis = 0;
              son[0] = son[1] = 0;
          }
      };
      node* merge(node* x, node* y)
      {
          if (!x)
              return y;
          if (!y)
              return x;
          if (x->data < y->data)
              swap(x, y);
          x->son[1] = merge(x->son[1], y);
          int d1 = (x->son[0] ? x->son[0]->dis : 0);
          int d2 = (x->son[1] ? x->son[1]->dis : 0);
          if (d1 < d2)
              swap(x->son[0], x->son[1]);
          if (x->son[1])
              x->dis = x->son[1]->dis + 1;
          else
              x->dis = 0;
          return x;
      }
      node *root[MAXN];
      int n, m;
      long long p[MAXN], c[MAXN];
      long long sum[MAXN], son[MAXN];
      vector<long long> point;
      int main()
      {
          n = readint();
          m = readint();
          for (int i = 2; i <= n + m; i++)
          {
              p[i] = readint();
              c[i] = readint();
          }
          for (int i = n + m; i >= 2; i--)
          {
              sum[p[i]] += sum[i] + c[i];
              son[p[i]] ++;
          }
          for (int i = n + m; i >= 2; i--)
          {
              if (!root[i])
                  root[i] = merge(new node(c[i]), new node(c[i]));
              else
              {
                  long long l, r;
                  for (int j = 0; j <= son[i]; j++)
                  {
                      if (j == son[i] - 1)
                          r = root[i]->data;
                      if (j == son[i])
                          l = root[i]->data;
                      root[i] = merge(root[i]->son[0], root[i]->son[1]);
                  }
                  root[i] = merge(root[i], new node(l + c[i]));
                  root[i] = merge(root[i], new node(r + c[i]));
              }
              root[p[i]] = merge(root[p[i]], root[i]);
          }
          while (root[1])
          {
              point.push_back(root[1]->data);
              root[1] = merge(root[1]->son[0], root[1]->son[1]);
          }
          int sz = (int)point.size();
          for (int i = 0; i < sz - i - 1; i++)
              swap(point[i], point[sz - i - 1]);
          long long now = sum[1], k = son[1] - sz;
          for (int i = 0; k; i++)
          {
              now += k * (i ? point[i] - point[i - 1] : point[i]);
              k++;
          }
          printf("%lld", now);
          return 0;
      }

      C. 最大差分

      题目

      【问题描述】

      N 个严格递增的非负整数 a1,a2,,aN 0a1<a2<<aN1018 )。你需要找出 ai+1ai 0iN1 )里的最大的值。

      你的程序不能直接读入这个整数序列,但是你可以通过给定的函数来查询该序列的信息。关于查询函数的细节,请根据你所使用的语言,参考下面的实现细节部分。

      你需要实现一个函数,该函数返回 ai+1ai 0iN1 )中的最大值。

      (实现细节及样例略,可参考UOJ#206

      官方题解

      点击这里下载

      30.38分算法

      先询问MinMax(0, inf, &mn, &mx),得到整个序列的最大最小值,然后分别替换掉调用时的上下界,即询问MinMax(mn + 1, mx - 1, &mn, &mx),依次类推,每次确定下来2个元素,直到确定下来所有元素。

      然后就随便搞了。显然在子任务1中,询问次数不超过 N+12 ,可以拿到满分,但子任务2就不好了。。。实测结果是0.38分。

      100分算法

      首先考虑一个问题,答案的范围。

      首先调用MinMax(0, inf, &mn, &mx),得到最大最小值,那么答案的上界就是 mxmn ,又由于元素个数的限制,答案的下界为 mxmnN1

      因此,把整数区间 [mn+1,mx1] 划分成不超过 N 个区段,使得每一段的长度都不超过mxmnN1,这显然是可以做到的。然后对于每一段,询问这一段的最大最小值(如果没有就不算啦),连上整个序列的最大最小值一起,排个序求最大差分。

      为什么这样是对的呢?首先,根据上面的分析,最大差分不可能在同一块中取得,而在不同块中的差分,必然只能是快内的最大最小值才有可能接触到别的块中的元素,所以这样是没问题的。

      总询问代价怎么算呢?首先,两个极值各询问了 1 次,其余的每个元素都恰询问了2次,而总共询问次数不超过 N+1 次,所以总代价不超过 2+2×(N2)+N+1=3N1 ,恰好通过。

      我觉得可能在某些数据可以有更优解的。。。

      #include "gap.h"
      #define MAXN 1000000000000000000LL
      long long a[200010];
      long long findGap(int T, int N)
      {
          if (T == 1)
          {
              long long nowmin, nowmax;
              MinMax(0, MAXN, &nowmin, &nowmax);
              a[1] = nowmin, a[N] = nowmax;
              for (int i = 2; i <= N - i + 1; i++)
              {
                  MinMax(a[i - 1] + 1, a[N - i + 2] - 1, &nowmin, &nowmax);
                  a[i] = nowmin, a[N - i + 1] = nowmax;
              }
              long long ans = 0;
              for (int i = 1; i < N; i++)
                  if (a[i + 1] - a[i] > ans)
                      ans = a[i + 1] - a[i];
              return ans;
          }
          else
          {
              long long nowmin, nowmax;
              MinMax(0, MAXN, &nowmin, &nowmax);
              long long block = (nowmax - nowmin) / (N - 1);
              if ((nowmax - nowmin) % (N - 1) != 0)
                  block++;
              int cnt = 1;
              a[0]  = nowmin;
              for (int i = 0; i < N; i++)
              {
                  long long l = nowmin + block * i + 1, r = nowmin + block * (i + 1);
                  if (r == nowmax)
                      r--;
                  if (l >= nowmax)
                      break;
                  long long lsmin, lsmax;
                  MinMax(l, r, &lsmin, &lsmax);
                  if (lsmin > -1)
                  {
                      a[cnt++] = lsmin;
                      a[cnt++] = lsmax;
                  }
              }
              a[cnt++] = nowmax;
              long long ans = 0;
              for (int i = 0; i < cnt - 1; i++)
                  if (a[i + 1] - a[i] > ans)
                      ans = a[i + 1] - a[i];
              return ans;
          }
      }

      这题还是不错的。。。至少让我看见自己有多么智障。。。
      (劳资干吗不去?去了怎么还没个Ag啊。。。艹)

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值