2021 NOIP 题解
21年的题有点难啊(除了T1),竟然没绿题,直接紫题+黑题。
T1 P7960 [NOIP2021] 报数
这道题还是挺水的。
因为是多组询问,首先预处理出答案,然后 O ( 1 ) O(1) O(1)查询。
在 O ( l o g n ) O(log n) O(logn)时间内判断一个数是否含有7,其中 n n n是数值的范围。
用筛法对每个含有 7 7 7的数的倍数给标记,因为含有 7 7 7的数密度很小,这一步近似看成是 O ( n ) O(n) O(n)
然后倒着循环一遍,处理一下每个数的下一个被标记的数是哪个,预处理的总时间复杂度为 O ( n l o g n ) O(n log n) O(nlogn)
c o d e code code
#include <bits/stdc++.h>
using namespace std;
int read() {
int x = 0, f = 1;
char ch = getchar();
while (ch < '0' || ch > '9') {
if (ch == '-') f = -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9')
x = x * 10 + ch - '0', ch = getchar();
return x * f;
}
const int N = 1e7 + 5;
bool flag[N];
int ans[N];
//根据题意判断是否有7
bool check(int x) {
while (x) {
if (x % 10 == 7) {
return true;
}
x /= 10;
}
return false;
}
int main() {
for (int i = 1; i <= (N - 4); i++) {
//判断是否是7的倍数
if (check(i)) {
for (int j = i; j <= (N - 4); j += i) {
flag[j] = 1;
}
}
}
int x = 0;
for (int i = (N - 4); i >= 1; i--) {
if (flag[i])
ans[i] = -1;
else {
ans[i] = x;
x = i;
}
}
int T;
T=read();
while (T--) {
int x;
x=read();
printf("%d\n", ans[x]);
}
return 0;
}
T2 P7961 [NOIP2021] 数列
这道题就开始没啥思路了,整的人挺无语的。
后来看了题解,感觉这道题还是可以理解的,当时比赛打暴力获得 45 p t s 45pts 45pts,还可以,比爆零好点。
先来说下暴力思路:
先不考虑序列里具体每个位置填哪些数,而是考虑每种数在序列中出现了多少次:
设 x i x_i xi为序列中值为 i i i的数的个数,即要满足 x 0 + x 1 + x 2 + . . . + x m = n x_0+x_1+x_2+...+x_m=n x0+x1+x2+...+xm=n。
这个不定方程的一个1非负整数解,即为一种不考虑具体位置的方案,如果要求真正1的方案数,只需乘以对应的组合数即可。
具体来说,就是第一步从 n n n个位置里选 x 0 x_0 x0个,第二步是从 n − x 0 n-x_0 n−x0个位置里选 x 1 x_1 x1个,以此类推,方案数就会乘以 C n x 0 C n − x 0 x 1 C n − x 0 − x 1 x 1 ⋯ C x m x m C_{n}^{x_{0}} C_{n-x_{0}}^{x_{1}} C_{n-x_{0}-x_{1}}^{x_{1}} \cdots C_{x_{m}}^{x_{m}} Cnx0Cn−x0x1Cn−x0−x1x1⋯Cxmxm
而这道题每种方案的贡献是 v a 1 ∗ v a 2 ∗ . . . ∗ v a n v_{a1}*v_{a2}*...*v_{an} va1∗va2∗...∗van,也就是还要再乘以 ∏ i = 0 m v i x i \prod_{i=0}^{m} v_{i}^{x_{i}} \text { } ∏i=0mvixi
这样可以首先预处理组合数以及每个
v
i
v_i
vi的幂次,然后写一个爆搜把每个不定方程的解求出来1,最后检查
1
1
1的个数是否合法。
45
p
t
s
45pts
45pts
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
int read() {
int x = 0, f = 1;
char ch = getchar();
while (ch < '0' || ch > '9') {
if (ch == '-') f = -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9')
x = x * 10 + ch - '0', ch = getchar();
return x * f;
}
const LL mod = 998244353;
const int N=40;
LL ans;
LL c[N][N];
LL a[105][40];
int n, m, K;
void dfs(int u, int x, LL now, LL s) {
if (u == m + 1) {
//__builtin_popcountll()函数非常好用,返回二进制中1的个数
if (x == 0 && __builtin_popcountll(s) <= K){
ans = (ans + now) % mod;
}
return;
}
for (int i = 0; i <= x; i++) {
dfs(u + 1, x - i, now * c[x][i] % mod * a[u][i] % mod, s + ((LL)i << u));
}
}
int main() {
for (int i = 0; i <= 30; i++) {
c[i][0] = 1;
for (int j = 1; j <= i; j++){
c[i][j] = (c[i - 1][j] + c[i - 1][j - 1]) % mod;
}
}
n=read(),m=read(),K=read();
for (int i = 0; i <= m; i++) {
int x;
x=read();
a[i][0] = 1;
for (int j = 1; j <= n; j++) {
a[i][j] = a[i][j - 1] * x % mod;
}
}
dfs(0, n, 1, 0);
printf("%lld\n", ans);
return 0;
}
接下来想想正解:
注意道在从小到大枚举 x i x_i xi的时候,可以把 S = 2 a 1 + 2 a 2 + . . . + 2 a n S=2^{a_1}+2^{a_2}+...+2^{a_n} S=2a1+2a2+...+2an分成两部分:
.
.
. 前面
0
∼
1
0\sim 1
0∼1位中一共出现了多少个
1
1
1
.
.
.当前有多少个位进位道
i
+
1
i+1
i+1位
现在我们就能dp了,我们设 f [ u ] [ i ] [ j ] [ k ] f[u][i][j][k] f[u][i][j][k]表示考虑完 ≤ u \le u ≤u的数的个数后,已经用了 i i i个数,进位为 j j j, 0 ∼ u 0 \sim u 0∼u位一共有 k k k个 1 1 1的总贡献。
最后 d p dp dp完将所有的贡献加起来就行了。
c o d e code code
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int INF = 0x3f3f3f3f;
const LL mod = 998244353;
const int N = 40;
LL c[N][N], a[105][N], f[105][N][N][N];
// f[u][i][j][k] 考虑完<=u的数的个数后,已经用了i个数,进位为j,0~u 位一共有k个1
int main() {
int n, m, K;
for (int i = 0; i <= 30; i++) {
c[i][0] = 1;
for (int j = 1; j <= i; j++) c[i][j] = (c[i - 1][j] + c[i - 1][j - 1]) % mod;
}
scanf("%d%d%d", &n, &m, &K);
for (int i = 0; i <= m; i++) {
int x;
scanf("%d", &x);
a[i][0] = 1;
for (int j = 1; j <= n; j++) {
a[i][j] = a[i][j - 1] * x % mod;
}
};
f[0][0][0][0] = 1;
for (int u = 0; u <= m; u++) {
for (int i = 0; i <= n; i++) {
for (int j = 0; j <= i; j++) {
for (int k = 0; k <= i; k++) {
for (int x = 0; x <= n - i; x++) {
f[u + 1][i + x][(j + x) / 2][k + (j + x) % 2] += f[u][i][j][k] * a[u][x] % mod * c[n - i][x] % mod;
f[u + 1][i + x][(j + x) / 2][k + (j + x) % 2] %= mod;
}
}
}
}
}
LL ans = 0;
for (int j = 0; j <= n; j++) {
for (int k = 0; k <= n; k++) {
if (__builtin_popcount(j) + k <= K) ans += f[m + 1][n][j][k];
}
}
printf("%lld\n", ans % mod);
return 0;
}
T3 P7962 [NOIP2021] 方差
这道题有点难,考的偏数学(好像这几道题都跟数学有关)
来看暴力思路:
首先需要知道方差公式为平方的平均数减去平均数的平方。
这题突破口是把 a i + 1 + a i − 1 − a i − > a i a_{i+1}+a_{i-1}-a_i->a_i ai+1+ai−1−ai−>ai这个操作和差分联系起来,不然会觉得操作可以执行无限次,很难有一个最终的目标。
记差分数组 d i = a i − a i − 1 d_i=a_i-a_{i-1} di=ai−ai−1,观察到对 a x a_x ax进行操作后,其差分仅 d x d_x dx和 d x + 1 d_{x+1} dx+1发生了改变,具体为:
d
x
=
(
a
x
+
1
+
a
x
−
1
−
a
x
)
−
a
x
−
1
=
a
x
+
1
−
a
x
=
d
x
+
1
d_x = (a_{x+1} + a_{x-1} -a_x)-a_{x-1}= a_{x+1} - a_x = d_{x+1}
dx=(ax+1+ax−1−ax)−ax−1=ax+1−ax=dx+1
d
x
+
1
=
a
x
+
1
−
(
a
x
+
1
+
a
x
−
1
−
a
x
)
=
a
x
−
a
x
−
1
=
d
x
d_{x+1}=a_{x+1}-(a_{x+1}+a_{x-1}-a_x)=a_x -a_{x-1} = d_x
dx+1=ax+1−(ax+1+ax−1−ax)=ax−ax−1=dx
可以发现一次操作仅仅是改变了相邻的两个差分,原数组的差分 d 1 d_1 d1是固定不变的,除此之外的 d 2 ∼ d n d_2\sim d_n d2∼dn显然都可以随便换位置,也就一共有 ( n − 1 ) ! (n-1)! (n−1)!种可能,那么全排列一下能骗20分。
可以 s h u f f l e shuffle shuffle一些初始情况,然后每次随机交换两个数,检查一下是否方差更小,若不是则不交换,这样直到方差到极值为止。(前置知识)
这种方法可以骗到72分。
72 p t s 72pts 72pts
int a[N], d[N], n;
LL ans = INF;
LL cal() {
for (int i = 1; i <= n; i++) {
a[i] = a[i - 1] + d[i];
}
LL now = 0, s1 = 0, s2 = 0;
for (int i = 1; i <= n; i++) s1 += a[i], s2 += a[i] * a[i];
now = -s1 * s1 + s2 * n;
return now;
}
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d", &a[i]), d[i] = a[i] - a[i - 1];
int wc = 2000;
mt19937 g(114514);
uniform_int_distribution<int> u1(2, n);//生成伪随机数
while (wc--) {
shuffle(d + 2, d + n + 1, g);
int t = 0;
LL now = INF;
while (t < 300) {
int x = u1(g), y = u1(g);
if (x != y) {
swap(d[x], d[y]);
LL tmp = cal();
if (tmp < now) {
t = 0;
now = tmp;
} else {
swap(d[x], d[y]);
t++;
}
}
}
ans = min(ans, now);
}
cout << ans << endl;
return 0;
}
等等再想正解
阿江又讲了种办法,感觉就是 72 p t s 72pts 72pts的简单版,就是去了生成随机数。加了全排列。并且 72 p t s 72pts 72pts的代码更加细节一点。
28 p t s 28pts 28pts
#include<bits/stdc++.h>
const int N=11000;
using namespace std;
int n,a[N],b[N];
long long ans;
inline long long getvar() {
long long sm=0,x=0,x2=0;
for(int i=1; i<=n; ++i) {
x+=sm,x2+=sm*sm;
sm+=b[i];
}
return n*x2-x*x;
}
int main() {
scanf("%d",&n);
for(int i=1; i<=n; ++i)
scanf("%d",&a[i]);
for(int i=1; i<n; ++i)
b[i]=a[i+1]-a[i];
ans=getvar();
while(next_permutation(b+1,b+n))
ans=min(ans,getvar());
printf("%lld",ans);
return 0;
}
下面来想正解:
正解的话需要知道一个性质:方差最小时,d数组一定是先递减后递增。
也就是把差分数组从小到大排序后,考虑存在一个核心不停往左右两边扩展,一个个地安排每个
d
i
d_i
di的位置(放当前的最左边或者最右边),每次有两种选择,这样是可以DP的。
我们用 f [ i ] [ j ] f[i][j] f[i][j]表示考虑完前 i i i小的差分后,当前总和 ∑ a i = j \sum a_i=j ∑ai=j时, ∑ a i 2 \sum a_i^2 ∑ai2 的最小值。
·若把 d i d_i di放到当前核心的右端,记 t = ∑ k = 1 i d k t=\sum _{k=1}^i d_k t=∑k=1idk,新的 a i = t a_i=t ai=t,之前核心里的所有数都不会变化,转移为 f [ i ] [ j ] = f [ i − 1 ] [ j − t ] + t ∗ t f[i][j]=f[i-1][j-t]+t*t f[i][j]=f[i−1][j−t]+t∗t
·若把 d i d_i di放到当前核心的左端,这 i i i个数都加上了 d i d_i di(包括新加进去的 a i a_i ai,初始看成0),这样总和是加了 i ∗ d i i*d_i i∗di,转移为 f [ i ] [ j ] = f [ i − 1 ] [ j − i ∗ d i ] + 2 d i ( j − i ∗ d i ) + i ∗ d i 2 f[i][j]=f[i-1][j-i*d_i]+2d_i(j-i*d_i)+i*d_i^2 f[i][j]=f[i−1][j−i∗di]+2di(j−i∗di)+i∗di2
上述DP总状态数有 n 2 a n^2a n2a个,每个状态都是 O ( 1 ) O(1) O(1)转移,空间需要用滚动数组来优化。
c o d e code code
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const LL INF = 1e18;
const LL mod = 1e9 + 7;
const int N = 10005;
int a[N], d[N];
LL f[2][500005];
int main() {
int n;
scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
int m = n - 1;
for (int i = 1; i <= m; i++) {
d[i] = a[i + 1] - a[i];
}
sort(d + 1, d + m + 1);
int s = 0;
for (int i = 1; i <= n; i++) s += d[i] * i;
int now = 0, u = 0;
for (int j = 0; j <= s; j++) f[u][j] = INF;
f[0][0] = 0;
for (int i = 1; i <= m; i++) {
if (d[i] == 0) continue;
u ^= 1;
now += d[i];
for (int j = 0; j <= s; j++) {
f[u][j] = INF;
int k = j - i * d[i];
if (k >= 0) f[u][j] = min(f[u][j], f[u ^ 1][k] + (LL)i * d[i] * d[i] + (LL)2 * k * d[i]);
k = j - now;
if (k >= 0) f[u][j] = min(f[u][j], f[u ^ 1][k] + now * now);
}
}
LL ans = INF;
for (int j = 0; j <= s; j++) {
if (f[u][j] < INF)
ans = min(ans, n * f[u][j] - (LL)j * j);
}
cout << ans << endl;
return 0;
}
T4 P7963 [NOIP2021] 棋局
这道题还不是很会。。。
总结
这个NOIP2021年的题除了T1简单一点,其他的都挺难的,两道是dp,但暴力还是都有部分分的,还不少,dp真的好考思维,TMD。先做简单题,把能拿得分拿到手,在尽可能地争取高分,先易后难,循序渐进,最后的分数也不会很低。
原文地址:https://blog.csdn.net/xiebohang/article/details/143660784
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!