Top Banner
IOI2011 中国国家集训队第二次作业 第二部分: 自选题推荐 湖南 雅礼中学 何朴藩 教练:胡伟栋 唐文斌 HNOI 2010 解题报告 【目录】 编号 名称 英文名 参考难度 A. 合唱队 chorus B. 平面图判定 planar ★★ C. 物品调度 fsk ★★★★ D. 公交路线 bus ★★★ E. 取石子游戏 stone ★★★★★ F. 城市建设 city ★★★★★ G. 弹飞绵羊 bounce ★★★★ H. 矩阵 matrix ★★★
33

IOI2011 中国国家集训队第二次作业 第二部分 自选题推荐 · ioi2011 中国国家集训队第二次作业 第二部分: 自选题推荐 湖南 雅礼中学 何朴藩

Oct 21, 2019

Download

Documents

dariahiddleston
Welcome message from author
This document is posted to help you gain knowledge. Please leave a comment to let me know what you think about it! Share it to your friends and learn new things together.
Transcript
Page 1: IOI2011 中国国家集训队第二次作业 第二部分 自选题推荐 · ioi2011 中国国家集训队第二次作业 第二部分: 自选题推荐 湖南 雅礼中学 何朴藩

IOI2011 中国国家集训队第二次作业

第二部分: 自选题推荐

湖南 雅礼中学 何朴藩

教练:胡伟栋 唐文斌

HNOI 2010 解题报告

【目录】

编号 名称 英文名 参考难度

A. 合唱队 chorus ★

B. 平面图判定 planar ★★

C. 物品调度 fsk ★★★★

D. 公交路线 bus ★★★

E. 取石子游戏 stone ★★★★★

F. 城市建设 city ★★★★★

G. 弹飞绵羊 bounce ★★★★

H. 矩阵 matrix ★★★

Page 2: IOI2011 中国国家集训队第二次作业 第二部分 自选题推荐 · ioi2011 中国国家集训队第二次作业 第二部分: 自选题推荐 湖南 雅礼中学 何朴藩

A.合唱队(chorus)

[相关信息]

传统型,chorus.c/cpp/pas,input.txt,output.txt

关键字:动态规划

[题目大意]

对于一个包含 N个整数的数列 A,我们可以把它的所有元素加入一个双头队列 B。

首先 A[1]作为队列的唯一元素,然后依次加入 A[2..N],如果 A[i]<A[i-1]那

么从 B的左端加入 A[i],否则从 B的右端加入 A[i]。

给出最终的队列 B,求原数列有多少种可能排列。输出答案对 19650827取余。

30%:N≤100

100%:1≤N≤1000≤B[i]≤2000, 没有重复数。

[资源限制]

每个测试点 1s 512MB

[分析]

由于每次只会在队列的两端进行加入操作,A[1..i]必然是最终队列 B 上的连续

一段。这就是本题的关键思路。至此选手很容易想到区间模型的动态规划。

F[l][r]表示:

生成队列 B上面 l到 r的区间,且最后一个数在 l的排列有多少种。

G[l][r]表示:

生成队列 B上面 l到 r的区间,且最后一个数在 r的排列有多少种。

这里只存储取余以后的答案。

初始值:

F[i][i] = G[i][i] = 1

F[i-1][i] = G[i-1][i] = (B[i-1]<B[i])

转移 :(j-i>1) //满足 j-i>1时,[i+1,j]和[i,j-1]的左右端是不同数字

F[i][j] = F[i+1][j]*(B[i]<B[i+1])+G[i+1][j]*(B[i]<B[j])

G[i][j] = G[i][j-1]*(B[j]>B[j-1])+F[i][j-1]*(B[j]>B[i])

至此我们已经可以在 O(N2)的时间内解决本题。

由于中途不涉及两个大数相乘的运算,我们可以用判断+减法的方法来代替每次取

模操作,这样可以大大提高效率。

[算法总结]

输入数列 B[N]

设定状态初值

枚举区间长度 len

枚举区间起点 i

计算 F[i][i+len]和 G[i][i+len]

输出(F[1][N]+G[1][N])%M

Page 3: IOI2011 中国国家集训队第二次作业 第二部分 自选题推荐 · ioi2011 中国国家集训队第二次作业 第二部分: 自选题推荐 湖南 雅礼中学 何朴藩

时间复杂度:O(N2)

空间复杂度:O(N) //可以按 len值使用滚动数组优化状态空间

[参考程序] chorus.cpp 33L 792C

#include <cstdio>

using namespace std;

#define N 1111

#define MD 19650827

int n, a[N], f[N][N], g[N][N];

int main()

{

freopen("input.txt", "r", stdin);

freopen("output.txt", "w", stdout);

scanf("%d", &n);

for (int i = 1; i <= n; ++i) // 输入

{

scanf("%d", a + i);

f[i][i] = g[i][i] = 1;

if (i > 1) // 初值

f[i - 1][i] = g[i - 1][i] = a[i - 1] < a[i];

}

for (int len = 2; len < n; ++len) // 动态规划

for (int i = 1, j; (j = i + len) <= n; ++i)

{

f[i][j] = f[i + 1][j]*(a[i]<a[i + 1]) + g[i + 1][j]*(a[i]<a[j]);

g[i][j] = g[i][j - 1]*(a[j]>a[j - 1]) + f[i][j - 1]*(a[j]>a[i]);

if (f[i][j] >= MD) f[i][j] -= MD;

if (g[i][j] >= MD) g[i][j] -= MD;

}

printf("%d\n", (f[1][n] + g[1][n]) % MD);

fclose(stdin);

fclose(stdout);

return 0;

}

Page 4: IOI2011 中国国家集训队第二次作业 第二部分 自选题推荐 · ioi2011 中国国家集训队第二次作业 第二部分: 自选题推荐 湖南 雅礼中学 何朴藩

B.平面图判定(planar)

[相关信息]

传统型,planar.c/cpp/pas,input.txt,output.txt

关键字:平面图,哈密顿回路,二分图判定

[题目大意]

给出 T 个无向图(无重边自环),并给出它们各自的哈密顿回路:即经过所有点恰

好一次最后回到起点的环。

分别判断每个图是否是平面图,输出 T行 YES/NO。

T≤100 3≤N≤200 M≤10000 N为点数 M为边数

[资源限制]

每个测试点 1s 512MB

[分析]

