IOI2011 中国国家集训队第二次作业 第二部分: 自选题推荐 湖南 雅礼中学 何朴藩 教练:胡伟栋 唐文斌 HNOI 2010 解题报告 【目录】 编号 名称 英文名 参考难度 A. 合唱队 chorus ★ B. 平面图判定 planar ★★ C. 物品调度 fsk ★★★★ D. 公交路线 bus ★★★ E. 取石子游戏 stone ★★★★★ F. 城市建设 city ★★★★★ G. 弹飞绵羊 bounce ★★★★ H. 矩阵 matrix ★★★
IOI2011 中国国家集训队第二次作业
第二部分: 自选题推荐
湖南 雅礼中学 何朴藩
教练:胡伟栋 唐文斌
HNOI 2010 解题报告
【目录】
编号 名称 英文名 参考难度
A. 合唱队 chorus ★
B. 平面图判定 planar ★★
C. 物品调度 fsk ★★★★
D. 公交路线 bus ★★★
E. 取石子游戏 stone ★★★★★
F. 城市建设 city ★★★★★
G. 弹飞绵羊 bounce ★★★★
H. 矩阵 matrix ★★★
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
时间复杂度: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;
}
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 再用二分图算法做。这样边就很少了,可以保证
效率。
[算法总结]
对于每组数据:
输入并判断边数上界
枚举非哈密顿圈上的边,建立约束关系图
判断约束关系图是否为二分图
输出
时间复杂度: 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()
{
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]))
{
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;
}
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,说明不需要任何操作就可以还原。
第一个轮换[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
初始时所有元素都指向自己。
每次填入一个数时修改 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;
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);
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;
}
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
这样一来我们容易想到用矩阵乘法来优化递推。
[算法总结]
对于每组数据:
输入 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;
}
}
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;
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
[分析]
本题和一般的博弈问题不一样。本题不讨论输赢,只让选手得到尽量多的石子。
由于双方最终石子数之和是确定的,双方的目标就是使自己-别人的石子数差最大
化。
首先我们可以抽象问题:
有两个栈,若干个双头队列,总长度不超过 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)的。
[算法总结]
输入数列,计算和
建立链表
不断进行化简直到不可化简
对剩余元素排序,从大到小:奇数位和偶数位分别归属于先手和后手
根据和与差计算双方各自最终石子数
时间复杂度: 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)
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];
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
[资源限制]
每个测试点 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
可以证明:
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;
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];
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)
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;
}
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左右的常数,这样就得到了
一个不用高级数据结构的算法。
[算法总结]
输入,预处理分块。
处理所有操作:
如果修改,从后往前更新当前块元素的属性值。
如果询问,则循环求解。
时间复杂度: 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);
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;
}
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
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的取值是互不影响的。
不断更新第一列每个元素的可以取值的范围,一旦出现有某个元素下界大于上界的
情况就剪枝。
显然这样搜索的字典序是最优的。
事实证明这个剪枝效率非常高。
[算法总结]
输入
令第一行第一列为 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);
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;
}
【感谢】 2011/04/20
中国计算机学会
湖南省信息学竞赛委员会
雅礼中学 张建平老师, 朱全民老师, 郑理安老师, 许春阳老师
清华大学 胡伟栋, 唐文斌, 莫涛, 漆子超, 陈丹琪
长郡中学 胡霄俊
雅礼中学 杨天 钟沛林 杨思逸