全国信息学奥林匹克联赛(NOIP2014)复赛 模拟题Day2 长乐一中
题目名称 |
改造二叉树 |
数字对 |
交换 |
英文名称 |
binary |
pair |
swap |
输入文件名 |
binary.in |
pair.in |
swap.in |
输出文件名 |
binary.out |
pair.out |
swap.out |
时间限制 |
1s |
2s |
1s |
空间限制 |
256M |
256M |
256M |
测试点数目 |
20 |
20 |
10 |
测试点分值 |
5 |
5 |
10 |
是否有部分分 |
无 |
无 |
无 |
题目类型 |
传统 |
传统 |
传统 |
是否有SPJ |
无 |
无 |
无 |
1.改造二叉树
【题目描述】
小Y在学树论时看到了有关二叉树的介绍:在计算机科学中,二叉树是每个结点最多有两个子结点的有序树。通常子结点被称作“左孩子”和“右孩子”。二叉树被用作二叉搜索树和二叉堆。随后他又和他人讨论起了二叉搜索树。
什么是二叉搜索树呢?二叉搜索树首先是一棵二叉树。设key[p]表示结点p上的数值。对于其中的每个结点p,若其存在左孩子lch,则key[p]>key[lch];若其存在右孩子rch,则key[p]<key[rch];注意,本题中的二叉搜索树应满足对于所有结点,其左子树中的key小于当前结点的key,其右子树中的key大于当前结点的key。
小Y与他人讨论的内容则是,现在给定一棵二叉树,可以任意修改结点的数值。修改一个结点的数值算作一次修改,且这个结点不能再被修改。若要将其变成一棵二叉搜索树,且任意时刻结点的数值必须是整数(可以是负整数或0),所要的最少修改次数。
相信这一定难不倒你!请帮助小Y解决这个问题吧。
【输入格式】
第一行一个正整数n表示二叉树结点数。结点从1~n进行编号。
第二行n个正整数用空格分隔开,第i个数ai表示结点i的原始数值。
此后n - 1行每行两个非负整数fa, ch,第i + 2行描述结点i + 1的父亲编号fa,以及父子关系ch,(ch = 0 表示i + 1为左儿子,ch = 1表示i + 1为右儿子)。
结点1一定是二叉树的根。
【输出格式】
仅一行包含一个整数,表示最少的修改次数。
【样例输入】
3
2 2 2
1 0
1 1
【样例输出】
2
【数据范围】
20 % :n <= 10 , ai <= 100.
40 % :n <= 100 , ai <= 200
60 % :n <= 2000 .
100 % :n <= 10 ^ 5 , ai < 2 ^ 31.
T1:
20% :暴力。 40% :可以用 DP 或者贪心或者神奇的暴力等其他奇怪的方法完成。 60% :正解的 LIS 打成 O(n ^ 2)。 100% :首先求出这颗二叉树的中序遍历,那么问题就转换成用最少的修改次数使这个整 数序列严格单调递增。于是很自然的想到了 LIS,但单纯用 LIS 是有一些问题的, 比如这种情况:2 3 1 4, LIS 为 2 3 4,答案求出来为 1,但由于整数的限制,应该 要修改 2 次。即直接 LIS 求出的答案是在非严格递增的情况下的答案。 所以我们将原序列稍加修改, 一个常见的将严格递增整数序列映射成非严格递增整 数序列的技巧就是将如下序列: a1, a2, a3, a4 ... an 映射成: a1 - 1, a2 - 2, a3 - 3, a4 - 4 ... an - n. (这种方法常见于计数类问题)。 这样映射后求最长不下降子序列的长度就没问题了。
1 #include <algorithm> 2 #include <iostream> 3 #include <cstring> 4 #include <cstdio> 5 #include <cmath> 6 using namespace std; 7 8 const int N = 1e5 + 3; 9 int n, fa, d, sum, qr, l, r, mid, top, stk[N], f[N], a[N], b[N], lc[N], rc[N]; 10 bool vis[N]; 11 12 char ch; 13 int read() { 14 while (ch = getchar(), ch < '0' || ch > '9'); 15 int res = ch - 48; 16 while (ch = getchar(), ch >= '0' && ch <= '9') res = res * 10 + ch - 48; 17 return res; 18 } 19 20 void Bfs() { 21 int x; stk[top = 1] = 1; 22 while (top) { 23 x = stk[top]; 24 if (lc[x] && !vis[lc[x]]) { 25 stk[++top] = lc[x]; 26 continue; 27 } 28 b[++sum] = a[x]; b[sum] -= sum; 29 vis[x] = true; --top; 30 if (rc[x] && !vis[rc[x]]) { 31 stk[++top] = rc[x]; 32 continue; 33 } 34 } 35 return ; 36 } 37 38 int main() { 39 freopen("binary.in", "r", stdin); 40 freopen("binary.out", "w", stdout); 41 n = read(); 42 for (int i = 1; i <= n; ++i) a[i] = read(); 43 for (int i = 2; i <= n; ++i) { 44 fa = read(); d = read(); 45 (d ? rc[fa] : lc[fa]) = i; 46 } 47 Bfs(); 48 f[qr = 1] = b[1]; 49 for (int i = 2; i <= n; ++i) { 50 if (b[i] >= f[qr]) f[++qr] = b[i]; 51 else { 52 l = 1; r = qr; 53 while (l <= r) { 54 mid = l + r >> 1; 55 if (f[mid] <= b[i]) l = mid + 1; 56 else r = mid - 1; 57 } 58 f[l] = b[i]; 59 } 60 } 61 cout << n - qr << endl; 62 fclose(stdin); fclose(stdout); 63 return 0; 64 }
2.数字对
【题目描述】
小H是个善于思考的学生,现在她又在思考一个有关序列的问题。
她的面前浮现出一个长度为n的序列{ai},她想找出一段区间[L, R](1 <= L <= R <= n)。
这个特殊区间满足,存在一个k(L <= k <= R),并且对于任意的i(L <= i <= R),ai都能被ak整除。这样的一个特殊区间 [L, R]价值为R - L。
小H想知道序列中所有特殊区间的最大价值是多少,而有多少个这样的区间呢?这些区间又分别是哪些呢?你能帮助她吧。
【输入格式】
第一行,一个整数n.
第二行,n个整数,代表ai.
【输出格式】
第一行两个整数,num和val,表示价值最大的特殊区间的个数以及最大价值。
第二行num个整数,按升序输出每个价值最大的特殊区间的L.
【样例输入1】
5
4 6 9 3 6
【样例输出1】
1 3
2
【样例输入2】
5
2 3 5 7 11
【样例输出2】
5 0
1 2 3 4 5
【数据范围】
30%: 1 <= n <= 30 , 1 <= ai <= 32.
60%: 1 <= n <= 3000 , 1 <= ai <= 1024.
80%: 1 <= n <= 300000 , 1 <= ai <= 1048576.
100%: 1 <= n <= 500000 , 1 <= ai < 2 ^ 31.
T2: 30% :暴力枚举判断。O(n^4)。 60% :特殊区间的特点实际上就是区间最小值等于这个区间的 GCD,于是暴力或递推算 出每个区间的最小值与 GCD。而对于最大价值,可以通过二分来进行求解。复杂 度 O(n ^ 2)。 100%:在 60%的基础上,最小值与 GCD 都使用 RMQ 算法来求解,对于这道题推荐使用 ST 表。最大价值仍然使用二分。复杂度 O(nlogn)。
1 #include <algorithm> 2 #include <iostream> 3 #include <cstring> 4 #include <cstdio> 5 #include <cmath> 6 using namespace std; 7 8 const int N = 5e5 + 3, M = 21; 9 int n, m, a, ans, l, r, mid, sum, A[N], f[N][M], g[N][M], p[M]; 10 11 inline int Gcd(const int &x, const int &y) { 12 return y == 0 ? x : Gcd(y, x % y); 13 } 14 15 bool check(int len) { 16 int q = log2(len--), k = n + 1 - p[q], j; 17 for (int i = 1; i <= k; ++i) { 18 j = i + len; 19 if (min(f[i][q], f[j - p[q] + 1][q]) == Gcd(g[i][q], g[j - p[q] + 1][q])) 20 return true; 21 } 22 return false; 23 } 24 25 char ch; 26 inline int read() { 27 while (ch = getchar(), ch < '0' || ch > '9'); 28 int res = ch - 48; 29 while (ch = getchar(), ch >= '0' && ch <= '9') res = res * 10 + ch - 48; 30 return res; 31 } 32 33 char s[10]; 34 inline void print(int x) { 35 int res = 0; 36 if (x == 0) putchar('0'); 37 while (x) { 38 s[++res] = x % 10; 39 x /= 10; 40 } 41 for (int i = res; i; --i) putchar(s[i] + '0'); 42 putchar(' '); 43 return ; 44 } 45 46 int main() { 47 freopen("pair.in", "r", stdin); 48 freopen("pair.out", "w", stdout); 49 n = read(); m = log2(n); 50 for (int i = 1; i <= n; ++i) { 51 a = read(); 52 f[i][0] = g[i][0] = a; 53 } 54 for (int i = 0; i <= m; ++i) p[i] = 1 << i; 55 for (int j = 1; j <= m; ++j) { 56 int k = n + 1 - p[j]; 57 for (int i = 1; i <= k; ++i) { 58 f[i][j] = min(f[i][j - 1], f[i + p[j - 1]][j - 1]); 59 g[i][j] = Gcd(g[i][j - 1], g[i + p[j - 1]][j - 1]); 60 } 61 } 62 l = 1; r = n; 63 while (l <= r) { 64 mid = l + r >> 1; 65 if (check(mid)) l = mid + 1; 66 else r = mid - 1; 67 } 68 ans = r; 69 if (ans == 1) { 70 printf("%d %d\n", n, 0); 71 for (int i = 1; i < n; ++i) 72 print(i); 73 printf("%d\n", n); 74 } 75 else { 76 int q = log2(ans--), k = n + 1 - p[q], j; 77 for (int i = 1; i <= k; ++i) { 78 j = i + ans; 79 if (min(f[i][q], f[j - p[q] + 1][q]) == Gcd(g[i][q], g[j - p[q] + 1][q])) 80 A[++sum] = i; 81 } 82 printf("%d %d\n", sum, ans); 83 for (int i = 1; i < sum; ++i) 84 print(A[i]); 85 printf("%d\n", A[sum]); 86 } 87 fclose(stdin); fclose(stdout); 88 return 0; 89 }
3.交换
【题目描述】
给定一个{0, 1, 2, 3, … , n - 1}的排列 p。一个{0, 1, 2 , … , n - 2}的排列q被认为是优美的排列,当且仅当q满足下列条件:
对排列s = {0, 1, 2, 3, ..., n - 1}进行n – 1次交换。
- 交换s[q0],s[q0 + 1]
- 交换s[q1],s[q1 + 1]
…
最后能使得排列s = p.
问有多少个优美的排列,答案对10^9+7取模。
【输入格式】
第一行一个正整数n.
第二行n个整数代表排列p.
【输出格式】
仅一行表示答案。
【样例输入】
3
1 2 0
【样例输出】
1
【样例解释】
q = {0,1} {0,1,2} ->{1,0,2} -> {1, 2, 0}
q = {1,0} {0,1,2} ->{0,2,1} -> {2, 0, 1}
【数据范围】
30%: n <= 10
100%: n <= 50
T3: 30%: 枚举所有排列,判定即可。 100%: 考虑倒着处理, 比如交换 (i, i + 1), 那么前面的所有数不管怎么交换都无法到后面去(下 标恒小于等于 i),后面的数也是一样到不了前面。说明这最后一次交换前,就要求对于所有 的 x <= i, y > i,px<py。所以交换前左边的数是连续的,右边也是连续的。由于交换前, 前 面和后面的数是互相不干涉的, 所以就归结成了两个子问题。 于是我们可以用记忆化搜索来 解决这个问题。 设 dp[n][low] 代表长度为 n,H 是{low, low + 1,…,low + n - 1}的排列,且 H 是 p 的子序 列,在 H 上优美序列的个数。 我们枚举交换哪两个相邻元素(k,k+1), 然后判断 k 前面的所有数是否都小于后面的所有 数,如果是则进行转移 dp[n][low] += dp[k][low] * dp[n – k][low + k ] * C(n – 2, n – 1 - k)。 即前面的 k 个元素与后面的 n - k 个元素是两个独立的子问题,前面是{low ... low + k - 1}的 排列,后面是{low + k ... low + n - 1}的排列,C(n - 2, n - 1 - k)代表的是在交换(k, k + 1)前左 右两边还分别要进行 n - 2 次交换,而每次交换左边与交换右边是不同方案,这相当于 n - 2 个位置选择 n - 1 - k 个位置填入,故还需要乘上 C(n - 2, n - 1 - k)。时间复杂度为 O(n^4)。
1 #include <algorithm> 2 #include <iostream> 3 #include <cstring> 4 #include <cstdio> 5 #include <cmath> 6 using namespace std; 7 8 typedef long long ll; 9 const int N = 52, Mod = 1e9 + 7; 10 int n, p[N], dp[N][N], C[N][N]; 11 12 int Dfs(int len, int low) { 13 if (dp[len][low] != -1) return dp[len][low]; 14 if (len == 1) return dp[len][low] = 1; 15 int &res = dp[len][low]; res = 0; 16 int t[N], m = 0, j, k; 17 for (int i = 1; i <= n; ++i) 18 if (p[i] >= low && p[i] < low + len) 19 t[++m] = p[i]; 20 for (int i = 1; i < m; ++i) { 21 swap(t[i], t[i + 1]); 22 for (j = 1; j <= i; ++j) 23 if (t[j] >= low + i) break; 24 for (k = i + 1; k <= m; ++k) 25 if (t[k] < low + i) break; 26 if (j > i && k > m) { 27 ll tmp = (ll)Dfs(i, low) * Dfs(m - i, low + i) % Mod; 28 tmp = tmp * C[m - 2][i - 1] % Mod; 29 res = (res + tmp) % Mod; 30 } 31 swap(t[i], t[i + 1]); 32 } 33 return res; 34 } 35 36 int main() { 37 freopen("swap.in", "r", stdin); 38 freopen("swap.out", "w", stdout); 39 scanf("%d", &n); 40 for (int i = 1; i <= n; ++i) scanf("%d", &p[i]); 41 memset(dp, -1, sizeof(dp)); 42 for (int i = 0; i <= n; ++i) { 43 C[i][0] = 1; 44 for (int j = 1; j <= i; ++j) 45 C[i][j] = (C[i - 1][j - 1] + C[i - 1][j]) % Mod; 46 } 47 Dfs(n, 0); 48 if (dp[n][0] != -1) cout << dp[n][0] << endl; 49 else puts("0"); 50 fclose(stdin); fclose(stdout); 51 return 0; 52 }