【题解】NOIP2021

咕咕咕的东西总是要补的。

A.报数

题目描述:

报数游戏是一个广为流传的休闲小游戏。参加游戏的每个人要按一定顺序轮流报数,但如果下一个报的数是 \(7\) 的倍数,或十进制表示中含有数字 \(7\),就必须跳过这个数,否则就输掉了游戏。

在一个风和日丽的下午,刚刚结束 SPC20nn 比赛的小 r 和小 z 闲得无聊玩起了这个报数游戏。但在只有两个人玩的情况下计算起来还是比较容易的,因此他们玩了很久也没分出胜负。此时小 z 灵光一闪,决定把这个游戏加强:任何一个十进制中含有数字 \(7\) 的数,它的所有倍数都不能报出来!

形式化地,设 \(p(x)\) 表示 \(x\) 的十进制表示中是否含有数字 \(7\),若含有则 \(p(x) = 1\),否则 \(p(x) = 0\)。则一个正整数 \(x\) 不能被报出,当且仅当存在正整数 \(y\)\(z\) ,使得 \(x = yz\)\(p(y) = 1\)

例如,如果小 r 报出了 \(6\) ,由于 \(7\) 不能报,所以小 z 下一个需要报 \(8\);如果小 r 报出了 \(33\),则由于 \(34 = 17 \times 2\)\(35 = 7 \times 5\) 都不能报,小 z 下一个需要报出 \(36\) ;如果小 r 报出了 \(69\),由于 \(70 \sim 79\) 的数都含有 \(7\),小 z 下一个需要报出 \(80\) 才行。

现在小 r 的上一个数报出了 \(x\),小 z 想快速算出他下一个数要报多少,不过他很快就发现这个游戏可比原版的游戏难算多了,于是他需要你的帮助。当然,如果小 r 报出的 x 本身是不能报出的,你也要快速反应过来小 r 输了才行。

由于小 r 和小 z 玩了很长时间游戏,你也需要回答小 z 的很多个问题。

对于 \(100\%\) 的数据,\(1 \le T \leq 2 \times {10}^5\)\(1 \le x \leq {10}^7\)

题目分析:

一个数的所有倍数显然想到类似埃氏筛的东西,所以其实我们可以类似埃氏筛只不过每一次是将十进制中含有数字 \(7\) 的筛掉,然后再筛它的倍数即可。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1e7 + 1e5;
int a[MAXN + 5];
bool is[MAXN + 5];
int nxt[MAXN + 5],cnt; 
int maxn = 0;
bool check(int k){
	while(k){
		if(k % 10 == 7){
			return true;
		}
		k/=10;
	}
	return false;
}
void pre(){
	for(int i=1; i<= maxn; i++){
		if(check(i))
			is[i] = true;
		if(is[i]){
			for(int j=1; j * i <= maxn; j++){
				is[i * j] = true;
			}
		}
	}
}
int main(){
//	freopen("number4.in","r",stdin);
//	freopen("out.txt","w",stdout);
	memset(is,false,sizeof(is));
	int t;
	scanf("%d",&t);
	if(t <= 1000){
		maxn = 11000;
	}
	else if(t <= 10000)
		maxn = 201000;
	else
		maxn = 10001000;
	pre();
	int last = 0;
	bool flag = false;
	for(int i=1; i<=maxn; i++){
		if(!is[i]){
			nxt[last] = i;
			last = i;
		}
	}
	while(t--){
		int x;
		scanf("%d",&x);
		if(is[x]){
			printf("%d\n",-1);
		}
		else{
			printf("%d\n",nxt[x]);
		}
	}
	return 0;
} 

B.数列

题目描述:

给定整数 \(n, m, k\),和一个长度为 \(m + 1\) 的正整数数组 \(v_0, v_1, \ldots, v_m\)

对于一个长度为 \(n\),下标从 \(1\) 开始且每个元素均不超过 \(m\) 的非负整数序列 \(\{a_i\}\),我们定义它的权值为 \(v_{a_1} \times v_{a_2} \times \cdots \times v_{a_n}\)

当这样的序列 \(\{a_i\}\) 满足整数 \(S = 2^{a_1} + 2^{a_2} + \cdots + 2^{a_n}\) 的二进制表示中 \(1\) 的个数不超过 \(k\) 时,我们认为 \(\{a_i\}\) 是一个合法序列。

计算所有合法序列 \(\{a_i\}\) 的权值和对 \(998244353\) 取模的结果。

对所有测试点保证 \(1 \leq k \leq n \leq 30\)\(0 \leq m \leq 100\)\(1 \leq v_i < 998244353\)

题目分析:

