【题解】NOIP2021模拟赛

A.

状压 dp 。考虑到合并和分裂是互逆操作,所以只需要考虑合并操作。

trick:b[i] 设置为 -b[i] ,题目转化为对一个序列,找到尽量多的和为 0 的集合。暴力转移是 O(3^(n+m)) ,我们考虑优化转移。

注意到 sum=0 时贡献加一,可以设计出如下转移方程 :

dp[i]=max(dp[j]+dp[i-j])+(sum==0),j \in i

时间复杂度 O((n+m)2^(n+m))。 实际优化并不大。

#include<bits/stdc++.h> #define INF 0x3f3f3f3f using namespace std; inline int read() { int X=0; bool flag=1; char ch=getchar(); while(ch<'0'||ch>'9') {if(ch=='-') flag=0; ch=getchar();} while(ch>='0'&&ch<='9') {X=(X<<1)+(X<<3)+ch-'0'; ch=getchar();} if(flag) return X; return ~(X-1); } const int mx=25; int T,n,m,a[mx],dp[1<<20]; int main() { freopen("operator.in","r",stdin); freopen("operator.out","w",stdout); scanf("%d",&T); while(T--) { memset(dp,0,sizeof(dp)); scanf("%d",&n);for(int i=0;i<n;i++) scanf("%d",&a[i]); scanf("%d",&m);for(int i=0;i<m;i++) scanf("%d",&a[n+i]),a[n+i]=-a[n+i]; for(int i=1;i<(1<<n+m);i++) { int tot=0; for(int j=0;j<n+m;j++) { if((i>>j)&1) { tot+=a[j]; } dp[i]=max(dp[i],1); for(int j=(i-1)&i;j;j=(j-1)&i) { dp[i]=max(dp[i],dp[i-j]+dp[j]); } } printf("%d\n",n+m-dp[(1<<n+m)-1]*2); } }

B.

题意:给定一个序列,有 m 次操作,每次选择数对 (a[i],a[j]),i!=j 且不重复,价值为 a[i]^a[j] ,求最大价值总和。 n<=5e4,m<=n^2

Solution:

比较棘手的一道题。

不难想到 Trie树 ,但是我 Trie 没学好。

那么这道题很像最大异或路径。想想我们是怎么做的:枚举 a[i] ,然后在 Trie 树上查找路径,时间复杂度 O(nlogn) ,前提是每次只跳一边。

方便起见,我们令 m*2 ,答案除以 2 即可。二分第 m*2 大值 mid ,计算出异或值 >=mid 的数对个数,这部分时间复杂度 O(nlog^2n)

最后来计算答案。对于 >=m*2 的部分,每一个减去 mid 即可。这里维护以 i 为子树,第 j 位为 1 的数的个数,时间复杂度 O(nlog^2n)

综上,我们用 Trie 树在 O(nlog^2n) 时间内过了此题。

#include <bits/stdc++.h> #define ll long long using namespace std; const int mx = 5e6 + 5; const int mod = 1e9 + 7; inline ll read() { ll X = 0; bool flag = 1; char ch = getchar(); while (ch < '0' || ch > '9') { if (ch == '-') flag = 0; ch = getchar(); } while (ch >= '0' && ch <= '9') { X = (X << 1) + (X << 3) + ch - '0'; ch = getchar(); } if (flag) return X; return ~(X - 1); } int n, tot = 1, a[mx], trie[mx][2]; ll m, siz[mx][30], siz2[mx]; ll fpow(ll x, ll y) { ll tot = 1; for (; y; y >>= 1) { if (y & 1) tot = tot * x % mod; x = x * x % mod; } return tot; } void insert(int x) { int it = 1; for (int i = 29; i >= 0; i--) { int k = (x >> i) & 1; if (!trie[it][k]) trie[it][k] = ++tot; it = trie[it][k]; siz2[it]++; for (int j = 29; j >= 0; j--) { if ((x >> j) & 1) { siz[it][j]++; } } } } ll dfs1(ll x, ll y) { int it = 1; ll res = 0; for (int i = 29; i >= 0; i--) { //假如这一位答案为 1,则必须走相反边 if (!it) break; // 走没了 int k = (x >> i) & 1; if ((y >> i) & 1) { it = trie[it][k ^ 1]; } //否则计算相反边的答案 else { res += siz2[trie[it][k ^ 1]]; it = trie[it][k]; } } res += siz2[it]; //记录剩下的贡献 return res; } ll calc(int p, ll x) { ll res = 0; for (int j = 29; j >= 0; j--) { //如果 x 的这一位为 1,则加上为 0 的个数 //将一个数拆分成每个数位的贡献 if ((x >> j) & 1) { res = (res + (siz2[p] - siz[p][j]) * ((1 << j) % mod) % mod) % mod; } else { res = (res + siz[p][j] * ((1 << j) % mod) % mod) % mod; } } return res; } ll dfs2(ll x, ll y) { int it = 1; ll res = 0; for (int i = 29; i >= 0; i--) { if (!it) break; int k = (x >> i) & 1; if ((y >> i) & 1) { it = trie[it][k ^ 1]; } else { int pos = trie[it][k ^ 1]; res = (res + calc(pos, x)) % mod; it = trie[it][k]; } } res = (res + calc(it, x)) % mod; return res; } int check(ll mid) { ll res = 0; for (int i = 1; i <= n; i++) { res += dfs1(a[i], mid); } return res; } ll check2(ll mid) { ll res = 0, cnt = 0; for (int i = 1; i <= n; i++) { //两个类似的函数 cnt += dfs1(a[i], mid); res = (res + dfs2(a[i], mid)) % mod; } if (cnt > m) res = (res - (cnt - m) % mod * mid % mod) % mod; return (res + mod) % mod; } int main() { freopen("xor.in", "r", stdin); freopen("xor.out", "w", stdout); n = read(), m = read(); m <<= 1; for (int i = 1; i <= n; i++) { a[i] = read(); insert(a[i]); } ll L = 0, R = (1<<30) - 1, res; while (L <= R) { ll mid = (L + R) >> 1; if (check(mid) >= m) L = mid + 1, res = mid; else R = mid - 1; } printf("%lld", check2(res) * fpow(2, mod - 2) % mod); }