考虑一个带哈密顿回路的无向图,如果它是一个平面图,即可以画在平面上使得没

有 2条边需要交叉,那么哈密顿圈之外的边要么画在圈内,要么画在圈外。

(绿色的环是哈密顿圈)

按顺序给哈密顿圈上的点编号,我们发现:

如果两条边 e,f,把它们都画在圈的内侧会相交,那么都画在外侧也一定会相交。

也就是说,对于两条边,要么没有相互约束,要么有一条约束:它们不能在圈的同

侧。

这让我们想起了二分图模型。

求出所有边和边的约束关系,用黑白染色法判断约束关系是否为二分图。

如果是二分图,则原图是平面图。否则原图不是平面图。

由于平面图中,不重复的边数 M 最大也只有 N*3-6,所以先判断如果边数过多则

一定不是平面图,如果边数≤N*3-6 再用二分图算法做。这样边就很少了,可以保证

效率。

Page 5: IOI2011 中国国家集训队第二次作业 第二部分 自选题推荐 · ioi2011 中国国家集训队第二次作业 第二部分: 自选题推荐 湖南 雅礼中学 何朴藩

[算法总结]

对于每组数据:

输入并判断边数上界

枚举非哈密顿圈上的边,建立约束关系图

判断约束关系图是否为二分图

输出

时间复杂度: O(N2) // 注意平面图中 O(M)=O(N)

空间复杂度: O(N2+M)

[参考程序] planar.cpp 96L 2047C

#include <cstdio>

#include <cstring>

#include <algorithm>

using namespace std;

const int N = 211, M = 1626, MM = 1111111;

int n, m, tst, t, a[N][N], c[N], rank[N];

int b[N][N], x[M], y[M], st[M], nx[MM], to[MM], col[M];

bool ansed;

inline bool crs(int x1, int y1, int x2, int y2) // 判断是否相交

{

if (x1 == x2 || x1 == y2 || y1 == x2 || y1 == y2) return false;

x1 = rank[x1];

y1 = rank[y1];

x2 = rank[x2];

y2 = rank[y2];

if (x1 > y1) swap(x1, y1);

return (bool)(x1 < x2 && x2 < y1) != (bool)(x1 < y2 && y2 < y1);

}

void sc(int v, int ccc) // 二分图染色判定

{

col[v] = ccc;

for (int i = st[v], j; j = to[i], i; i = nx[i])

{

if (col[j] == 0) sc(j, 3 - ccc); else

if (col[j] == ccc)

ansed = true;

if (ansed) return;

}

}

int main()

Page 6: IOI2011 中国国家集训队第二次作业 第二部分 自选题推荐 · ioi2011 中国国家集训队第二次作业 第二部分: 自选题推荐 湖南 雅礼中学 何朴藩

{

freopen("input.txt", "r", stdin);

freopen("output.txt","w", stdout);

memset(b, 0, sizeof b);

for (scanf("%d", &tst); tst; --tst)

{

ansed = false;

scanf("%d%d", &n, &m); // 输入

memset(a, 0, sizeof a);

for (int i = 0; i < m; ++i)

{

int xx, yy;

scanf("%d%d", &xx, &yy);

a[xx][++a[xx][0]] = yy;

}

for (int i = 0; i < n; ++i)

scanf("%d", c + i), rank[c[i]] = i;

for (int i = 1; i < n; ++i)

b[c[i]][c[i - 1]] = b[c[i - 1]][c[i]] = tst;

b[c[n - 1]][c[0]] = b[c[0]][c[n - 1]] = tst;

if (m > 3 * n) // 合法上界判断

{

puts("NO");

ansed = true;

continue;

}

t = 0;

for (int i = 1; i <= n; ++i)

for (int k = 1; k <= a[i][0]; ++k)

{

int j = a[i][k];

if (b[i][j] != tst) // 寻找非哈密顿圈上的边

{

++t;

x[t] = i;

y[t] = j;

}

}

int top = 1;

memset(st, 0, sizeof st);

for (int i = 1; i < t; ++i)

for (int j = i + 1; j <= t; ++j) // 枚举边对,判断是否互相约束

if (crs(x[i], y[i], x[j], y[j]))

Page 7: IOI2011 中国国家集训队第二次作业 第二部分 自选题推荐 · ioi2011 中国国家集训队第二次作业 第二部分: 自选题推荐 湖南 雅礼中学 何朴藩

{

nx[++top] = st[i];

to[st[i] = top] = j; // 判定图上建边

nx[++top] = st[j];

to[st[j] = top] = i;

}

memset(col, 0, sizeof col);

for(int i = 1; i <= t; ++i) if (!col[i]) // 对判定图进行染色判断

{

sc(i, 1);

if (ansed) break; // 如果是二分图 那么 YES 否则 NO

}

if (!ansed) puts("YES"); else puts("NO");

}

fclose(stdin);

fclose(stdout);

return 0;

}

Page 8: IOI2011 中国国家集训队第二次作业 第二部分 自选题推荐 · ioi2011 中国国家集训队第二次作业 第二部分: 自选题推荐 湖南 雅礼中学 何朴藩

C.物品调度(fsk)

[相关信息]

传统型,fsk.c/cpp/pas,input.txt,output.txt

关键字:路径压缩,置换群

[题目大意]

对于每组数据,给出 6个整数:n, s, q, p, m, d

生成数列 c0..n-1: c0=0, ci+1=(ci*q+p) MOD m

你需要确定非负整数 x1..n-1, y1..n-1

使得 pos0=s, posi=(ci+d*xi+yi) MOD n

恰好是一个 0到 n-1的排列

当(xi,yi)有多种选择时,yi要尽量小,然后 xi尽量小。

通过这种方法我们可以得出唯一的 pos0..n-1

假设有 n-1个物品 1..n-1 posi表示物品 i的位置 pos0表示空位的位置

初始时空位在 0 号位,物品 i在 i 号位。每次移动可以把一个物品放到空位去,

然后它原来所在的位置变成空位。问从初始移动到目标 pos最少需要多少移动。

30%: n≤100

100%: 数据组数 t≤20 n≤100000 中途运算可能超过 32位整数

[资源限制]

每个测试点 1s 512MB

[分析]

这个题分为两问:

第一问确定 xi yi,生成 pos

第二问,求最小移动次数

【第二问】是一个置换群中比较经典的贪心问题。