显然是需要 \(dp\) 解决这个问题的,因为限制里有二进制下 \(1\) 的个数不超过 \(k\) 所以考虑直接把这个东西记到状态里。
可以发现我们相当于将每一个数确定它是二进制下的哪一位的 \(1\),因为我们需要考虑二进制下的进位问题,所以为了方便我们肯定是从低到高考虑每一位。
所以这样的话 \(dp\) 的状态就很显然了,\(dp[i][j][k][p]\) 表示考虑到了二进制下从低到高前 \(i\) 位,现在已经确定了 \(j\)\(a\) 的值,二进制下 \(1\) 的个数为 \(k\),向第 \(i\) 位的进位数量为 \(p\)
转移的话就是枚举第 \(i+1\) 位的 \(a\) 有多少个:

\[dp[i][j][k][p] \times v_i^t \times \binom{n-j}{t} \to dp[i+1][j+t][(p+1)\&1][\lfloor \frac{t+p}{2} \rfloor] \]

统计答案的时候就是要考虑 \(p\) 的进位会产生多少个位数超过 \(m\)\(1\)

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MOD = 998244353;
int f[105][35][35][35];
int pw[105][105];
int v[105];
int fac[105],inv[105];
int mod(int x){
	return x % MOD;
}
int power(int a,int b){
	int res = 1;
	while(b){
		if(b & 1)	res = mod(res * a);
		a = mod(a * a);
		b >>= 1;
	}
	return res;
}
int binom(int n,int m){
	if(n < m || n < 0 || m < 0)	return 0;
	return mod(fac[n] * mod(inv[m] * inv[n-m]));
}
signed main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	int n,m,K;scanf("%lld%lld%lld",&n,&m,&K);
	for(int i=0; i<=m; i++)	scanf("%lld",&v[i]);
	fac[0] = 1;
	for(int i=1; i<=n; i++)	fac[i] = mod(fac[i-1] * i);
	inv[n] = power(fac[n],MOD-2);
	for(int i=n-1; i>=0; i--)	inv[i] = mod(inv[i+1] * (i + 1));
	for(int i=0; i<=m; i++){
		pw[i][0] = 1;
		for(int j=1; j<=n; j++)	pw[i][j] = pw[i][j-1] * v[i] % MOD;
	}
	f[0][0][0][0] = 1;
	for(int i=0; i<=m; i++){
		for(int j=0; j<=n; j++){
			for(int k=0; k<=K; k++){
				for(int p=0; p<=n; p++){
					for(int t=0; t<=n-j; t++){
						f[i+1][j+t][k+((t+p)&1)][(t+p)/2] = (f[i+1][j+t][k+((t+p)&1)][(t+p)/2] + f[i][j][k][p] * binom(n-j,t)%MOD * pw[i][t])%MOD;
					}
				}
			}
		}
	}
	int ans = 0;
	for(int k=0; k<=K; k++){
		for(int p=0; p<=n; p++){
			if(k + __builtin_popcount(p) <= K)	ans = (ans + f[m+1][n][k][p])%MOD;
		}
	}
	printf("%lld\n",ans);
	return 0;
}

C.方差

题目描述:

给定长度为 \(n\) 的非严格递增正整数数列 \(1 \le a_1 \le a_2 \le \cdots \le a_n\)。每次可以进行的操作是:任意选择一个正整数 \(1 < i < n\),将 \(a_i\) 变为 \(a_{i - 1} + a_{i + 1} - a_i\)。求在若干次操作之后,该数列的方差最小值是多少。请输出最小值乘以 \(n^2\) 的结果。

其中方差的定义为:数列中每个数与平均值的差的平方的平均值。更形式化地说,方差的定义为 \(D = \frac{1}{n} \sum_{i = 1}^{n} {(a_i - \bar a)}^2\),其中 \(\bar a = \frac{1}{n} \sum_{i = 1}^{n} a_i\)

测试点编号 \(n \le\) \(a_i \le\)
\(1 \sim 3\) \(4\) \(10\)
\(4 \sim 5\) \(10\) \(40\)
\(6 \sim 8\) \(15\) \(20\)
\(9 \sim 12\) \(20\) \(300\)
\(13 \sim 15\) \(50\) \(70\)
\(16 \sim 18\) \(100\) \(40\)
\(19 \sim 22\) \(400\) \(600\)
\(23 \sim 25\) \({10}^4\) \(50\)

题目分析:

第一步显然就是把 \(n^2\) 乘到贡献里:(不会真的有人是算出最小值再乘 \(n^2\) 吧)

\[\begin{aligned} ans &= n \times \sum_{i=1}^n (a_i - \bar a)^2 \\ &= n \times \sum_{i=1}^n a_i^2 - 2 \times n \times \sum_{i=1}^n (a_i \times \bar a) + n \times \sum_{i=1}^n \bar a^2 \\ &= n \times \sum_{i=1}^n a_i^2 - (\sum_{i=1}^n a_i)^2 \\ \end{aligned} \]