C.

题意:给定一个序列,每次修改操作改变一个数的值,查询操作
询问 [l,r] 能否组成等差序列。 n,m<=3e5

solution:

我们考虑如何确定一个等差数列,等价于:

  1. 任意 i!=j ,a[i]!=a[j]
  2. 设等差序列最大值为 Max,最小值为 Min,则满足 (Max-Min)/k==r-l
  3. gcd(a[2]-a[1],a[3]-a[2],..,a[n]-a[n-1])==d

于是乎,我们可以用两个线段树维护区间最大最小值,和区间 gcd

对于判重的情况,我们 将值相同的数放入同一 set ,每次 lower_bound 查询最近的值相同的数 <l 即可。

#include <bits/stdc++.h> #define PII pair<int, int> #define int long long using namespace std; inline int read() { int X = 0; bool flag = 1; char ch = getchar(); while (ch < '0' || ch > '9') { if (ch == '-') flag = 0; ch = getchar(); } while (ch >= '0' && ch <= '9') { X = (X << 1) + (X << 3) + ch - '0'; ch = getchar(); } if (flag) return X; return ~(X - 1); } // Task : 1. 补题 2. 写状压题解 3. 总结 dsu on tree 4. 补写 POI 试题 const int mx = 3e5 + 5; int n, m, xo, num, a[mx], t2[mx << 2]; set<int> s[mx * 2]; // 类似于离散化 set<int>::iterator it; map<int, int> ID; // This code RE ! //算法:线段树 //维护:区间 gcd ,最小值,最大值,最近前缀 int gcd(int x, int y) { return (y == 0) ? x : gcd(y, x % y); } struct SegmentTree { int val, Min, Max; } t[mx << 2]; //找到 (x,y) 的前缀 int id(int x) { if (!ID[x]) ID[x] = ++num; return ID[x]; } int getpre(int x, int y) { it = s[id(y)].lower_bound(x); if (it == s[id(y)].begin()) return 0; return *--it; } void up(int p) { t[p].val = max(t[p << 1].val, t[p << 1 | 1].val); t[p].Min = min(t[p << 1].Min, t[p << 1 | 1].Min); t[p].Max = max(t[p << 1].Max, t[p << 1 | 1].Max); } void update(int p, int l, int r, int x, int y) { if (l == r) { t[p].Min = t[p].Max = y; t[p].val = getpre(x, y); return; } int mid = (l + r) >> 1; if (x <= mid) update(p << 1, l, mid, x, y); else update(p << 1 | 1, mid + 1, r, x, y); up(p); } //将第 x 位 gcd 修改为 y void update2(int p, int l, int r, int x, int y) { if (l == r) { t2[p] = y; return; } int mid = (l + r) >> 1; if (x <= mid) update2(p << 1, l, mid, x, y); else update2(p << 1 | 1, mid + 1, r, x, y); t2[p] = gcd(t2[p << 1], t2[p << 1 | 1]); } SegmentTree query(int p, int l, int r, int ql, int qr) { if (ql <= l && r <= qr) return t[p]; int mid = (l + r) >> 1; if (qr <= mid) return query(p << 1, l, mid, ql, qr); if (mid < ql) return query(p << 1 | 1, mid + 1, r, ql, qr); SegmentTree t1 = query(p << 1, l, mid, ql, qr), t2 = query(p << 1 | 1, mid + 1, r, ql, qr), t3; t3.Max = max(t1.Max, t2.Max); t3.Min = min(t1.Min, t2.Min); t3.val = max(t1.val, t2.val); return t3; } int query2(int p, int l, int r, int ql, int qr) { if (ql <= l && r <= qr) return t2[p]; int mid = (l + r) >> 1; if (qr <= mid) return query2(p << 1, l, mid, ql, qr); if (mid < ql) return query2(p << 1 | 1, mid + 1, r, ql, qr); int t1 = query2(p << 1, l, mid, ql, qr), t2 = query2(p << 1 | 1, mid + 1, r, ql, qr); return gcd(t1, t2); } signed main() { freopen("grade.in", "r", stdin); freopen("grade.out", "w", stdout); n = read(), m = read(); for (int i = 1; i <= n; i++) a[i] = read(), s[id(a[i])].insert(i), update(1, 1, n, i, a[i]), update2(1, 1, n, i, abs(a[i] - a[i - 1])); for (int i = 1; i <= m; i++) { int op = read(); if (op == 1) { int x = read(), y = read(); x ^= xo, y ^= xo; if (a[x] == y) continue; //先删除之前的 s[id(a[x])].erase(x); //再加入修改的 a[x] = y; s[id(y)].insert(x); update(1, 1, n, x, y); //最后再来修改两个相邻位置的 gcd update2(1, 1, n, x, abs(a[x] - a[x - 1])); if (x + 1 <= n) update2(1, 1, n, x + 1, abs(a[x + 1] - a[x])); } else { int l = read(), r = read(), k = read(); l ^= xo, r ^= xo; if(l > r) swap(l, r); if (l == r) { printf("Yes\n"); xo++; continue; } if (k == 0) { SegmentTree tmp = query(1, 1, n, l, r); if (tmp.Min == tmp.Max) { printf("Yes\n"); xo++; } else { printf("No\n"); } continue; } if (query2(1, 1, n, l + 1, r) != k) { printf("No\n"); continue; } SegmentTree tmp = query(1, 1, n, l, r); if (tmp.val < l && (tmp.Max - tmp.Min) / k == r - l) { printf("Yes\n"); xo++; } else { printf("No\n"); } } } }