关于如何用最少次数把一个 0到 n-1 的排列 A从{0..n-1}变来,每次只允许交

换 0和一个其他元素的位置。

首先求出该排列对应的置换中,所有长度大于 1的轮换。

对于每个轮换 X,都需要|X|+1次移动才能还原。特殊的,如果轮换包含元素 0,

则只需要|X|-1次。

(把 0 换进来,按照轮换顺序交换|X|-1 次还原置换,把 0 换出去。如果 0 在该

轮换中,则不需要换进和换出。)

举例:

5 2 3 1 4 6 0 --> 0 1 2 3 4 5 6

有 3个轮换: [5 0 6] [2 1 3] [4]

其中[4]的长度为 1,说明不需要任何操作就可以还原。

Page 9: IOI2011 中国国家集训队第二次作业 第二部分 自选题推荐 · ioi2011 中国国家集训队第二次作业 第二部分: 自选题推荐 湖南 雅礼中学 何朴藩

第一个轮换[5 0 6]长度为 3且包含 0。

先交换 6,0再交换 5,0即可。(3-1次)

此时为 0 2 3 1 4 5 6

第二个轮换[2 1 3]长度为 3不包含 0。

先把 0和任何一个元素交换,不妨交换 0,2

接着不断把应该在 0所在位置的数和 0交换,即 0,1 0,3

最后交换 0,2 (3+1次)

此时为 0 1 2 3 4 5 6

综上第二问可以 O(n)解决。

【第一问】:按顺序确定(xi,yi)

由于 pos需要是排列,所以不能出现重复元素。

xi每增加 1,位置会增加 d (MOD n)

yi每增加 1,位置会增加 1 (MOD n)

首先 y要尽量小,理想状态是 y=0,此时只调节 x会到达 n/GCD(n,d)个位置

举例:

d = 2, n = 6, s = 0, c = {0,1,2,0,1,2}

0 1 2 3 4 5 红色表示已经在之前的 pos中出现过了。

显然 s=0会占掉一个位置

当 i=1时,ci=1 还是空位,所以不需要调节,xi=yi=0

0 1 2 3 4 5

当 i=2时,ci=2同理不需要调节

0 1 2 3 4 5

当 i=3时,ci=0 已经被选过。

此时只调节 x3可以把位置改成{0,2,4}这三种可能值。我们发现{0,2,4}中

还有可选值,所以不用调节 y,找到最近的空位 4并填入。

xi=2,yi=0

0 1 2 3 4 5

当 i=4时,ci=1已经选过。

不调节 y仍然可以在{1,3,5}中找到空位 3,所以填入。xi=1,yi=0

0 1 2 3 4 5

当 i=5时,ci=2已经选过

此时如果不调节 y,{2,4,0}已经没有可选位置了,所以必须调节 y

y=1时{3,5,1}还有可选元素 5

所以 xi=1,yi=1.

维护 GCD(n,d)个等差子序列中还没被选的数的个数。

n=6,d=2时就是{0,2,4}和{1,3,5}

实际上就是按编号对 GCD(n,d)的余数分类。

我们可以用 2个跳转链表的方式来维护每个候选位置最近的空闲位置在哪。

第一个用来求 y,共有 GCD(n,d)个元素,每次加 1模 GCD(n,d)

第二个用来求 x,共有 n个元素,每次加 d模 n

Page 10: IOI2011 中国国家集训队第二次作业 第二部分 自选题推荐 · ioi2011 中国国家集训队第二次作业 第二部分: 自选题推荐 湖南 雅礼中学 何朴藩

初始时所有元素都指向自己。

每次填入一个数时修改 x的链表,每次把一组可选值用完时修改 y的链表。

这两个跳转表都可以用路径压缩的方法寻找最近的空位。这样每次的均摊复杂度约

为 O(1)。

至此,第一问也在 O(n)时间内解决。

[算法总结]

对于每组数据:

输入参数 n,s,q,p,m,d

计算 c

预处理跳转表

枚举 i求 pos[i]

求 pos中的每个轮换,当前轮换长度 len

如果 0在轮换中

ans+=len-1

否则

ans+=len+1

输出 ans

注意:大量使用取余运算和长整型运算会降低算法效率。

可以用判断+减法来优化取余运算,不会上溢的地方尽量用普通整型。

时间复杂度: O(nt)

空间复杂度: O(n)

[参考程序] fsk.cpp 85L 1750C

#include <iostream>

#include <cstring>

using namespace std;

int tst;

const int N = 111111;

int n, s, q, p, m, d, dd, c[N], x[N], y[N], pos[N], a[N], ans, t;

int cnt[N], rx[N], ry[N], rx_cnt[N], ry_cnt[N];

bool b[N];

int gcd(int a, int b)

{

for (int c; c = a; a = b % a, b = c);

return b;

}

inline void fill(int x)

{

int x2;

Page 11: IOI2011 中国国家集训队第二次作业 第二部分 自选题推荐 · ioi2011 中国国家集训队第二次作业 第二部分: 自选题推荐 湖南 雅礼中学 何朴藩

if ((x2 = x + d) >= n) x2 -= n;

rx_cnt[x] = 1;

rx[x] = x2;

x %= m;

if (--cnt[x]) return;

if ((x2 = x + 1) >= m) x2 -= m;

ry_cnt[x] = 1;

ry[x] = x2;

}

inline int get_cnt(int *r, int *r_cnt, const int &x)

{

if (r[x] == x) return 0;

r_cnt[x] = r_cnt[x] + get_cnt(r, r_cnt, r[x]);

r[x] = r[r[x]];

return r_cnt[x];

}

int main()