然后就是一次操作可以干什么,如果我们将原序列差分的话会发现这个操作本质上是交换相邻的两个差分数组的数,那么自然想到最小值是不是和差分数组有什么关系。
然后手动模拟一下样例或者搜搜就会发现,在最优情况下差分数组是单谷的。
这个有什么用呢,就是假设我们将差分数组 \(d\) 排序之后,从小到大插入每一个数,那么我们每次都是在序列的首或者尾插入数的。
这个就意味着我们可以直接 \(dp\) 这个过程了,如果我们直接设 \(dp[i]\) 表示插入了 \(d\) 中前 \(i\) 个数的最优答案的话,我们会发现根本没办法转移,因为我们好像啥也不知道。
所以可以尝试是不是可以把某个东西放到状态里呢,就是可以将差分数组还原后形成的原数组的和记下来,也就是设 \(dp[i][j]\) 表示插入了 \(d\) 中前 \(i\) 个数,原数组的和为 \(j\) 时平方和的最小值。
这里需要注意的一点就是:我们无需在意到底是怎么操作得到的,我们已经 \(dp\) 得到了差分数组,所以直接做一次前缀和就可以得到原数组。
转移就是讨论插在首还是尾:
如果插在首:

\[f[i][j] + 2 \times j \times d_{i+1} + (i+1) \times d_{i+1}^2 \to f[i+1][j + (i+1) \times d_{i+1}] \]

如果插在尾:

\[f[i][j] + (\sum_{k=1}^{i+1} d_k)^2 \to f[i+1][j + \sum_{k=1}^{i+1} d_k] \]

虽然看似一切完美但是有一个十分严重的问题就是:\(a_1\) 与前一个数形成的这个差分值我们该怎么处理,因为在前面的分析中我们是直接将这个忽略才得到了这一系列的结论,而加入这个值显然会让我们直接求得的一切都不成立。
因为我们的操作不会对这个值产生任何影响,考虑加上这个值就是会多一个数 \(a_1\) 并且将所有数加 \(a_1\),推推可以发现完全能直接完全忽略这个值,因为把 \(a_1\) 的贡献带入上面的式子之后会直接被消掉。
但是消掉是消掉,我们最后带入算式的 \(n\) 依旧是 \(n\) 而不是 \(n-1\) 这个自己带带就能发现。

这样的话时空复杂度就是 \(O(n \times n \times a)\)
空间复杂度很好说,直接滚掉第一维就好了。
观察数据范围会发现 \(a\) 特别小相比来说 \(n\) 比较大,也就是说 \(d_i = 0\) 的位置很多,而 \(d_i = 0\) 意味着转移不会产生任何影响,所以可以直接忽略掉。
这样的话时间复杂度就变成了 \(O(\min(n,a) \times n \times a)\),这样就可以过了。

代码:

点击查看代码
#include <stdio.h>
#include <algorithm>
#include <string.h>
#include <iostream>
#include <assert.h>
using namespace std;

#define re register
#define sf scanf
#define pf printf
#define nl() putchar('\n')
#define ll long long
#define _for(i, a, b) for(re int (i) = (a); (i) < (b); ++(i))
#define _rfor(i, a, b) for(re int (i) = (a); (i) <= (b); ++(i))
#define inf 0x7ffffffffffffffll
#define maxn 10005
#define maxx 500005

int rdnt(){
   re int x = 0, sign = 1;
   re char c = getchar();
   while (c < '0' || c > '9') { if (c == '-') sign = -1; c = getchar(); }
   while (c >= '0' && c <= '9') x = (x<<3) + (x<<1) + (c ^ 48), c = getchar();
   return x * sign;
}

ll  a[maxn], d[maxn], s[maxn], f[maxx];

inline void ud(re ll &x, re ll y){ if (y < x) x = y; }

int main(){
   #ifndef ONLINE_JUDGE
   freopen("variance.in", "r", stdin);
   freopen("variance.out", "w", stdout);
   #endif

   re int n = rdnt(), rg = 0; re ll mx = 0, ma = a[1] = rdnt();
   _rfor(i, 2, n) d[i-1] = (a[i] = rdnt())-a[i-1], ma = max(ma, a[i]);
   _rfor(x, 1, ma*n) f[x] = inf; f[0] = s[0]= 0;
   sort(d+1, d+n);
   _for(i, 1, n){
   	s[i] = s[i-1] + d[i];
   	if (d[i] == 0) continue;
   	for(re int x = mx; x >= 0; --x){
   		if (f[x] == inf) continue;
   		ud(f[x+i*d[i]], f[x] + 2*x*d[i] + i*d[i]*d[i]);
   		ud(f[x+s[i]], f[x] + s[i]*s[i]);
   		mx = max(mx, max(x+i*d[i], x+s[i]));
   		f[x] = inf;
   	}
   }
   re ll ans = inf;
   _rfor(x, 0, mx) if (f[x] < inf) ud(ans, n*f[x] - (ll)x*x);
   pf("%lld\n", ans);

   return 0;
}
posted @ 2023-09-02 19:47  linyihdfj  阅读(49)  评论(0编辑  收藏  举报