模拟赛 R16
R16 - A「弄潮铁血」
题意
凯撒喜欢和剑旗爵下棋,但今天她们想尝试一些新的游戏!
她们首先构思了一个经典的游戏:规定两个参数 \(n,k\),接着在地上摆放 \(n\) 块石子,两人轮流行动,每次行动需要移除至少一块,至多 \(k\) 块石子,无法行动的人输掉这局游戏。但这样显然太没有挑战性了,因此两人约定了 \(m\) 个两两不同的正整数 \(a_{1\cdots m}\),在轮到某人行动时,如果当前石子个数为某个 \(a_i\),则她会直接获得胜利。假设两人足够聪明,始终按最优策略行动,则凯撒定义 \(f(n,k)\) 为在她作为先手开始这局游戏时,她是否能取得胜利,若能则 \(f(n,k)\) 为 \(1\),否则为 \(-1\)。
现在给出正整数 \(N\),作为一个有大局观的君王,凯撒想要对所有 \(1\le n,k\le N\) 的数对 \((n,k)\) 都求出 \(f(n,k)\) 的值。但考虑到你不可能输出 \(N^2\) 个数,因此她只需要你求出 \(\sum_{n,k}f(n,k)\times (n\oplus k)\) 的值即可。其中 \(\oplus\) 为按位异或运算。
注意,对不同的 \((n,k)\),规定的 \(m\) 个 \(a_{1\cdots m}\) 都是相同的。
Solution
题外话:如果 \(m = 0\),这个东西叫经典 \(nim\) 博弈。
还是先考虑 \(m = 0\),发现 \(\text{N-Position}\) 是 \(\mod (k + 1) = 0\) 的那些。那么可以枚举 \(k\),暴力找到所有先手必败的 \(n\),将他们的贡献减掉。这个数量级(调和级数)是 \(O(N \log N)\)。
那么总的贡献 \(\sum_{n,k} n \oplus k\) 怎么算?二进制按位考虑,贡献是 \(\sum_j 2 ^ j(2 \times \text{#} x \in [1, n] \text{ satisfy the jth bit is 0}\times \text{#} x \in [1, n] \text{ satisfy the jth bit is 1})\),当然也可以继续枚举一个 \(i\) 算贡献。复杂度也是 \(O(N \log N)\)。
当 \(m \ge 1\) 时,我们发现枚举 \(k\), \(\text{N-Position}\) 的数量会更加少,因为我们强制另一些东西变成了先手必胜。我们在跳的过程中,如果发现本来跳到这个先手必败的 \(i\) 是 \(a\) 中的某个数,我们就强制让他跳到下一个不在 \(a\) 中的位置,这一位显然是先手必败的,然后继续跳 \(k + 1\) 即可。
Code
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 1e6 + 5;
int n, m, buc[21][2], ans, a[N], nxt[N];
signed main(){
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> m;
for(int i = 1; i <= m; ++i) cin >> a[i], nxt[a[i]] = -1;
nxt[n + 1] = n + 1;
for(int i = n; i >= 1; --i){
if(nxt[i] == -1) nxt[i] = nxt[i + 1];
else nxt[i] = i;
}
for(int i = 1; i <= n; ++i){
for(int j = 0; (1 << j) <= n; ++j){
if(i & (1 << j)) buc[j][1]++;
else buc[j][0]++;
}
}
for(int k = 1; k <= n; ++k){
int sum = 0;
for(int j = 0; (1 << j) <= n; ++j){
int x = ((k & (1 << j)) ? 1 : 0);
sum += buc[j][x ^ 1] * (1 << j);
}
ans += sum;
for(int i = k + 1; i <= n; i += (k + 1)){
i = nxt[i];
if(i > n) break;
ans -= 2 * (i ^ k);
}
}
cout << ans;
return 0;
}
R16 - B「绀碧绝音」
题意
凯撒准备了 \(n\) 块小蛋糕,第 \(i\) 块小蛋糕的美味程度为 \(a_i\)。她们俩决定使用以下方法来确定(抢夺)这些小蛋糕的归属:
- 一开始,凯撒和剑旗爵各有 \(m_1,m_2\) 颗石子(之前玩游戏剩下的)
- 接着,剑旗爵会将她的所有 \(m_2\) 颗石子分配给各个小蛋糕,第 \(i\) 个小蛋糕分配到的石子数为 \(p_i\)。
- 在剑旗爵分配完成之后,凯撒会将她的所有 \(m_1\) 颗石子分配给各个小蛋糕,第 \(i\) 个小蛋糕分配到的石子数为 \(q_i\),此时若 \(p_i\le q_i\),则第 \(i\) 块小蛋糕会归凯撒所有,否则会归剑旗爵所有。请注意,凯撒在分配石子时已知晓剑旗爵的分配方案。
现在凯撒和剑旗爵都想最大化各自拿到的所有小蛋糕的美味程度之和,请问,如果她们均按最优方案操作,则最终凯撒得到的小蛋糕的美味程度之和为多少。
Solution
一个 trick : 组合数(插板法)+ 单调性 = 分拆数。
我们枚举剑旗爵将石子分配给蛋糕的所有方案 \(p\)。当 \(p\) 确定的情况下,此时凯撒分石子的最优方案是 \(q\),显然可以用一个背包求出。如果写得是多重背包,那么复杂度是 \(O({n + m_2 - 1 \choose m_2 - 1}n{m_1}^2)\)。
发现一个性质,一定存在一个能达到最优的方案是剑旗爵总是会将石子分给那些权重更大蛋糕,即石子随着蛋糕权重减小而递减。证明的话就是,在此基础上,交换相邻两个答案的蛋糕的话,答案不会更优。那么我们就可以排序之后枚举分拆数,而非组合数了。同时可以发现,凯撒实际上只会选择拿不拿这个蛋糕的权重,这样就变成了 \(0-1\) 背包,总的复杂度是 \(O(F(m_2)nm_1)\)。
分拆数在 20 时只有 607,30 时只有 5604,40 时只有 37338,50时也只有二十万左右,因此有些时候是一个可以考虑的非多项式复杂度。
Code
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 35;
int f[N], p[N], a[N], n, m1, m2, ans = 1e18;
void chmax(int &x, int y){ x = max(x, y); }
void chmin(int &x, int y){ x = min(x, y); }
int cal(int m){
memset(f, 0, sizeof(f));
for(int i = 1; i <= n; ++i){
for(int j = m; j >= p[i]; --j){
chmax(f[j], f[j - p[i]] + a[i]);
}
}
return f[m];
}
void dfs(int x, int rem, int lst){
if(rem == 0){
chmin(ans, cal(m1));
return;
}
if(x > n) return;
for(int i = lst; i <= rem; ++i){
if((n - x + 1) * i > rem) break;
p[x] = i;
dfs(x + 1, rem - i, i);
}
p[x] = 0;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
int T; cin >> T;
while(T--){
cin >> n >> m1 >> m2;
for(int i = 1; i <= n; ++i) cin >> a[i];
ans = 1e18;
sort(a + 1, a + 1 + n);
for(int i = 1; i <= n; ++i) dfs(i, m2, 1);
cout << ans << '\n';
}
return 0;
}
R16 - C「记忆的回响」
题意
众所周知,阿格莱雅用金线编织了一张遍布圣城的网,用于洞察城中的大小事件。
为方便起见,这张网的结构可以用一棵包含 \(n\) 个节点的无根树来描述,其中每条边上有一只若虫用于控制金线震动的强度。形式化地描述,这棵树包含 \(n-1\) 条边,第 \(i\) 条边连接节点 \(u_i\) 与 \(v_i\),且当前的震动强度为 \(a_i\)。
由于黑潮迫近,阿格莱雅希望重新调整每条金线的震动强度,来为圣城更好地布防。具体来说,她希望调整之后第 \(i\) 条边的震动强度为 \(b_i\)。
为了重新调整,阿格莱雅可以进行如下操作:
- 第一步,选择两个节点 \(p\) 与 \(q\) 和一个正整数 \(w\),要求 \(p\) 与 \(q\) 不为某条边的两个端点。
- 第二步,阿格莱雅从节点 \(p\) 沿最短路径走至节点 \(q\),对经过的第奇数条边,将其震动强度加上 \(w\),对经过的第偶数条边,将其震动强度减去 \(w\)。
由于阿格莱雅统领圣城事务繁忙,因此她希望至多进行 \(D\) 次操作,且行走距离大于 \(2\) 的操作次数不超过 \(K\)(行走距离定义为最短路径上边的个数,\(D,K\) 均为给定的常数)。
现在金织女士将这个问题交给了你,请你帮助她规划一个可行的操作方案,由于调整金线是一件非常严肃的事,因此阿格莱雅保证至少存在一组合法的操作方式。
Solution
赛时连菊花图都没有想出来,实际上是漏掉了一个关键对于守恒量的思考。由于我们是在构造增量 \(c_i = a_i - b_i\) 的操作方案,使得最后为 \(c_i\) 全部为 0。对于菊花图来说,我们的操作(一加一减)不会让总量改变,因此 \(\sum c_i = 0\),这样只用让第一条边和别的边进行操作,把别的边都调成 0,第一条边也自然变成了 0。
\(D = n, K = 1\) 的构造方案。因为一加一减是一个非常难处理的东西,而这个形式有非常像裂项开来,所以我们可以把它求和回去看看。具体来说,让 \(d_i\) 为所有以 \(i\) 为端点的边的 \(c_i\) 之和。当改变链的长度是偶数时,中间一加一减全消了,只剩下头和尾一加一减 \(w\),对于奇数的话就是头和尾都加 \(w\)。我们发现,\(d_i\) 与 \(c_i\) 有双射关系(从叶子往上递推就行),并且 \(d_i\) 全 0 时,\(c_i\) 也是全 0。所以我们只要构造方案使得 \(d_i\) 全 0。
如何做呢?由于我们限制更宽的链长为 2 实际上是跨层进行操作,所以我们按层数的奇偶性将所有点分成两组。手玩一下,发现同组之间的调整过程类似菊花图的那种,只有组内 \(\sum d_i = 0\) 时才有解。那么我们那次长度超过三的操作(实际上将两个 \(d_i\) 同时加上 \(w\))就得用来把两组的组内和同时调整到 0,由于保证存在有解,肯定可以调整到。
那么组内调整次数 \(n - 1\) 加上组间调整 1 次,刚好是 \(n\)。
Code
#include <bits/stdc++.h>
using namespace std;
typedef pair<int, int> pii;
const int N = 1e4 + 5;
vector<pii> e[N];
int d[N], n, sum[2], fa[N], dep[N], ver, cnt;
struct node{
int u, v, w;
}ans[N];
void print(int u, int v){
if(!d[u]) return;
++cnt;
if(d[u] < 0) ans[cnt] = {u, v, -d[u]};
else ans[cnt] = {v, u, d[u]};
d[v] += d[u]; d[u] = 0;
}
void init(int u, int f){
fa[u] = f;
dep[u] = dep[f] + 1;
for(auto [v, w] : e[u]){
d[u] += w;
if(v != f) init(v, u);
}
sum[dep[u] & 1] += d[u];
if((dep[u] & 1) == 0 && dep[u] >= 4) ver = u;
}
vector<int> tmp;
void getans(int u, int op){
for(auto [v, w] : e[u]){
if(v != fa[u]){
getans(v, op);
}
}
if(u == 1) return;
if((dep[u] & 1) == op){
int v;
if((v = fa[fa[u]])) print(u, v);
else tmp.emplace_back(u);
}
}
void solve(){
memset(e, 0, sizeof(e));
memset(sum, 0, sizeof(sum));
int k;
tmp.clear();
cin >> n >> k >> k;
for(int i = 1; i < n; ++i){
int u, v, a, b;
cin >> u >> v >> a >> b;
int w = a - b;
e[u].emplace_back(v, w);
e[v].emplace_back(u, w);
}
cnt = 0;
init(1, 0);
int x = sum[0];
// cout << x << '\n';
if(x < 0) ans[++cnt] = {1, ver, -x};
d[1] -= x, d[ver] -= x;
getans(1, 0), getans(1, 1);
for(int i = 1; i < tmp.size(); ++i) print(tmp[i], tmp.front());
cout << cnt << '\n';
for(int i = 1; i <= cnt; ++i)
cout << ans[i].u << ' ' << ans[i].v << ' ' << ans[i].w << '\n';
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
int T; cin >> T;
while(T--) solve();
return 0;
}
R16 - D「永劫燔阳」
题意
白厄在和迷路迷境里的妖精玩一个特殊的棋类游戏!他们的本意是玩国际象棋,但由于迷路迷境的时空非常混乱,因此棋盘变成了 \(n\) 维,每一维的大小分别为 \(a_1,a_2,\cdots, a_n\),棋盘的对角坐标为 \((1,1,\cdots,1)\) 与 \((a_1,a_2,\cdots, a_n)\)。
棋盘中只有一个棋子,双方会不断改变棋子的位置,而由于特殊的时空性质,这个棋子的类型也会时刻改变!具体来说,可能变成的棋子种类一共有以下五种,他们的移动规则与一般的国际象棋相仿:
- 车:选择一个维度与一个正整数 \(x\),使这个维度的坐标加上 \(x\) 或减去 \(x\)。
- 王:选择维度的非空子集,使这个子集中的所有维度的坐标各自加上 \(1\) 或减去 \(1\)。
- 后:选择维度的非空子集与一个正整数 \(x\),使这个子集中的所有维度的坐标各自加上 \(x\) 或减去 \(x\)。
- 马:选择两个不同的维度,使一个维度的坐标加上 \(1\) 或减去 \(1\),另一个维度的坐标加上 \(2\) 或减去 \(2\)。
- 象:选择两个不同的维度与一个正整数 \(x\),使两个维度的坐标加上 \(x\) 或减去 \(x\)。
现在白厄和妖精们一共改变了 \(q\) 次棋子的位置,并给出了棋子变成的类型,请你回答在改变位置之后,有多少个棋盘上的位置可以通过棋子移动恰好一步到达。由于答案可能很大,你只需要输出答案对 \(998244353\) 取模之后的结果即可。
Solution
又看错题了,以为选的那个集合内只能同事加 x 或者减 x,然后就去维护了一个 \(2 ^ x\) 的东西,写完了,发现样例一个过不了,再次告诉我们手模样例的重要性。
实际上是一个计数题,把式子拆开之后用线段树维护,比较无聊。马和象的移动时要选择不同的维度的限制可以整体减部分满足,可以改天自己再推一遍。
最后要维护 \(\sum 3^p2^q\),\(\sum pq\),\(\sum p^2\),\(\sum q ^ 2\),\(\sum p\),\(\sum q\) 和区间加 \(p\) ,区间加 \(q\) 操作,都比较 trival。
Code
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 1e6 + 5, mod = 998244353, i2 = 499122177, i3 = 332748118, V = 1e6;
struct node{
int len, s[2], s2[2], pq, tag[2] = {0, 0}, mi;
}tr[N << 2];
void add(int &x, int y){ x = ((x + y) % mod + mod) % mod; }
void mul(int &x, int y){ (x *= y) %= mod; }
int plu(int x, int y){ return ((x + y) % mod + mod) % mod; }
int times(int x, int y){ return (x * y) % mod; }
int pw2[N], ipw2[N], pw3[N], ipw3[N];
#define ls(p) (p << 1)
#define rs(p) (p << 1 | 1)
void addtag(node &x, int k, int op){
node y = x;
if(op == 0) mul(x.mi, ((k >= 0) ? pw3[k] : ipw3[-k]));
else mul(x.mi, ((k >= 0) ? pw2[k] : ipw2[-k]));
add(x.s[op], y.len * k);
x.s2[op] = (y.s2[op] + 2 * k * y.s[op] + y.len * k * k) % mod;
add(x.pq, k * y.s[op ^ 1]);
x.tag[op] += k;
}
void pushdown(int p){
for(int op : {0, 1}){
int &x = tr[p].tag[op];
if(x != 0){
addtag(tr[ls(p)], x, op);
addtag(tr[rs(p)], x, op);
x = 0;
}
}
}
void pushup(int p){
node &x = tr[p], y = tr[ls(p)], z = tr[rs(p)];
x = {y.len + z.len, {(y.s[0] + z.s[0]) % mod, (y.s[1] + z.s[1]) % mod},
{(y.s2[0] + z.s2[0]) % mod, (y.s2[1] + z.s2[1]) % mod},
(y.pq + z.pq) % mod,
{x.tag[0], x.tag[1]},
(y.mi + z.mi) % mod};
}
void build(int p = 1, int pl = 1, int pr = V){
if(pl == pr) return tr[p] = {1, {0, 0}, {0, 0}, 0, {0, 0}, 1}, void();
int mid = (pl + pr) >> 1;
build(ls(p), pl, mid);
build(rs(p), mid + 1, pr);
pushup(p);
}
void update(int L, int R, int op, int x, int p = 1, int pl = 1, int pr = V){
if(L <= pl && R >= pr) return addtag(tr[p], x, op), void();
pushdown(p);
int mid = (pl + pr) >> 1;
if(L <= mid) update(L, R, op, x, ls(p), pl, mid);
if(R > mid) update(L, R, op, x, rs(p), mid + 1, pr);
pushup(p);
}
int query(int x, int p = 1, int pl = 1, int pr = V){
if(pl == pr) return p;
pushdown(p);
int mid = (pl + pr) >> 1;
if(x <= mid) return query(x, ls(p), pl, mid);
else return query(x, rs(p), mid + 1, pr);
}
int a[N], b[N], n, q, w[N], totw;
void upd(int p, int x){
auto work = [](int p, int k){
int l = min(b[p] - 1, a[p] - b[p]), r = max(b[p] - 1, a[p] - b[p]);
if(1 <= l) update(1, l, 0, k);
if(l < r) update(l + 1, r, 1, k);
};
if(b[p]) work(p, -1), add(totw, -w[p]);
b[p] = x;
w[p] = ((x > 1 ? 1 : 0) + (x <= a[p] - 1 ? 1 : 0)) * ((x > 2 ? 1 : 0) + (x <= a[p] - 2 ? 1 : 0));
add(totw, w[p]);
work(p, 1);
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
int sum = 0;
cin >> n >> q;
pw2[0] = ipw2[0] = pw3[0] = ipw3[0] = 1;
for(int i = 1; i <= V; ++i){
pw2[i] = times(pw2[i - 1], 2);
pw3[i] = times(pw3[i - 1], 3);
ipw2[i] = times(ipw2[i - 1], i2);
ipw3[i] = times(ipw3[i - 1], i3);
}
for(int i = 1; i <= n; ++i)
cin >> a[i], add(sum, a[i] - 1);
build();
for(int i = 1; i <= n; ++i){
int x;
cin >> x;
upd(i, x);
}
while(q--){
int k; cin >> k;
for(int i = 1; i <= k; ++i){
int c, d; cin >> c >> d;
upd(c, d);
}
int id; cin >> id;
if(id == 1) cout << sum << '\n';
if(id == 2){
int p = query(1);
cout << plu(tr[p].mi, -1) << '\n';
}
if(id == 3) cout << plu(tr[1].mi, -V) << '\n';
if(id == 4){
int p1 = query(1), p2 = query(2);
cout << plu(times(plu(2 * tr[p1].s[0], tr[p1].s[1]),
plu(2 * tr[p2].s[0], tr[p2].s[1])), -totw) << '\n';
}
if(id == 5){
cout << plu(plu(plu(2 * tr[1].s2[0], -2 * tr[1].s[0]),
plu(times(i2, tr[1].s2[1]), -times(i2, tr[1].s[1]))), times(tr[1].pq, 2)) << '\n';
}
}
return 0;
}
Summary
T1、T2 都差一点摸到正解了啊。前面差不多写完了之后,可以把后面的能写的写了回去想,说不定有不同的想法。
还是不够快,所有部分分都得在 2.5h 写完才有更多时间来想正解。