{

freopen("input.txt", "r", stdin);

freopen("output.txt","w", stdout);

for (cin >> tst; tst; --tst)

{

cin >> n >> s >> q >> p >> m >> d;

d %= n;

c[0] = 0;

for (int i = 0; i < n; ++i)

c[i + 1] = ((long long)c[i] * q + p) % m;

m = gcd(d, n);

for (int i = 0; i < m; ++i)

{

cnt[i] = n / m;

ry[i] = i;

ry_cnt[i] = 0;

}

for (int i = 0; i < n; ++i)

rx[i] = i;

memset(rx_cnt, 0, sizeof rx_cnt);

memset(b, 0, sizeof b);

fill(pos[0] = s);

for (int i = 1; i < n; ++i)

{

y[i] = get_cnt(ry, ry_cnt, c[i] % m);

Page 12: IOI2011 中国国家集训队第二次作业 第二部分 自选题推荐 · ioi2011 中国国家集训队第二次作业 第二部分: 自选题推荐 湖南 雅礼中学 何朴藩

x[i] = get_cnt(rx, rx_cnt, (c[i] + y[i]) % n);

pos[i] = (c[i] + (long long) x[i] * d + y[i]) % n;

fill(pos[i]);

}

for (int i = 0; i < n; ++i)

a[pos[i]] = i;

ans = 0;

memset(b, 0, sizeof b);

for (int i = 0; i < n; ++i)

if (!b[i])

{

t = 0;

for (int k = i; !b[k]; b[k] = true, k = a[k], ++t);

if (t != 1)

ans += t - 1 + (i != 0) * 2;

}

cout << ans << endl;

}

fclose(stdin);

fclose(stdout);

return 0;

}

Page 13: IOI2011 中国国家集训队第二次作业 第二部分 自选题推荐 · ioi2011 中国国家集训队第二次作业 第二部分: 自选题推荐 湖南 雅礼中学 何朴藩

D.公交路线(bus)

[相关信息]

传统型,bus.c/cpp/pas,input.txt,output.txt

关键字:状态压缩动态规划,矩阵乘法

[题目大意]

有 N个车站 1..N,K条公交线路。

第 1到 K站是这 K线路的起点站。

第 N-K+1到 N是终点站

车只会从编号小的车站驶向编号大的车站。

要求每个车站恰好只属于一个线路,而且同一个线路相临两站距离不得大于 P

求有多少种安排方法。输出答案对 30031取余数。

40%: N≤1000

100%: 1≤N≤109 1<K≤P≤10 K<N

[资源限制]

每个测试点 2s 512MB

[分析]

本题是一个划分方案数统计问题。

举例:N=10 K=3 P=4 以下是一些合法方案:字母表示线路

1 2 3 4 5 6 7 8 9 10

A B C A B C B A C B

1 2 3 4 5 6 7 8 9 10

A B C A A B C A C B

不妨抛开前 K个车站不看。

我们发现,只要每连续的 P个站中,都出现了所有 K种公交车,方案就是合法的。

证明:如果方案不合法,必有一线路有相邻站距离大于 P,即这连续 P个站中缺少

一种公交车。根据逆否命题等价,得证。

由于没有线路车站数的限制,P又不大,容易想到状态压缩动态规划:

F[i][S]表示:

前 i 位已经确定完毕,不同公交车最后经停站距 i+1 的位置的状态为 S,此时的

方案总数。

由于公交车是无差别的,S 实际上是 K 个不同整数的集合。每个元素都是 1 到 P

的数。

更进一步,集合 S中一定有一个元素 1,其余的都是 2到 P。

所以最大只有 C(P-1,K-1)个状态即 C(9,5)=126

Page 14: IOI2011 中国国家集训队第二次作业 第二部分 自选题推荐 · ioi2011 中国国家集训队第二次作业 第二部分: 自选题推荐 湖南 雅礼中学 何朴藩

这样一来我们容易想到用矩阵乘法来优化递推。

[算法总结]

对于每组数据:

输入 N K P

预处理所有合法状态

预处理转移矩阵

矩阵快速幂动态规划

注意:大量使用取余运算会降低算法效率。

矩阵乘法时可以先累加最后一次性取模。

时间复杂度: O(logNk3) k = C(P-1,K-1)

空间复杂度: O(k2)

[参考程序] bus.cpp 81L 1516C

#include <iostream>

#include <cstring>

#include <algorithm>

using namespace std;

const int MD = 30031, M = 222;

int n, k, p, t;

bool a[M][M];

typedef int matrix[M][M];

matrix c, d;

long long tmp[M][M];

void dfs(int v, int bound)

{

if (v == k)

{

memcpy(a[t + 1], a[t], sizeof a[0]);

++t;

} else

for (int i = bound; i < p; ++i)

{

a[t][i] = true;

dfs(v + 1, i + 1);

a[t][i] = false;

}

}

Page 15: IOI2011 中国国家集训队第二次作业 第二部分 自选题推荐 · ioi2011 中国国家集训队第二次作业 第二部分: 自选题推荐 湖南 雅礼中学 何朴藩

void multify(matrix c, matrix a, matrix b) // c=a*b

{

memset(tmp, 0, sizeof tmp);

for (int i = 0; i < t; ++i)

for (int j = 0; j < t; ++j)

for (int k = 0; k < t; ++k)

tmp[i][j] += a[i][k] * b[k][j];

for (int i = 0; i < t; ++i)

for (int j = 0; j < t; ++j)

c[i][j] = tmp[i][j] % MD;

}

int main()

{

freopen("input.txt", "r", stdin);

freopen("output.txt","w", stdout);

cin >> n >> k >> p;

t = 0;

dfs(1, 1);

for (int i = 0; i < t; ++i)

for (int j = 0; j < t; ++j)

{

int expected = a[j][1];

for (int s = 1; s < p; ++s)

if (a[i][s] && !a[j][s + 1])

--expected;

if (!expected)

c[i][j] = 1;

}

n -= k;

for (int i = 0; i < t; ++i)

d[i][i] = 1;

for (int i = 0; i < 31; ++i)

{

if (n >> i & 1) multify(d, d, c);

multify(c, c, c);

}

int ans = 0;

for (int i = 0; i < t; ++i)

{

int expected = k - 1;

for (int s = 1; s <= k - 1; ++s)

if (a[i][s]) --expected;

Page 16: IOI2011 中国国家集训队第二次作业 第二部分 自选题推荐 · ioi2011 中国国家集训队第二次作业 第二部分: 自选题推荐 湖南 雅礼中学 何朴藩

if (!expected)

(ans += d[0][i]) %= MD;

}

cout << ans << endl;

fclose(stdin);

fclose(stdout);

return 0;

}

E.取石子游戏(stone)

[相关信息]

传统型,stone.c/cpp/pas,input.txt,output.txt

关键字:博弈,贪心

[题目大意]

有 N堆石子排在一行。第 i堆有 ai个。初始时有些堆是空的。甲乙两人轮流取石

子,每次必须选一个空堆的相邻的石子堆全部取掉。取完以后也会变成空堆。(不能直

接从两端取)

