非常好 hfu 开发的神秘打一场比赛改两场题,使我的国庆假期旋转.
CF2129 Div1
B
逆序对考虑在较小的数处统计贡献,枚举序列里面每个数,一并枚举逆序对的另一个数. \(p_i\) 有贡献当且 \(i>j\),\(2n-p_i\) 有贡献当且仅当 \(j<i\),取较小值即可.
C
构造交互,畏惧了.
注意询问可以选择相同的下标,返回的是合法子串的数量.
如果我们知道了一个左括号或一个右括号的位置,我们可以通过构造来尝试一次询问得到多个位置的信息. 一次询问形如:
其中 \((s_i\) 重复 \(x\) 次,如果 \(s_i\) 为 \()\) 贡献为 \({x(x-1)\over2}\),反之贡献为 \(0\). 为了能够根据询问信息正确解码信息,我们需要确定恰当的重复次数使得子集 \(\sum{x(x-1)\over2}\) 不等于另一个 \({y(y-1)\over2}\). 这个东西可以预先打表出来,每次最多询问 \(13\) 个位置的信息. 所以操作次数一部分是 \(\lceil{1000\over13}\rceil\). 为了确定一个左/右括号可以二分最小的前缀有 \(1\) 贡献的,当然可以直接拿 \(s_1\) 作为分隔符代替 \((\) 或 \()\),因为最后可以拿贡献为 \(0\) 的 \(O(1)\) 判断 \(s_1\) 是啥,然后反推回去得到整个串.
D
神秘区间 DP.
直接对 \(p_i\) 计数不好做,考虑为每个位置安排贡献. 一个重要的性质是当 \(x\) 位置被染色,那么左右的贡献就独立了,只能区间内的相互贡献,考虑用区间 DP 求解.
设 \(f_{i,j,a,b}\) 表示钦定区间 \([i,j]\) 贡献了 \(a\) 分到 \(l-1\),\(b\) 分到 $r+1,并且内部所有限制处理好的方案数. 考虑区间中 \(k\) 位置的限制怎么满足,显然转移是从 \(f_{i,k-1,a1,b1}\) 与 \(f_{k+1,r,a2,b2}\). 如果 \(s_k\neq-1\),当且仅当 \(s_k=b_1+a_2\) 可以转移,否则都可以转移. 考虑贡献有先后顺序,两个子问题贡献到大问题,实际上随意交换顺序是不会改变的,所以乘上组合数 \({r-l\choose k-l}\). 即可.
看起来时空复杂度爆炸,然而每个位置被贡献 \(O(\log n)\) 次之后可贡献的范围就会退化成单点,所以时间复杂度是 \(O(n^3\log^3 n)\),空间复杂度 \(O(n^2\log^2 n)\).
点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;const int maxn = 1e2 + 10, maxv = 2e2, mo = 998244353;
int T, n, s[maxn];
int f[maxn][maxn][12][12];ll fac[maxn << 1], ifac[maxn << 1];
ll qpow(ll x, ll y) {ll res = 1;while(y) {if(y & 1) (res *= x) %= mo;(x *= x) %= mo, y >>= 1;} return res;
}
void init() {fac[0] = ifac[0] = 1; for(int i = 1; i <= maxv; i++) fac[i] = fac[i - 1] * i % mo;ifac[maxv] = qpow(fac[maxv], mo - 2); for(int i = maxv - 1; i >= 1; i--) ifac[i] = ifac[i + 1] * (i + 1) % mo;
}
inline ll C(const int &x, const int &y) {return fac[x] * ifac[y] % mo * ifac[x - y] % mo;}inline int add(const int &x, const int &y) {return x + y >= mo ? x + y - mo : (x + y < 0 ? x + y + mo : x + y);}
inline void upd(int &x, const int &y) {return x = add(x, y), void(0);}void clearr() {for(int l = 0; l <= n; l++) for(int r = max(l - 1, 0); r <= n; r++) for(int a = 0; a < 12; a++) for(int b = 0; b < 12; b++) f[l][r][a][b] = 0;
}
void solve() {scanf("%d", &n);for(int i = 1; i <= n; i++) scanf("%d", &s[i]);for(int i = 2; i <= n; i++) if(s[i] <= 0) f[i][i][1][0] = 1; if(s[1] <= 0) f[1][1][0][1] = 1;for(int i = 0; i <= n; i++) f[i + 1][i][0][0] = 1;for(int len = 2; len <= n; len++) {for(int l = 1; l + len - 1 <= n; l++) {int r = l + len - 1;for(int k = l; k <= r; k++) {int ul = 0, ur = 0;if(l == 1) ur = 1;else if(r == n || l + r >= 2 * k) ul = 1;else ur = 1;if(s[k] == -1) {for(int a1 = 0; a1 < 12; a1++) {for(int b2 = 0; b2 < 12; b2++) {int sum = 0;for(int b1 = 0; b1 < 12; b1++) upd(sum, f[l][k - 1][a1][b1]);for(int a2 = 0; a2 < 12; a2++) upd(f[l][r][a1 + ul][b2 + ur], 1ll * sum * f[k + 1][r][a2][b2] % mo * C(r - l, k - l) % mo);}}}else {for(int b1 = 0; b1 < 12; b1++) {if(s[k] - b1 < 0 || s[k] - b1 > 11) continue;int a2 = s[k] - b1;for(int a1 = 0; a1 < 12; a1++) {for(int b2 = 0; b2 < 12; b2++) upd(f[l][r][a1 + ul][b2 + ur], 1ll * f[l][k - 1][a1][b1] * f[k + 1][r][a2][b2] % mo * C(r - l, k - l) % mo);}}}}}} int ans = 0; for(int a = 0; a < 12; a++) for(int b = 0; b < 12; b++) upd(ans, f[1][n][a][b]);printf("%d\n", ans); return clearr();
}int main() {// ios :: sync_with_stdio(false); cin.tie(0); cout.tie(0);init();cin >> T; while(T--) solve();return 0;
}
CF1951 Div2+Div1
D
神秘 ad-hoc,数据放二进制构造误导向,结果正解是 \(O(1)\).
分类讨论 \(n,k\) 的关系:
- \(n<k\) 无解.
- \(n=k\),设置一个价格为 \(1\) 的商店.
- \(k<n<2k\),只有 \(n=2k-1\) 有解,容易构造.
- \(n\ge2k\) 第一个商店价格 \(n-k+1\),由于 \(2(n-k+1)>n\),所以必然会剩下 \(k-1\),第二个价格设 \(1\) 即可.
放这种题何意味?
E
比较好的一道构造题.
设 \(pos_i=p^{-1}_i\),即数 \(i\) 在 \(p\) 中的位置. 考虑 \(p\) 中逆序对,对答案的贡献,令 \(i<j\),有 \(p_i>p_j\),\(pos_{p_i}=i<j=pos_{p_j}\),即 \(pos_{p_i}<pos_{p_j}\). 由于 \(t\) 与 \(pos_t\) 构成双射,且大小关系不同,所以无论 \(q\) 怎么取必定会对答案造成 \(1\) 的贡献. 同理,\(p\) 中顺序对的贡献在 \(q_i<q_j\) 时为 \(0\),\(q_i>q_j\) 时为 \(2\).
于是我们可以判断是否有解了. 现在我们要构造一个排列使得恰有 \(c\) 个逆序对,不妨将值域划分成两部分,用较小的一部分凑出 \(c\) 个逆序对,较大的部分升序放在最后不对前面造成贡献. 但是看不懂,放弃了.