「NOIP模拟赛」Round 3
Tag
计数+LIS, 二分+ST表, 计数+记搜
A. 改造二叉树
Description
Solution
如果目标序列非严格递增,或者说目标序列是不下降的,那么答案就是 \(n\) 减去最长不下降子序列的长度。
比如这种情况:\(2\ 3\ 1\ 4\),\(LIS\) 为 \(2\ 3\ 4\),答案求出来为 \(1\),但由于整数的限制,应该要修改 \(2\) 次。即直接 \(LIS\) 求出的答案是在非严格递增的情况下的答案。
现在要求目标序列严格递增,一个常见的将严格递增整数序列映射成非严格递增整数序列的技巧就是将如下序列:
\(a_1, a_2, a_3, a_4 ... a_n\)
映射成:
\(a_1 - 1, a_2 - 2, a_3 - 3, a_4 - 4 ... a_n - n\)
这种方法常见于计数类问题。
这样映射后求最长不下降子序列的长度就没问题了。
考虑证明这个做法的正确性,如果 \(O(n^2)\) 转移 \(LIS\) 的话,每次从位置 \(i\) 转移到位置 \(j\) 时,只有满足 \(a[j]-a[i] \geq j-i\) 才能转移,而整体减去 \(\{1, 2, 3, ..., n\}\) 正是对这个限制的体现。
#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
using namespace std;
const int N = 1e5 + 3;
int n, fa, d, sum, qr, l, r, mid, top, stk[N], f[N], a[N], b[N], lc[N], rc[N];
bool vis[N];
char ch;
int read() {
while (ch = getchar(), ch < '0' || ch > '9');
int res = ch - 48;
while (ch = getchar(), ch >= '0' && ch <= '9') res = res * 10 + ch - 48;
return res;
}
void Bfs() {
int x; stk[top = 1] = 1;
while (top) {
x = stk[top];
if (lc[x] && !vis[lc[x]]) {
stk[++top] = lc[x];
continue;
}
b[++sum] = a[x]; b[sum] -= sum;
vis[x] = true; --top;
if (rc[x] && !vis[rc[x]]) {
stk[++top] = rc[x];
continue;
}
}
return ;
}
int main() {
freopen("binary.in", "r", stdin);
freopen("binary.out", "w", stdout);
n = read();
for (int i = 1; i <= n; ++i) a[i] = read();
for (int i = 2; i <= n; ++i) {
fa = read(); d = read();
(d ? rc[fa] : lc[fa]) = i;
}
Bfs();
f[qr = 1] = b[1];
for (int i = 2; i <= n; ++i) {
if (b[i] >= f[qr]) f[++qr] = b[i];
else {
l = 1; r = qr;
while (l <= r) {
mid = l + r >> 1;
if (f[mid] <= b[i]) l = mid + 1;
else r = mid - 1;
}
f[l] = b[i];
}
}
cout << n - qr << endl;
fclose(stdin); fclose(stdout);
return 0;
}
B. 数字对
Description
Solution 1
二分区间长度,枚举左端点,显然可能的 \(a_k\) 就是区间最小值,判断 \(a_k\) 是否能整除这个区间的所有数就是判断 \(a_k\) 是否与这个区间的 \(gcd\) 相等,\(ST\)表维护区间最小值和区间\(gcd\)。
#include <cstdio>
#include <cmath>
const int N = 500005;
int f[N][21], g[N][21], ans[N], n; //区间最小值,区间gcd
int read() {
int x = 0; char c = getchar();
while (c < '0' || c > '9') c = getchar();
while (c >= '0' && c <= '9') {
x = (x << 3) + (x << 1) + (c ^ 48);
c = getchar();
}
return x;
}
int min(int x, int y) {
return x < y ? x : y;
}
int gcd(int a, int b) {
return !b ? a : gcd(b, a % b);
}
int Min(int l, int r) {
int k = log2(r - l + 1);
return min(f[l][k], f[r-(1<<k)+1][k]);
}
int Gcd(int l, int r) {
int k = log2(r - l + 1);
return gcd(g[l][k], g[r-(1<<k)+1][k]);
}
bool check(int k) {
int sta[N] = {}, top = 0;
for (int i = 1; i + k - 1 <= n; ++i)
if (Min(i, i + k - 1) == Gcd(i, i + k - 1))
sta[++top] = i;
if (!top) return false;
ans[0] = top;
for (int i = 1; i <= top; ++i) ans[i] = sta[i];
return true;
}
int main() {
freopen("pair.in", "r", stdin);
freopen("pair.out", "w", stdout);
n = read();
for (int i = 1; i <= n; ++i) f[i][0] = g[i][0] = read();
for (int j = 1; j <= 19; ++j)
for (int i = 1; i + (1 << j) - 1 <= n; ++i) {
f[i][j] = min(f[i][j-1], f[i+(1<<j-1)][j-1]);
g[i][j] = gcd(g[i][j-1], g[i+(1<<j-1)][j-1]);
}
int l = 0, r = n;
while (l < r) { //二分区间长度
int mid = l + (r - l + 1 >> 1);
if (check(mid)) l = mid;
else r = mid - 1;
}
printf("%d %d\n", ans[0], l - 1);
for (int i = 1; i <= ans[0]; ++i) printf("%d ", ans[i]);
fclose(stdin);
fclose(stdout);
return 0;
}
Solution 2
然后还跟 \(Asia\) 学了一个除排序以外 \(O(n)\) 的做法…… Orz
大体思路是:
先从小到大排序,然后一个一个作为 \(a_k\) 向左右两边拓展
拓展到的点打上标记,表示不会从这个点开始拓展
这样的话每个点最多只会被它左边或者右边的一个点拓展到,所以复杂度是 \(O(n)\) 的
为什么是 \(O(n)\) 的呢,考虑三个位置 \(x,y,z\),\(y \leq x, z = x * y\),现用 \(x,y\) 去拓展 \(z\),如果 \(x\) 是 \(y\) 的倍数,那么 \(x\) 会被 \(y\) 拓展到,也就不能再去拓展 \(z\) 了;如果 \(x\) 不是 \(y\) 的倍数,\(x\) 拓展到 \(y\) 就会停止,也不会去拓展 \(z\)。
由此可知,每个元素只会被它左边的一个点和它右边的一个点拓展到,所以除排序外的复杂度为 \(O(n)\)。
C. 交换
Description
给定一个 \(\{0, 1, 2, 3, … , n - 1\}\) 的排列 \(p\)。一个 \(\{0, 1, 2 , … , n - 2\}\) 的排列 \(q\) 被认为是优美的排列,当且仅当 \(q\) 满足下列条件:
对排列 \(s = \{0, 1, 2, 3, ..., n - 1\}\) 进行 \(n – 1\) 次交换。
- 交换 \(s[q_0],s[q_0 + 1]\)
- 交换 \(s[q_1],s[q_1 + 1]\)
- …
最后能使得排列 \(s = p\)。
问有多少个优美的排列,答案对 \(10^9+7\) 取模。\(n \leq 50\)
Solution
一个很厉害的计数\(DP\)。
考虑倒着处理, 比如交换 \((i, i + 1)\), 那么前面的所有数不管怎么交换都无法到后面去,后面的数也是一样到不了前面。说明这最后一次交换前,就要求对于所有的 \(x <= i, y > i\),\(p_x<p_y\)。所以交换前左边的数是连续的,右边也是连续的。由于交换前,前面和后面的数是互相不干涉的,所以就归结成了两个子问题。于是我们可以用记忆化搜索来解决这个问题。
设 \(dp[n][low]\) 代表长度为 \(n\),\(H\) 是 \(\{low, low + 1,…,low + n - 1\}\) 的排列,且 \(H\) 是 \(p\) 的子序列,在 \(H\) 上优美序列的个数。
我们枚举交换哪两个相邻元素 \((k,k+1)\), 然后判断 \(k\) 前面的所有数是否都小于后面的所有数,如果是则进行转移
即前面的 \(k\) 个元素与后面的 \(n - k\) 个元素是两个独立的子问题,前面是 \(\{low ... low + k - 1\}\) 的排列,后面是 \(\{low + k ... low + n - 1\}\) 的排列,\(C(n - 2, k - 1)\) 代表的是在交换 \((k, k + 1)\) 前左右两边一共还要进行 \(n - 2\) 次交换,而每次交换左边与交换右边是不同方案,这相当于 \(n - 2\) 个位置选择 \(k - 1\) 个位置填入,故还需要乘上 \(C(n - 2, k - 1)\)。
时间复杂度为 \(O(n^4)\)。
#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
using namespace std;
typedef long long ll;
const int N = 52, Mod = 1e9 + 7;
int n, p[N], dp[N][N], C[N][N];
int Dfs(int len, int low) {
if (dp[len][low] != -1) return dp[len][low];
if (len == 1) return dp[len][low] = 1;
int &res = dp[len][low]; res = 0;
int t[N], m = 0, j, k;
for (int i = 1; i <= n; ++i)
if (p[i] >= low && p[i] < low + len)
t[++m] = p[i];
for (int i = 1; i < m; ++i) {
swap(t[i], t[i + 1]);
for (j = 1; j <= i; ++j)
if (t[j] >= low + i) break;
for (k = i + 1; k <= m; ++k)
if (t[k] < low + i) break;
if (j > i && k > m) {
ll tmp = (ll)Dfs(i, low) * Dfs(m - i, low + i) % Mod;
tmp = tmp * C[m - 2][i - 1] % Mod;
res = (res + tmp) % Mod;
}
swap(t[i], t[i + 1]);
}
return res;
}
int main() {
freopen("swap.in", "r", stdin);
freopen("swap.out", "w", stdout);
scanf("%d", &n);
for (int i = 1; i <= n; ++i) scanf("%d", &p[i]);
memset(dp, -1, sizeof(dp));
for (int i = 0; i <= n; ++i) {
C[i][0] = 1;
for (int j = 1; j <= i; ++j)
C[i][j] = (C[i - 1][j - 1] + C[i - 1][j]) % Mod;
}
Dfs(n, 0);
if (dp[n][0] != -1) cout << dp[n][0] << endl;
else puts("0");
fclose(stdin); fclose(stdout);
return 0;
}