甲乙两人都希望取得尽量多的石子。问:当它们都用最优策略时甲乙最终会有多少

石子。

40%: N≤100

100%: 2≤N≤106 0≤ai≤10

8 至少有 1个空堆。

[资源限制]

每个测试点 2s 512MB

Page 17: IOI2011 中国国家集训队第二次作业 第二部分 自选题推荐 · ioi2011 中国国家集训队第二次作业 第二部分: 自选题推荐 湖南 雅礼中学 何朴藩

[分析]

本题和一般的博弈问题不一样。本题不讨论输赢,只让选手得到尽量多的石子。

由于双方最终石子数之和是确定的,双方的目标就是使自己-别人的石子数差最大

化。

首先我们可以抽象问题:

有两个栈,若干个双头队列,总长度不超过 106

每次可以从栈顶取一个数,也可以从双头队列选一端取一个数。

2人轮流以最大化自己数字和的目标取数,问最终结果。

如果只有一个栈,那么取法是一定的。

如果只有一个队列,如果是奇数个,取法也是一定的。如果是偶数个,先手会取

max(奇数位的和,偶数位的和).

本题的关键难点是组合策略。

如果可取元素都是递减的,比如 1 2 3 0 2 1 2 0 4 1

容易发现先手只要贪心地从能取的元素里面拣最大的取走即可。

这样不会给后手好情况。

由于每次一定可以取全场最大值,所以只要一次排序然后交替取值即可。

4 3 2 2 2 1 1 1

如果不是这样,我们可以通过 2个操作来化简数列:

1. 如果最左端是 A B.. 或者最右端是..B A, 且 A>=B

那么双方在有其它方案时都不会愿意先取走 B,故这种情况可以留到博弈的最后。

由于石子数是确定的,可以直接推出最后谁取到了 A,算出相应差值。

由于可以留到游戏的最后,此时删除这两堆并不影响两人之前的决策。

2. 如果有一段 ..A B C.. 且满足 B>=A B>=C

那么我们直接把 ABC替换成一个 A+C-B即可。

我们可以这样想:选 A,B,C的时候是因为没有更好的决策而被迫选的。事实上当

全场没有大于 A+C-B的石子堆可以直接取时,才会考虑取 A,C中的一个。那么不管第

一次取 A,B,C中的元素是从哪边,后手一定也没有别的更好的选择,既然先手选 A/C

都已是被迫了,所以后手选 B 一定不会是差的。留下来的一个也一定是当前不差的选

择。故先手一定取走 A+C,后手取走 B。从对分数差的贡献来看,我们可以直接把 A,B,C

代替成 A+C-B

化简以后,就变成前面所说的基础情况了。

用链表实现 O(N)的化简,最后排序,总时间复杂度是 O(NlogN)的。

[算法总结]

输入数列,计算和

Page 18: IOI2011 中国国家集训队第二次作业 第二部分 自选题推荐 · ioi2011 中国国家集训队第二次作业 第二部分: 自选题推荐 湖南 雅礼中学 何朴藩

建立链表

不断进行化简直到不可化简

对剩余元素排序,从大到小:奇数位和偶数位分别归属于先手和后手

根据和与差计算双方各自最终石子数

时间复杂度: O(NlogN)

空间复杂度: O(N)

提示:C/C++语言使用字符数组一次性读入后再处理,能显著提高输入效率。

注意:答案和中途运算均可能超过 32位整数。

[参考程序] stone.cpp 86L 1937C

#include <iostream>

#include <cstdio>

#include <cstring>

#include <algorithm>

using namespace std;

const int N = 1111111;

int n, t, k, cnt;

char s[11 * N];

int l[N], r[N], head, tail;

bool b[N];

long long a[N], sum, dif, ss[N];

void del(int x)

{

b[x] = true;

if (l[x] >= 0) r[l[x]] = r[x];

if (r[x] < n) l[r[x]] = l[x];

}

int main()

{

freopen("input.txt", "r", stdin);

freopen("output.txt", "w", stdout);

scanf("%d%*c", &n);

gets(s);

sum = k = t = cnt = 0;

for (char *p = s; t != n; ++p)

if (*p == ' ' || !*p)

sum += a[t++] = k, k = 0;

else

k = k * 10 + *p - '0';

for (int i = 0; i < n; ++i)

Page 19: IOI2011 中国国家集训队第二次作业 第二部分 自选题推荐 · ioi2011 中国国家集训队第二次作业 第二部分: 自选题推荐 湖南 雅礼中学 何朴藩

l[i] = i - 1, r[i] = i + 1, b[i] = !a[i], cnt += !b[i];

head = 0, tail = n - 1;

for (bool ok = true; ok && head < tail;)

{

ok = false;

while (!b[head] && !b[r[head]] && a[head] >= a[r[head]])

{

if (cnt & 1)

dif += a[head] - a[r[head]];

else

dif += a[r[head]] - a[head];

ok = true;

int tmp = r[r[head]];

del(r[head]);

del(head);

head = tmp;

}

while (!b[tail] && !b[l[tail]] && a[tail] >= a[l[tail]])

{

if (cnt & 1)

dif += a[tail] - a[l[tail]];

else

dif += a[l[tail]] - a[tail];

ok = true;

int tmp = l[l[tail]];

del(l[tail]);

del(tail);

tail = tmp;

}

for (int i = head; i <= tail; ++i)

if (!b[i] && l[i] >= 0 && r[i] < n)

if (!b[l[i]] && !b[r[i]])

if (a[i] >= a[l[i]] && a[i] >= a[r[i]])

{

ok = true;

a[i] = a[l[i]] + a[r[i]] - a[i];

if (l[i] == head) head = i;

if (r[i] == tail) tail = i;

del(l[i]);

del(r[i]);

}

}

for (int i = t = 0; i < n; ++i)

if (!b[i]) ss[t++] = a[i];

Page 20: IOI2011 中国国家集训队第二次作业 第二部分 自选题推荐 · ioi2011 中国国家集训队第二次作业 第二部分: 自选题推荐 湖南 雅礼中学 何朴藩

sort(ss, ss + t);

reverse(ss, ss + t);

for (int i = 0; i < t; ++i)

if (i & 1) dif -= ss[i]; else dif += ss[i];

cout << (sum + dif) / 2 << " " << (sum - dif) / 2 << endl;

fclose(stdin);

fclose(stdout);

return 0;

}

