今年的题比去年的不知道高到哪里去了。。。
Orz 蛤省Cu观光团
Orz cstdio
Orz 韩国出题人
A. 划艇
先吐槽:这题不是叫划艇么,怎么就赛艇了?
题目
【问题描述】
在首尔城中,汉江横贯东西。在汉江的北岸,从西向东星星点点地分布着
N
个划艇学校,编号依次为
值得注意的是,编号为 i 的学校如果选择派出划艇参加庆典,那么它派出的划艇数量必须大于任意一所编号小于它的学校派出的划艇数量。
【任务描述】
输入所有学校的
【输入格式】
第一行包括一个整数 N ,表示学校的数量。
接下来
【输出格式】
输出一行,一个整数,表示所有可能的派出划艇的方案数除以
1,000,000,007
得到的余数。
【样例输入】
2 1 2 2 3
【样例输出】
7
【样例说明】
在只有一所学校派出划艇的情况下有4种方案,两所学校都派出划艇的情况下有3种方案,所以答案为7。
【子任务】
子任务 1(9 分): 1≤N≤500 且对于所有的 1≤i≤N ,保证 ai=bi 。
子任务 2(22 分): 1≤N≤500 且 ∑1≤i≤N(bi−ai)≤106 。
子任务 3(27 分): 1≤N≤100 。
子任务 4(42 分): 1≤N≤500 。
9分算法
子任务1中一定满足 ai=bi ,因此我们这样DP:
设
f[i]
为最后一个取到的位置为
i
的方案数,定义
最后输出 ∑ni=1f[i] 即可。
时间复杂度 O(n2) ,期望得分9分。
100分算法
中间的子任务没什么意思。。。估计是照顾乱搞党?不管了直接说标算。
这种输入很少,但数字范围很大的问题,往往可以离散化处理。为了方便处理 ai=bi 的情况,我们把区间初始化成左闭右开区间 [ai,bi+1) ,然后把所有的输入数据进行离散化。
比如说,输入中的所有数据排序去重后,得到一个序列
{x1,x2,…,xcnt}
,那么我们把所有的
ai
值换做它在上述数组中的位置,
bi
同理,这里二分查找或者map
都是可以的。
然后,所有的值就都不超过
cnt
了。我们记
f[i][j]
为最后取到第
i
个学校,且在这里的取值在区间
然后考虑怎么转移。当前要计算 f[i][j] ,那么之前取的一个有两种情况:
· 之前取的数不在区间 [xj,xj+1) 内,这显然是没有什么问题的,随便取都合法,因此这一部分给 f[i][j] 带来的贡献为
· 之前取的数在区间 [xj,xj+1) 内。这就很麻烦了,因为这里都是区间而不是点,因此统计方案不能直接相加,否则会出现大量不合要求的方案。解决的办法是组合学方法。
先看一个很简单的问题:在区间 [xj,xj+1) 任取两个数,使得前一个小于后一个,有几种取法?
小学数学题吧,答案是 C2xj+1−xj 。
那如果取3个数 a,b,c 满足 a<b<c 呢?废话, C3xj+1−xj .
……
为了方便,我们把 xj+1−xj 记作 l 好了。现在考虑更难一些的问题:如果给你3个相同的区间,每个区间你可以选择选或不选,在你选出的所有区间中,每个取出一个数,并且取出的数严格递增,有几种取法?
分情况讨论一下。
· 一个区间都不选,那么有
· 选一个区间,那么有 C13×C1l=3l 种取法。
· 选两个区间,那么有 C23×C2l=l(l−1)6 种取法。
· 选三个区间,那么有 C33×C3l=l(l−1)(l−2)6 种取法。
因此总共有 1+3l+l(l−1)6+l(l+1)(l−2)6 种取法。这什么玩意啊。。。有毛线规律。。。
还是用组合形式表述吧。取法总数为 C03×C0l+C13×C1l+C23×C2l+C33×C3l 种取法。根据 Ckn=Cn−kn 的原则,这个式子又可以写为
是不是似曾相识?有没有在高中课本上见过?它明显是等于
C3l+3
的。这就好像在
l+3
个人(其中恰有
3
个神犇)里面选择
因此,上述问题稍加扩展,就是:
给你x个相同的区间( x≤l ),每个区间你可以选择选或不选,在你选出的所有区间中,每个取出一个数,并且取出的数严格递增,取法总数为 Cxl+x 。
好了,这个重要结论得到之后,我们回到问题。不是要求 f[i][j] 吗?我们枚举在之前第一次取到区间 [xj,xj+1) 的学校 k ,为了保证不重不漏,这个学校之前的学校都不能取到这个区间,它们的取法总数为
然后在
k
到
只要稍作代换,不难发现它的值等于
Ci−k−1i−k+l−1
。这样,通过枚举
k
,就可以计算出所要求的结果。归纳一下,这一部分造成的贡献是
似乎很完美?但一看这式子就知道复杂度是 O(n5) 的。。。
不难发现,凡是出现二维前缀和的地方,都是矩形左上角一块的所有数之和,那么再维护一下二维前缀和就好了,时间复杂度降至 O(n3) 。
还有一个问题,就是组合数的计算。显然直接按照定义是不行的,最好根据杨辉三角什么的先预处理出来组合数表,当然也可以利用递推式 Cm−1n−1=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 从开关点燃火花时,每一发烟花的爆炸时间。
Hyunmin为烟花表演设计了导火索的连线布局。不幸的是,在他设计的布局中,烟花不一定同时爆炸。我们希望修改一些导火索的长度,让所有烟花在同一时刻爆炸。例如,为了让[图1]中的所有烟花在时刻
修改导火索长度的代价等于修改前后长度之差的绝对值。例如,将[图1]中布局修改为[图2]左边布局的总代价为
6
,而将[图1]中布局修改为[图2]右边布局的总代价为
导火索的长度可以被减为 0 ,同时保持连通性不变。
给定一个导火索的连线布局,你需要编写一个程序,去调整导火索长度,让所有的烟花在同一时刻爆炸,并使得代价最小。
【输入格式】
所有的输入均为正整数。令
输入格式如下:
N M
P2 C2
P3 C3
…
PN CN
PN+1 CN+1
…
PN+M CN+M
其中
Pi
满足
1≤Pi<i
,代表和分叉点或烟花
i
相连的分叉点。
【输出格式】
输出调整导火索长度,让所有烟花同时爆炸,所需要的最小代价。
【样例输入】
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,1≤M≤100 。
子任务 2(19 分): 1≤N+M≤300 且开关到任意烟花的距离不超过 300 。
子任务 3(29 分): 1≤N+M≤5,000 。
子任务 4(45 分): 1≤N+M≤300,000 。
cstdio神犇的PPT
点击这里查看
7分算法
只有一个分叉点的话,显然改动分叉点到根结点的路没有卵用,问题转化为把一坨东西改成一样大所需的最小代价,求一下中位数就行了。
时间复杂度 O(M) ,期望得分7分。
26分算法
数据规模那么小,不妨这样表示状态:
f[i][j]
表示以
i
为根结点的子树中,所有叶子结点到
当然预处理要把不合法的东西搞得很大很大。
时间复杂度
结合算法一获得26分。
100分算法
这个建模真是巧妙,最好去看上面的课件,下面的内容纯属口胡。
这个费用啊,明显是和绝对值函数有点关系,最后肯定是一个分段函数。设函数 F(x) 表示最终每个叶结点到根结点距离均为 x 时的最小费用。它满足如下性质:
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 ( 0≤a1<a2<⋯<aN≤1018 )。你需要找出 ai+1−ai ( 0≤i≤N−1 )里的最大的值。你的程序不能直接读入这个整数序列,但是你可以通过给定的函数来查询该序列的信息。关于查询函数的细节,请根据你所使用的语言,参考下面的实现细节部分。
你需要实现一个函数,该函数返回 ai+1−ai ( 0≤i≤N−1 )中的最大值。
(实现细节及样例略,可参考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)
,得到最大最小值,那么答案的上界就是 mx−mn ,又由于元素个数的限制,答案的下界为 mx−mnN−1 。因此,把整数区间 [mn+1,mx−1] 划分成不超过 N 个区段,使得每一段的长度都不超过
mx−mnN−1 ,这显然是可以做到的。然后对于每一段,询问这一段的最大最小值(如果没有就不算啦),连上整个序列的最大最小值一起,排个序求最大差分。为什么这样是对的呢?首先,根据上面的分析,最大差分不可能在同一块中取得,而在不同块中的差分,必然只能是快内的最大最小值才有可能接触到别的块中的元素,所以这样是没问题的。
总询问代价怎么算呢?首先,两个极值各询问了 1 次,其余的每个元素都恰询问了
2 次,而总共询问次数不超过 N+1 次,所以总代价不超过 2+2×(N−2)+N+1=3N−1 ,恰好通过。我觉得可能在某些数据可以有更优解的。。。
#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啊。。。艹)
- 当
x
足够大时,