【题解】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:
我们考虑如何确定一个等差数列,等价于:
- 任意
i!=j,a[i]!=a[j] - 设等差序列最大值为 Max,最小值为 Min,则满足
(Max-Min)/k==r-l 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 个位置是 1 ,i-1 次后的方案数。f 数组为辅助数组,。
我们转移的目标 : 先消除某一个格子。
dp[i-2] * (i-2) * C(i,2),即i与j匹配。

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

f[] 数组转移:
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);
}

浙公网安备 33010602011771号