F.城市建设(city)

[相关信息]

传统型,city.c/cpp/pas,input.txt,output.txt

关键字:动态最小生成树,离线算法

[题目大意]

无向图有 N个点,M条边,每条边有个初始边权。

输入 Q 次操作,每次内容是,把某一条边的边权改为输入的值,然后要求输出最

小生成树的边权和。

20%: N≤1000 M,Q≤6000

另 20%:N≤1000 M≤50000 Q≤8000 且每次修改费用不会比之前低

100%: N≤20000 M≤50000 Q≤50000

[资源限制]

Page 21: IOI2011 中国国家集训队第二次作业 第二部分 自选题推荐 · ioi2011 中国国家集训队第二次作业 第二部分: 自选题推荐 湖南 雅礼中学 何朴藩

每个测试点 2s 512MB

[分析]

本题是一道动态最小生成树问题(Dynamic MST)。

这里提供一个离线算法:

引用<<Offline Algorithms for Dynamic MST Problems>> David Eppstein 1992

原图有 N个点 M条边,对于每次修改我们需要做出一个回答。

我们用 work(L,R)表示对于图 G,求从第 L到第 R个修改后的询问。

则该算法的大体思路是:

work(L, R):

IF L == R

act_modify_number(L)

Ans[L] = MST(G)

ELSE

simplify(G, L, R)

work(L, (L+R)/2)

work((L+R)/2+1, R)

recover(G, L, R)

我们知道这样修改的顺序实际上就是从 1到 Q,而且要做 Q次 Kruskal算法。

为了保证效率,我们在分治时需要化简图。

如果对于一个区间(L,R),我们都能用一个 O(R-L)左右的算法把图的点数和边数

化简到 O(R-L)的大小级别,那么这个算法就可以快速出解了。

这里化简分为两步:

1. Contraction

把 L..R要修改的所有边权暂时标记为-∞;

对图做 MST;

此时观察图中不是-∞但被选入 MST的边集;

它们在 L..R的询问中也一定会被选入;

于是可以直接先用这些边把点集做合并操作。这样图的点数被缩小;

还原边权标记.

2. Reduction

把 L..R要修改的所有边权暂时标记为∞;

对图做 MST;

此时观察图中不是∞但没被选入 MST的边集;

它们在 L..R的询问中是无意义的,可以直接删除。这样图的边数被缩小;

还原边权标记.

这样每次化简的复杂度是 O(ElogE)。//E为图中的边数 化简需要两次排序

设 K=R-L

可以证明:

Page 22: IOI2011 中国国家集训队第二次作业 第二部分 自选题推荐 · ioi2011 中国国家集训队第二次作业 第二部分: 自选题推荐 湖南 雅礼中学 何朴藩

Contraction之后,图最多只剩 K+1个点

Reduction之后,图最多只剩 V-1+K = 2K条边。

详细证明请参见论文原文。

关于还原操作(recover),很显然,如果记录所有修改操作,recover的复杂度

≤simplify

所以,根据主定理,该算法的总时间复杂度为 O((M+Q)log2(M+Q))

另有一有个复杂度稍高的在线算法,它每次操作的均摊时间复杂度为O(N1/3logN)。

参见<<Maintaining Minimum Spanning Trees in Dynamic Graphs>> 1997

[算法总结]

输入并存储数据

分治法离线解决所有询问

时间复杂度: O((M+Q)log2(M+Q))

空间复杂度: O(MlogQ)

[参考程序] city.cpp 141L 2849C

(程序时间常数较大,作者测试时需要编译优化才能在 2s内出解。)

#include <cstdio>

#include <cstring>

#include <algorithm>

using namespace std;

const int N = 21111, M = 51111;

const long long INF = 1LL << 40;

int n, m, q, x[M], y[M], zz[M], num[M], to[M], f[N], ord[M];

long long z[M], base_ans;

struct recover

{

int cnt;

int a[M*5], b[M*5];

inline int find(int v)

{

if (!f[v]) return v;

int ret = v;

while (f[ret]) ret = f[ret];

while (f[v] != ret)

{

++cnt;

Page 23: IOI2011 中国国家集训队第二次作业 第二部分 自选题推荐 · ioi2011 中国国家集训队第二次作业 第二部分: 自选题推荐 湖南 雅礼中学 何朴藩

b[cnt] = f[a[cnt] = v];

f[v] = ret;

v = b[cnt];

}

return ret;

}

inline void merge(int u, int v)

{

u = find(u);

v = find(v);

a[++cnt] = u;

b[cnt] = f[u];

f[u] = v;

}

inline void action(int top = 0)

{

for (; cnt != top; --cnt)

f[a[cnt]] = b[cnt];

}

} re[18];

int opt[M];

void qs(int l, int r)

{

long long md = z[ord[l + r >> 1]];

int i = l, j = r;

while (i < j)

{

while (z[ord[i]] < md) ++i;

while (z[ord[j]] > md) --j;

if (i <= j)

swap(ord[i++], ord[j--]);

}

if (l < j) qs(l, j);

if (i < r) qs(i, r);

}

void work(int dep, int l, int r)