D.

题意:有一个 n 序列,初始全为空,每次操作选取两个不同的数 +1 ,求 n 次操作后序列全为 2 的方案数。 n<=1e7

Solution:

该解法由巨佬 dj 提供。

经典递推。多项式数列。

dp[i] 表示 i 个数 i 次操作后全为 2 的方案数,f[i] 表示当前状态已有 2 个位置是 1i-1 次后的方案数。f 数组为辅助数组,。

我们转移的目标 : 先消除某一个格子。

  1. dp[i-2] * (i-2) * C(i,2) ,即 ij 匹配。
    在这里插入图片描述
  2. f[i-1] * C(i-1,2) * A(i,2) ,即 i 分别与· j,k 匹配。

在这里插入图片描述

f[] 数组转移:

  1. dp[i-3] * (i-2) * A(i-1,2) ,即· j,k 分别与 i 匹配。

在这里插入图片描述
2. dp[i-2] * (i-1) ,即· j,k 互消。

在这里插入图片描述
3. f[i-2] * A(i-1,2) * A(i-2,2) ,注意配对有顺序。在这里插入图片描述

综上,时间复杂度 O(n)

总结:这种题的关键是抽象出模型,没有清晰的状态定义时最好不要侥幸推转移方程。必要时待定系数猜式子也是一法。(详见巨佬 ljs)。

#include <bits/stdc++.h> #define ll long long #define mod 998244353 using namespace std; const int mx = 1e7 + 5; inline int read() { int X = 0; bool flag = 1; char ch = getchar(); while (ch < '0' || ch > '9') { if (ch == '-') flag = 0; ch = getchar(); } while (ch >= '0' && ch <= '9') { X = (X << 1) + (X << 3) + ch - '0'; ch = getchar(); } if (flag) return X; return ~(X - 1); } int n; ll dp[mx], f[mx], res; //转移的目标 : 先消除某一个格子 // dp[i] = dp[i-2] * (i-1) * C(i,2) + f[i-1] * C(i-1,2) * A(i,2) // f[i] = dp[i-2] * (i-1) + dp[i-3] * A(i-1,2) * (i-2) + f[i-2] * A(i-1,2) * A(i-2,2) ll A(ll x) { return x * (x - 1) % mod; } ll C(ll x) { return x * (x - 1) / 2 % mod; } int main() { freopen("matrix.in", "r", stdin); freopen("matrix.out", "w", stdout); scanf("%d", &n); dp[0] = 1, dp[2] = 1; f[2] = 1; for (int i = 3; i <= n; i++) { dp[i] = (dp[i - 2] * (i - 1) % mod * C(i) % mod + f[i - 1] * C(i - 1) % mod * A(i) % mod) % mod; f[i] = (dp[i - 2] * (i - 1) % mod + dp[i - 3] * A(i - 1) % mod * (i - 2) % mod + f[i - 2] * A(i - 1) % mod * A(i - 2) % mod) % mod; } for (int i = 1; i <= n; i++) { res = (res + dp[i]) % mod; } printf("%lld", res); }

__EOF__

本文作者仰望星空的蚂蚁
本文链接https://www.cnblogs.com/cqbzly/p/17530327.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   仰望星空的蚂蚁  阅读(18)  评论(0编辑  收藏  举报  
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· DeepSeek 开源周回顾「GitHub 热点速览」
点击右上角即可分享
微信分享提示