{

recover &e = re[dep];

e.cnt = 0;

if (l == r)

{

z[num[l]] = zz[num[l]] = to[l];

Page 24: IOI2011 中国国家集训队第二次作业 第二部分 自选题推荐 · ioi2011 中国国家集训队第二次作业 第二部分: 自选题推荐 湖南 雅礼中学 何朴藩

qs(1, m);

long long ans = base_ans;

for (int i = 1; i <= m; ++i)

if (e.find(x[ord[i]]) != e.find(y[ord[i]]))

{

ans += z[ord[i]];

e.merge(x[ord[i]], y[ord[i]]);

}

printf("%I64d\n", ans);

e.action();

return;

}

int m_rem = m;

long long base_rem = base_ans;

// Start Contraction

for (int i = l; i <= r; ++i)

z[num[i]] = -INF;

qs(1, m);

opt[0] = 0;

for (int i = 1; i <= m; ++i)

if (e.find(x[ord[i]]) != e.find(y[ord[i]]))

{

if (z[ord[i]] != -INF)

opt[++opt[0]] = ord[i];

e.merge(x[ord[i]], y[ord[i]]);

}

e.action();

for (int i = 1; i <= opt[0]; ++i)

{

base_ans += z[opt[i]];

e.merge(x[opt[i]], y[opt[i]]);

}

int cnt_rem = e.cnt;

// Start Reduction

for (int i = l; i <= r; ++i)

z[num[i]] = INF;

qs(1, m);

opt[0] = 0;

for (int i = 1; i <= m; ++i)

if (e.find(x[ord[i]]) != e.find(y[ord[i]]))

e.merge(x[ord[i]], y[ord[i]]);

else if (z[ord[i]] != INF)

Page 25: IOI2011 中国国家集训队第二次作业 第二部分 自选题推荐 · ioi2011 中国国家集训队第二次作业 第二部分: 自选题推荐 湖南 雅礼中学 何朴藩

opt[++opt[0]] = i;

sort(opt + 1, opt + 1 + opt[0]);

for (int i = opt[0]; i; --i)

swap(ord[opt[i]], ord[m--]);

e.action(cnt_rem);

// Recover edge costs

for (int i = l; i <= r; ++i)

z[num[i]] = zz[num[i]];

work(dep + 1, l, (l + r) / 2);

work(dep + 1, (l + r) / 2 + 1, r);

// Recover All

e.action();

m = m_rem;

base_ans = base_rem;

}

int main()

{

freopen("input.txt", "r", stdin);

freopen("output.txt", "w", stdout);

scanf("%d%d%d", &n, &m, &q);

for (int i = 1; i <= m; ++i)

scanf("%d%d%d", x + i, y + i, zz + i), ord[i] = i, z[i] = zz[i];

for (int i = 1; i <= q; ++i)

scanf("%d%d", num + i, to + i);

if (q) work(0, 1, q);

fclose(stdin);

fclose(stdout);

return 0;

}

Page 26: IOI2011 中国国家集训队第二次作业 第二部分 自选题推荐 · ioi2011 中国国家集训队第二次作业 第二部分: 自选题推荐 湖南 雅礼中学 何朴藩

G.弹飞绵羊(bounce)

[相关信息]

传统型,bounce.c/cpp/pas,input.txt,output.txt

关键字:括号序列,动态树,分块

[题目大意]

一个数列 k0..N-1,当绵羊落在 i位置时它会被弹到 i+ki位置去。它会不断被弹,

一直到它的位置≥N。

有 M个操作,每次询问落到某点的绵羊会被弹多少次,或者修改 k的一项数值。

20%: N,M≤10000

100%:N≤200000,M≤100000

[资源限制]

每个测试点 1s 512MB

[分析]

不难发现,这题是一道动态树模型:如果落到 a后会被弹到 b,我们把 a连一条边

到 b,根节点表示出界。整个数列就会变成一棵树,操作就是修改父亲指针,询问就是

求深度。

可以参见经典的 Link-Cut Tree算法。

另一种方法是:维护树的括号序列,即 DFS遍历,进是“(”出是“)”。

每次询问相当于是问括号的层数。

每次修改是把一段括号序列进行移动。可以用 splay来维护。

以上算法时间复杂度均为 O(N+MlogN)。

这里提供一个更简单的方法:分块处理。

我们从 0开始,把连续的 D个归为一块。出界视为单独一块。

预处理并维护 next[i]和 value[i]分别表示:

从 i弹到一个最早的不同的块中的位置,以及相应要弹的次数。

这样每次询问只需要 O(N/D)次走 next边就可以得到答案,而每次修改只需要修

改同块内的 next和 value,为 O(D)。

最坏复杂度:O(N+Mlog(max(N/D,D)))

我们知道 max(N/D,D)≥N0.5恒成立。不妨取 D=N

0.5左右的常数,这样就得到了

一个不用高级数据结构的算法。

Page 27: IOI2011 中国国家集训队第二次作业 第二部分 自选题推荐 · ioi2011 中国国家集训队第二次作业 第二部分: 自选题推荐 湖南 雅礼中学 何朴藩

[算法总结]

输入,预处理分块。

处理所有操作:

如果修改,从后往前更新当前块元素的属性值。

如果询问,则循环求解。

时间复杂度: O(N+MN0.5

)

空间复杂度: O(N)

注意用预处理的方法避免取模和除法。

[参考程序] bounce.cpp 73L 1281C

#include <cstdio>

#include <cstring>

#include <algorithm>

using namespace std;

const int N = 222222;

const int D = 731;

int n, k, t, x, y, a[N], to[N], nx[N];

int belong[N], l[N], bv;

inline void maintain(int v, int d)

{

nx[v] = a[v] = d;

to[v] = 1;

bv = belong[v];

if (belong[nx[v]] == bv)

{

to[v] += to[nx[v]];

nx[v] = nx[nx[v]];

}

for (int i = v - 1; i >= l[v]; --i)

if (belong[a[i]] == bv)

{

to[i] = 1 + to[a[i]];

nx[i] = nx[a[i]];

}

}

int main()

{

freopen("input.txt", "r", stdin);

freopen("output.txt", "w", stdout);

Page 28: IOI2011 中国国家集训队第二次作业 第二部分 自选题推荐 · ioi2011 中国国家集训队第二次作业 第二部分: 自选题推荐 湖南 雅礼中学 何朴藩

scanf("%d", &n);

for (int i = 0; i < n; ++i)

{

scanf("%d", a + i);

if (i + a[i] > n)

a[i] = n; else a[i] += i;

belong[i] = i / D;

l[i] = belong[i] * D;

nx[i] = a[i];

to[i] = 1;

}

for (int i = n - 1; i >= 0; --i)

if (belong[a[i]] == belong[i])

{

to[i] = 1 + to[a[i]];

nx[i] = nx[a[i]];

}

scanf("%d", &k);

for (int i = 0; i < k; ++i)

{

scanf("%d", &t);

if (t == 1)

{

scanf("%d", &x);

t = 0;

while (x < n)

t += to[x], x = nx[x];

printf("%d\n", t);

} else

{

scanf("%d%d", &x, &y);

if (x + y > n)

y = n; else y += x;

maintain(x, y);

}

}

fclose(stdin);

fclose(stdout);

return 0;

}

Page 29: IOI2011 中国国家集训队第二次作业 第二部分 自选题推荐 · ioi2011 中国国家集训队第二次作业 第二部分: 自选题推荐 湖南 雅礼中学 何朴藩

H.矩阵(matrix)

[相关信息]

传统型,matrix.c/cpp/pas,input.txt,output.txt

关键字:搜索优化

[题目大意]

矩阵 A和矩阵 S都是 N*M的非负整数矩阵。

矩阵 A的元素都小于 P

矩阵 S的第一行和第一列均为 0;

且满足 Si,j=Ai,j+Ai-1,j+Ai,j-1+Ai-1,j-1(i,j>1)

给出 N,M,P,以及矩阵 S

求字典序最小的矩阵 A使得满足条件。

30%: N,M≤10

另 30%:P=2

100%: 1<N,M≤200 1<P≤10

[资源限制]

每个测试点 1s 512MB

[分析]

首先可以推出,一旦 A 的第一行与第一列已经确定,那么我们就可以按部就班地

算出 A的所有元素的值了。这样一来需要决策的元素个数就从 2002变成了 399。

为了方便计算,我们试图建立 Ai,j与第一行或第一列之间的直接等式关系。

以下是一个较易理解的建立等量关系的方法:

首先不看元素在 0到 P-1范围内的限制,设 A1,k和 Ak,1都为 0,并计算将 A补全。

(此时 A中可能有负数或者大于等于 P的整数)

S:

0 0 0

0 4 5

0 5 3

Page 30: IOI2011 中国国家集训队第二次作业 第二部分 自选题推荐 · ioi2011 中国国家集训队第二次作业 第二部分: 自选题推荐 湖南 雅礼中学 何朴藩

A:

0 0 0

0 4 1

0 1 -3

当我们改变 A1,1+=k时,要把 A调整成合法矩阵,事实上只需把 i+j为偶数的 Ai,j

全部增加 k,i+j为奇数的 Ai,j全部减去 k即可。

A’: A1,1+=1

1 -1 1

-1 5 0

1 0 -2

当我们把 A1,i增加 k时,要把 A调整成合法,只需要把第 i列奇数位加 k,偶数位

减 k即可。

A’: A1,2+=1

0 1 0

0 3 1

0 2 -3

修改 Ai,1类似。

设通过令第一行第一列都为 0得到的矩阵为 C,

那么有公式:

Ai,j = Ci,j + (-1)i+j-2

A1,1 + (-1)i-1A1,j + (-1)

j-1Ai,1

(i>1,j>1)

接下来只需要考虑对第一行第一列进行决策即可。

前 30%的数据可以搜索解决。

另 30%实际上是一个 2-Sat.问题。参见 SGU 307 Cipher。

对于 100%的数据并没有很好的方法。

研究 30%的搜索方法,如果加入较强的剪枝,一般出解是很快的。

我们只需要搜索 A的第一行,每当确定一个元素,就可以更新 Aj,1的取值范围。

根据公式,除了 A1,1以外,Ai,1的取值是互不影响的。

不断更新第一列每个元素的可以取值的范围,一旦出现有某个元素下界大于上界的

情况就剪枝。

显然这样搜索的字典序是最优的。

事实证明这个剪枝效率非常高。

Page 31: IOI2011 中国国家集训队第二次作业 第二部分 自选题推荐 · ioi2011 中国国家集训队第二次作业 第二部分: 自选题推荐 湖南 雅礼中学 何朴藩

[算法总结]

输入

令第一行第一列为 0,构造矩阵 C

枚举 A[1][1]

如果按顺序搜索第一行发现有解

输出,退出算法

[参考程序] matrix.cpp 75L 1596C

#include <iostream>

#include <cstring>

#include <algorithm>

using namespace std;

const int N = 222;

int s[N][N], c[N][N], a[N][N], n, m, p;

int l[N][N], r[N][N];

inline int sign(int a)

{

return a & 1 ? -1 : 1;

}

inline int num(int i, int j)

{

return c[i][j] + a[0][0] * sign(i + j + 1) + a[0][j] * sign(i) + a[i][0]

* sign(j);

}

bool dfs(int j)

{

if (j == m)

return true;

for (a[0][j] = 0; a[0][j] < p; ++a[0][j])

{

bool ok = true;

for (int i = 1; i < n; ++i)

{

int tl, tr;

tl = (c[i][j] + a[0][0] * sign(i + j + 1) + a[0][j] * sign(i)

- 0) * (-1) * sign(j);

tr = (c[i][j] + a[0][0] * sign(i + j + 1) + a[0][j] * sign(i)

- (p - 1)) * (-1) * sign(j);

if (tl > tr) swap(tl, tr);

l[j][i] = max(l[j - 1][i], tl);

r[j][i] = min(r[j - 1][i], tr);

Page 32: IOI2011 中国国家集训队第二次作业 第二部分 自选题推荐 · ioi2011 中国国家集训队第二次作业 第二部分: 自选题推荐 湖南 雅礼中学 何朴藩

if (l[j][i] > r[j][i])

{

ok = false;

break;

}

}

if (ok)

if (dfs(j + 1))

return true;

}

return false;

}

int main()

{

freopen("input.txt", "r", stdin);

freopen("output.txt", "w", stdout);

cin >> n >> m >> p;

for (int i = 0; i < n; ++i)

for (int j = 0; j < m; ++j)

{

cin >> s[i][j];

if (i && j)

c[i][j] = s[i][j] - c[i - 1][j] - c[i][j - 1] - c[i - 1][j

- 1];

l[j][i] = 0;

r[j][i] = p - 1;

}

for (a[0][0] = 0; a[0][0] < p; ++a[0][0])

if (dfs(1))

{

for (int i = 1; i < n; ++i)

a[i][0] = l[m - 1][i];

for (int i = 0; i < n; ++i)

for (int j = 0; j < m; ++j)

printf("%d%s", num(i, j), j + 1 == m ? "\n" : " ");

break;

}

fclose(stdin);

fclose(stdout);

return 0;

}

Page 33: IOI2011 中国国家集训队第二次作业 第二部分 自选题推荐 · ioi2011 中国国家集训队第二次作业 第二部分: 自选题推荐 湖南 雅礼中学 何朴藩

【感谢】 2011/04/20

中国计算机学会

湖南省信息学竞赛委员会

雅礼中学 张建平老师, 朱全民老师, 郑理安老师, 许春阳老师

清华大学 胡伟栋, 唐文斌, 莫涛, 漆子超, 陈丹琪

长郡中学 胡霄俊

雅礼中学 杨天 钟沛林 杨思逸