【解题报告】2022牛客寒假算法基础集训营1
- A:九小时九个人九扇门
- B:炸鸡块君与FIFA22
- C:Baby's first attempt on CPU
- D:牛牛做数论
- E:炸鸡块君的高中回忆
- F:中位数切分
- G:ACM is all you need
- H:牛牛看云
- I:B站与各唱各的
- J:小朋友做游戏
- K:冒险公社 (天了噜,简单题没人做?)
- L:牛牛学走路
- 2022牛客寒假算法基础集训营1
由于题目难度有些偏基础,太基础的今后就不细讲了。
话不多说,开冲!
A:九小时九个人九扇门
题意 | 简单
- 经典 999 游戏,之前就看过极限脱出系列的攻略全流程了…(咳咳
数字根:一个数字的各个位数字和,如果该和不是一位数,继续操作,直到一位数字,就是这个数字的数字根
比如87->(8+7=15)15->(1+5=6)6
所以87的数字根为6
- 有
n
n
n 个人,给定每个人的数字
a
i
a_i
ai
选出一些人的集合,求集合的数字根为 1 ∼ 9 1\sim 9 1∼9 的集合选法,取模 998244353 998244353 998244353
集合的数字根:为集合的所有元素的和的数字根
1 ≤ n ≤ 1 0 5 1\le n\le 10^5 1≤n≤105
1 ≤ a i ≤ 1 0 9 1\le a_i\le 10^9 1≤ai≤109
思路 | 数学 + dp
- 容易猜想或者得到,设
f
(
x
)
f(x)
f(x) 表示
x
x
x 的数字根,那么有:
f ( x ) = ( x − 1 ) m o d 9 + 1 (1) f(x)=(x-1)\bmod 9+1\tag{1} f(x)=(x−1)mod9+1(1)
还有一些性质:
f ( x + y ) = f ( f ( x ) + f ( y ) ) f ( x + 9 ) = f ( x ) f ( 9 x ) = 9 f ( x y ) = f ( f ( x ) × f ( y ) ) f(x+y)=f(f(x)+f(y))\\ f(x+9)=f(x)\\ f(9x)=9\\ f(xy)=f(f(x)\times f(y)) f(x+y)=f(f(x)+f(y))f(x+9)=f(x)f(9x)=9f(xy)=f(f(x)×f(y))
这些都可以由上面 ( 1 ) (1) (1) 式推导出来。
如果让我证明的话,感觉不大好证,我会用反证法,先假设 ( 1 ) (1) (1) 式成立
然后我能得到 f ( x + y ) = f ( f ( x ) + f ( y ) ) f(x+y)=f(f(x)+f(y)) f(x+y)=f(f(x)+f(y)) 也是同时成立的,并且这个和数字根的定义相同
所以原 ( 1 ) (1) (1) 成立 - 不管怎么样,我们可以利用这个
f
(
x
+
y
)
=
f
(
f
(
x
)
+
f
(
y
)
)
f(x+y)=f(f(x)+f(y))
f(x+y)=f(f(x)+f(y))
设 d p [ i ] [ x ] dp[i][x] dp[i][x] 表示考虑完前 i i i 个数字,数字根为 x ( x ≤ 9 ) x(x\le9) x(x≤9) 的方案数,然后转移即可。
随便计算数字根,用定义法 O ( 1 ) O(1) O(1) 还是递归法 O ( log ) O(\log ) O(log) 都行
代码
- 时间复杂度:
O
(
n
(
10
+
O
(
算
一
个
数
字
根
)
)
)
O(n(10+O(算一个数字根)))
O(n(10+O(算一个数字根)))
空间复杂度:滚动数组了一下, Θ ( 20 ) \Theta(20) Θ(20)
const int st = 0;
int cnt[15][2];
int cal(int a){ // 你当然可以写 return (x-1) % 9 + 1 更快速得到数字根
if(a < 10)return a;
return cal(cal(a / 10) + a % 10);
}
int main()
{
cnt[0][st] = 1;
int n;cin >> n;
for(int i = 1;i <= n;++i){
int t;cin >> t;
for(int j = 0;j < 10;++j){
int aim = cal(j + t);
cnt[aim][st^1] = (cnt[aim][st^1] + cnt[j][st]) % MOD;
}
for(int j = 0;j < 10;++j){
cnt[j][st] = (cnt[j][st] + cnt[j][st^1]) % MOD;
cnt[j][st^1] = 0;
}
}
for(int i = 1;i < 10;++i){
cout << cnt[i][st] << ' ';
}
return 0;
}
B:炸鸡块君与FIFA22
题意 | 中等
- 给定一个长度为
n
n
n 的字符串
S
S
S ,和
q
q
q 个询问
字符串只包含三种字符:- W W W 表示赢,加一分。
- L L L 表示输,扣一分。如果扣分前分数是三的倍数,则不扣分
- D D D 表示平局,分数不变。
- 每组询问给定 l , r , s l,r,s l,r,s ,表示初始分数为 s s s ,经历 S [ l : r ] S[l:r] S[l:r] 之后的分数为多少
-
1
≤
n
,
q
≤
2
×
1
0
5
1\le n,q\le 2\times 10^5
1≤n,q≤2×105
1 ≤ l ≤ r ≤ n 1\le l\le r\le n 1≤l≤r≤n
0 ≤ s ≤ 1 0 9 0\le s\le10^9 0≤s≤109
思路 | 数据结构 + 简单同余
- 容易想到,快速计算分数肯定要用能分治的数据结构。我这里使用倍增表。
倍增表就是:
d p [ i ] [ j ] = ⨁ i + 2 j − 1 p = i a r r p dp[i][j]=\underset{p=i}{\overset{i+2^j-1}{\bigoplus}}arr_p dp[i][j]=p=i⨁i+2j−1arrp
它比线段树要内存小,更快,但是无法修改操作。同样要求操作满足结合律。 - 这里想到,分数是三的倍数扣分不记录,则按分数取余
3
3
3 分别记录,就变成
d
p
[
i
]
[
j
]
[
0
/
1
/
2
]
dp[i][j][0/1/2]
dp[i][j][0/1/2]
想好转移。
d p [ i ] [ j ] [ k ] = d p [ i ] [ j − 1 ] [ k ] + d p [ i + 2 j − 1 ] [ j − 1 ] [ k ′ ] dp[i][j][k]=dp[i][j-1][k]+dp[i+2^{j-1}][j-1][k^\prime] dp[i][j][k]=dp[i][j−1][k]+dp[i+2j−1][j−1][k′]
- 容易得到整段的分数等于左边段的分数加上右边段的分数
这个时候 k ′ k' k′ 变成了什么呢?当然就是 k ′ = ( k + d p [ i ] [ j − 1 ] [ k ] % + 3 ) % 3 k'=(k+dp[i][j-1][k]\%+3)\%3 k′=(k+dp[i][j−1][k]%+3)%3,注意分数可能有负,所以要这么处理。 - 然后是查询。查询就是把 [ l : r ] [l:r] [l:r] 分成不同个 2 p 2^p 2p 的段,来让查询复杂度达到 O ( log ) O(\log) O(log) 级别,具体可以看代码
代码
- 时间复杂度: O ( ( n + q ) log n ) O((n+q)\log n) O((n+q)logn)
int dp[MAX][22][3];
int p2[22]; // 预处理 2^p
int main()
{
IOS;
p2[0] = 1;
for(int i = 1;i <= 20;++i)
p2[i] = p2[i-1] * 2;
int n,q;cin >> n >> q;
string ss;
cin >> ss;
ss = " " + ss;
for(int i = 1;i <= n;++i){
for(int j = 0;j < 3;++j){ // 计算 dp 初值
dp[i][0][j] = 0;
if(ss[i] == 'W')dp[i][0][j]++;
else if(ss[i] == 'L' && j != 0)dp[i][0][j]--;
}
}
for(int i = 1;i <= 20;++i) // 预处理转移 dp
for(int p = 1;p <= n;++p){
if(p + p2[i] - 1 > n)break;
for(int j = 0;j < 3;++j){
int f1 = dp[p][i-1][j];
int f2 = dp[p+p2[i-1]][i-1][((j+f1)%3+3)%3];
dp[p][i][j] = f1 + f2;
}
}
for(int i = 1;i <= q;++i){
int l,r,s;
cin >> l >> r >> s;
int delta = 0;
int now = s % 3;
for(int j = 20;j >= 0;--j){ // 查询
if(l + p2[j] - 1 <= r){
delta += dp[l][j][now];
now = (now + dp[l][j][now] % 3 + 3) % 3;
l += p2[j];
}
}
cout << s + delta << '\n';
}
return 0;
}
C:Baby’s first attempt on CPU
题意 | 简单
- 一共有
n
n
n 个语句。若语句
i
i
i 向语句
j
j
j 调用(
j
<
i
j<i
j<i),则这里两个语句之间必须至少间隔三个语句,否则会出现问题。
给定语句调用表。
你要插入最少数量的空白语句,来满足所有语句调用没有问题。 - 3 ≤ n ≤ 100 3\le n\le 100 3≤n≤100
思路 | 简单dp
- 分类讨论即可
当我们讨论下面第 i i i 个语句时,注意到,如果有需要,只会在它的左边放空白语句是最优的。
放置之后,蓝色调用将不会出现问题
绿色调用若有,则间隔需求降低。
- 然后贪心处理,解就是一定的最优的了。
代码
- 时间复杂度: O ( n ) O(n) O(n)
int aa[105][5];
int main()
{
int ans = 0;
int n;cin >> n;
for(int i = 1;i <= n;++i)
cin >> aa[i][1] >> aa[i][2] >> aa[i][3];
for(int i = 1;i <= n;++i){
if(aa[i][1] == 1){
ans += 3;
aa[i+1][2] = aa[i+1][3] = 0;
aa[i+2][3] = 0;
}else if(aa[i][2] == 1){
ans += 2;
aa[i+1][2] = aa[i+1][3] = 0;
aa[i+2][3] = 0;
}else if(aa[i][3] == 1){
ans++;
aa[i+1][3] = aa[i+1][2];
aa[i+1][2] = 0;
aa[i+2][3] = 0;
}
}
cout << ans;
return 0;
}
D:牛牛做数论
题意 | 中等
- H ( x ) = φ ( x ) x H(x)=\frac{\varphi(x)}{x} H(x)=xφ(x)
思路 | 数论
- 欧拉函数是积性函数,可以写成
φ
(
x
)
=
x
∏
(
p
i
−
1
p
i
)
\varphi(x)=x\prod (\cfrac{p_i-1}{p_i})
φ(x)=x∏(pipi−1)
所以得到 H ( x ) = ∏ ( p i − 1 p i ) H(x)=\prod (\cfrac{p_i-1}{p_i}) H(x)=∏(pipi−1) - 那么最大值,就是
[
2
,
n
]
[2,n]
[2,n] 中最大的质数取到的值
最小值,就是范围之内最多、尽量小的质数的乘积,暴力检查前 10 10 10 个质数即可。
代码
- 时间复杂度: O ( T n ) O(T\sqrt{n}) O(Tn),要有一个质数判定,根据素数密度定理,判定次数为常数
bool is_p(int x){
for(int i = 2;1LL * i * i <= x;++i){
if(x % i == 0)return false;
}
return true;
}
int pp[]={2,3,5,7,11,13,17,19,23,29};
int main()
{
int T;cin >> T;
while(T--){
int n;cin >> n;
if(n == 1){
cout << -1 << endl;
continue;
}
int L,R;
for(int i = n;;--i){
if(is_p(i)){
R = i;
break;
}
}
L = 2;
for(int i = 1;i < 10;++i){
if(1LL * L * pp[i] <= n)L *= pp[i];
else break;
}
cout << L << " " << R << endl;
}
return 0;
}
E:炸鸡块君的高中回忆
- 签到题
先特判,再做下述操作:
操作1:花两步,校园里人少掉 m − 1 m-1 m−1。
操作2:花一步,校园里人少掉 m m m,但是只能做一次。 - 设操作1 做了
k
k
k 次,答案就是最小的
k
k
k ,满足:
( m − 1 ) k + m ≥ n 即 k ≥ n − m m − 1 (m-1)k+m\ge n\\ 即 \qquad k\ge \frac{n-m}{m-1} (m−1)k+m≥n即k≥m−1n−m
答案就是 2 k + 1 2k+1 2k+1
int main()
{
IOS;
int T;cin >>T;
while(T--){
int n,m;
cin >> n >> m;
if(m == 1){
if(n == 1)cout << 1 << '\n';
else cout << -1 << '\n';
continue;
}
int k = ceil((1.0 * n - m) / (m - 1));
cout << 1LL * k * 2 + 1 << '\n';
}
return 0;
}
F:中位数切分
题意 | 中等
- 把长度为 n n n 的序列划分成最多份,满足每份的中位数 ≥ m \ge m ≥m 。若长度为偶数,中位数取中间的左边那个数字。
-
1
≤
n
≤
1
0
5
1\le n\le 10^5
1≤n≤105
1 ≤ a i , m ≤ 1 0 9 1\le a_i,m\le 10^9 1≤ai,m≤109
思路 | 数据结构 + dp
- 划分满足无后效性,想到 d p dp dp ,则设: d p [ i ] dp[i] dp[i] 表示考虑玩前 i i i 个元素,最多划分的段数
- 看到中位数,想到把元素变化。若
a
i
≥
m
a_i\ge m
ai≥m ,即为
1
1
1,否则记为
−
1
-1
−1
这样,序列的中位数 ≥ m \ge m ≥m 转化为序列的和 ≥ 1 \ge 1 ≥1 - 想
d
p
dp
dp 的转移,如下:
d p [ i ] = max { d p [ j ] + 1 } , 满 足 [ j + 1 , i ] 这 一 段 的 元 素 和 ≥ 1 dp[i]=\max\{dp[j]+1\},满足[j+1,i] 这一段的元素和 \ge 1 dp[i]=max{dp[j]+1},满足[j+1,i]这一段的元素和≥1 - 元素和,想到前缀和。我们用
p
r
e
pre
pre 数组记录前缀和,于是后面就变成了:
d p [ i ] = max { d p [ j ] + 1 } , p r e [ i ] − p r e [ j ] ≥ 1 dp[i]=\max\{dp[j]+1\},pre[i]-pre[j]\ge 1 dp[i]=max{dp[j]+1},pre[i]−pre[j]≥1
由于 p r e [ i ] pre[i] pre[i] 是固定的,我们移动元素位置,得到:
d p [ i ] = max { d p [ j ] + 1 } , p r e [ j ] ≤ p r e [ i ] − 1 dp[i]=\max\{dp[j]+1\},pre[j]\le pre[i]-1 dp[i]=max{dp[j]+1},pre[j]≤pre[i]−1 - 于是,右边就变成了求区间最小值,线段树搞定。
最后答案当然就是 d p [ n ] dp[n] dp[n] - 注意到,
p
r
e
pre
pre 数组是可以为负数的,所以我们每个位置的元素增加一个增量
O
=
1
e
5
+
2
O=1e5+2
O=1e5+2 即可。
还注意到,有些位置的分隔是不可行的,因为求的是区间最大值,用 − 1 -1 −1 做初始化。
代码
- 时间复杂度:
O
(
n
log
n
)
O(n\log n)
O(nlogn)
这里还使用了快读,就把完整代码贴上来吧。
#include<bits/stdc++.h>
using namespace std;
#define IOS ios::sync_with_stdio(false);cin.tie(NULL);cout.tie(NULL);
void show(){std::cerr << endl;}template<typename T,typename... Args>void show(T x,Args... args){std::cerr << "[ " << x << " ] , ";show(args...);}
typedef long long ll;
namespace Fast_IO{ ///orz laofu
const int MAXL((1 << 18) + 1);int iof, iotp;
char ioif[MAXL], *ioiS, *ioiT, ioof[MAXL],*iooS=ioof,*iooT=ioof+MAXL-1,ioc,iost[55];
char Getchar(){
if (ioiS == ioiT){
ioiS=ioif;ioiT=ioiS+fread(ioif,1,MAXL,stdin);return (ioiS == ioiT ? EOF : *ioiS++);
}else return (*ioiS++);
}
void Write(){fwrite(ioof,1,iooS-ioof,stdout);iooS=ioof;}
void Putchar(char x){*iooS++ = x;if (iooS == iooT)Write();}
inline int read(){
int x=0;for(iof=1,ioc=Getchar();(ioc<'0'||ioc>'9')&&ioc!=EOF;)iof=ioc=='-'?-1:1,ioc=Getchar();
if(ioc==EOF)exit(0);
for(x=0;ioc<='9'&&ioc>='0';ioc=Getchar())x=(x<<3)+(x<<1)+(ioc^48);return x*iof;
}
inline long long read_ll(){
long long x=0;for(iof=1,ioc=Getchar();(ioc<'0'||ioc>'9')&&ioc!=EOF;)iof=ioc=='-'?-1:1,ioc=Getchar();
if(ioc==EOF)exit(0);
for(x=0;ioc<='9'&&ioc>='0';ioc=Getchar())x=(x<<3)+(x<<1)+(ioc^48);return x*iof;
}
/**
#define lll __int128
inline lll read_lll(){
lll x=0;for(iof=1,ioc=Getchar();(ioc<'0'||ioc>'9')&&ioc!=EOF;)iof=ioc=='-'?-1:1,ioc=Getchar();
if(ioc==EOF)exit(0);
for(x=0;ioc<='9'&&ioc>='0';ioc=Getchar())x=(x<<3)+(x<<1)+(ioc^48);return x*iof;
}*/
template <class Int>void Print(Int x, char ch = '\0'){
if(!x)Putchar('0');if(x<0)Putchar('-'),x=-x;while(x)iost[++iotp]=x%10+'0',x/=10;
while(iotp)Putchar(iost[iotp--]);if (ch)Putchar(ch);
}
void Getstr(char *s, int &l){
for(ioc=Getchar();ioc==' '||ioc=='\n'||ioc=='\t';)ioc=Getchar();
if(ioc==EOF)exit(0);
for(l=0;!(ioc==' '||ioc=='\n'||ioc=='\t'||ioc==EOF);ioc=Getchar())s[l++]=ioc;s[l] = 0;
}
void Putstr(const char *s){for(int i=0,n=strlen(s);i<n;++i)Putchar(s[i]);}
} // namespace Fast_IO
using namespace Fast_IO;
const int MAX = 2e5+50;
const int MOD = 998244353;
const int PHI = MOD-1;
ll qpow(ll a,ll n){/* */ll res = 1LL;while(n){if(n&1)res=res*a%MOD;a=a*a%MOD;n>>=1;}return res;}
ll qpow(ll a,ll n,ll p){a%=p;ll res = 1LL;while(n){if(n&1)res=res*a%p;a=a*a%p;n>>=1;}return res;}
ll npow(ll a,ll n){/* */ll res = 1LL;while(n){if(n&1)res=res*a;a=a*a;n>>=1;if(res<0||a<0)return 0;}return res;}
ll inv(ll a){/* */return qpow(a,MOD-2);}
ll inv(ll a,ll p){return qpow(a,p-2,p);}
#define ls (p<<1)
#define rs (p<<1|1)
#define md ((l+r)>>1)
const int O = 1e5+2;
const int U = 2e5+4;
int tree[MAX*4];
inline void push_up(int p){
tree[p] = max(tree[ls],tree[rs]);
}
inline void build(int p,int l,int r){
if(l == r){
tree[p] = -1;
return;
}
build(ls,l,md);
build(rs,md+1,r);
push_up(p);
}
inline void add(int p,int l,int r,int k){
tree[p] = k;
}
inline void update(int p,int l,int r,int ux,int uy,int k){
if(ux <= l && uy >= r){
add(p,l,r,k);
return;
}
if(ux <= md)update(ls,l,md,ux,uy,k);
if(uy > md)update(rs,md+1,r,ux,uy,k);
push_up(p);
}
inline int query(int p,int l,int r,int qx,int qy){
int res = -1;
if(qx <= l && r <= qy)return tree[p];
if(qx <= md)res = max(res,query(ls,l,md,qx,qy));
if(qy > md)res = max(res,query(rs,md+1,r,qx,qy));
return res;
}
int pre[100050];
int main()
{
int T;
scanf("%d",&T);
while(T--){
int n,m;scanf("%d%d",&n,&m);
for(int i = 1;i <= n;++i){
int t;scanf("%d",&t);
if(t >= m)t = 1;
else t = -1;
pre[i] = pre[i-1] + t;
}
build(1,1,U);
update(1,1,U,O,O,0);
int ans = -1;
for(int i = 1;i <= n;++i){
int dpj = query(1,1,U,1,O + pre[i] - 1);
if(dpj != -1){
int dpi = dpj + 1;
if(i == n)ans = dpi;
update(1,1,U,O + pre[i],O + pre[i],dpi);
}
}
printf("%d\n",ans);
}
return 0;
}
G:ACM is all you need
题意 | 中偏难
- 给定一维函数上
n
n
n 个整数点
f
i
f_i
fi
定义 l o c a l m i n local\ min local min 表示满足 f i < min ( f i − 1 , f i + 1 ) f_i<\min(f_{i-1},f_{i+1}) fi<min(fi−1,fi+1) - 变化,表示让所有的值
f
i
f_i
fi 变成
f
i
=
∣
f
i
−
b
∣
+
b
f_i=|f_i-b|+b
fi=∣fi−b∣+b,其中
b
b
b 是自己选择的一个整数
求经过一次变化, l o c a l m i n local\ min local min 的最小值是多少 -
∑
n
≤
1
0
6
\sum n\le 10^6
∑n≤106
1 ≤ f i ≤ 1 0 9 1\le f_i\le 10^9 1≤fi≤109
思路 | 思维 + 差分
- 首先,每个点都加
b
b
b 当然不影响
l
o
c
a
l
m
i
n
local\ min
local min ,我们把变化看做
f
i
=
∣
f
i
−
b
∣
f_i=|f_i-b|
fi=∣fi−b∣
看到几何意义,就是这个函数上的每个点距离 y = b y=b y=b 的直线的距离。
我们可以当做把这个函数沿着某个 y y y 向上翻折,但是这样对整体答案不好考虑。 - 我们只考虑单独一个位置。
由于 l o c a l m i n local\ min local min 只和当前位置与前后两个相邻位置有影响。
且仔细分析,我们只用关注增减情况即可,只有四种抽象情况:
- 因为如果存在
f
i
=
f
i
−
1
f_i=f_{i-1}
fi=fi−1 或者
f
i
=
f
i
+
1
f_i=f_{i+1}
fi=fi+1,这个点变化前和变化后当然都不会是
l
o
c
a
l
m
i
n
local\ min
local min
接下来,我们分析当 y y y 处于什么情况下, l o c a l m i n local\ min local min 的变化值
我们记录成一个段 [ L , R , D ] [L,R,D] [L,R,D] ,表示 y ∈ [ L , R ] y\in [L,R] y∈[L,R] 时候, l o c a l m i n local\ min local min 的变化值为 D D D
当然, R R R 可以是正无穷,我们使用一个正大值 I N F INF INF 表示。(为什么 L L L 不用负大值记录,可以仔细想想) - 于是问题就变成,给定许多个段,求处于哪个位置,
l
o
c
a
l
m
i
n
local\ min
local min 是最小的
我们只要对段排序,位置肯定是某个端点处的位置。
这个时候我们可能会想到,退出某个段, D D D 的影响就没了,应该把 D D D 退回,这样就写成滑动窗口了,我们可以用简单差分:再累加一个 [ R + 1 , I N F , − D ] [R+1,INF,-D] [R+1,INF,−D] 的段把影响去掉即可。
代码
- 时间复杂度: O ( n log n ) O(n\log n) O(nlogn)
int aa[MAX];
int cnt;
struct node{
ll L,R;
int delta;
bool operator<(const node &ND)const{
if(L != ND.L)return L < ND.L;
return R < ND.R;
}
}seg[2 * MAX];
int main()
{
int T = read();
while(T--){
int n = read();
for(int i = 1;i <= n;++i){
aa[i] = read();
}
int ans1 = 0;
cnt = 0;
for(int i = 2;i <= n-1;++i){
if(aa[i] == aa[i-1] || aa[i] == aa[i+1])continue;
ll L,R,D;
if(aa[i-1] < aa[i] && aa[i] < aa[i+1]){
L = (aa[i] + aa[i-1]) / 2 + 1;
R = (aa[i] + aa[i+1] + 1) / 2 - 1;
D = 1;
cnt++;
seg[cnt].L = L;
seg[cnt].R = R;
seg[cnt].delta = D;
cnt++;
seg[cnt].L = R+1;
seg[cnt].R = INF;
seg[cnt].delta = -D;
}else if(aa[i-1] < aa[i] && aa[i] > aa[i+1]){
int L1 = (aa[i] + aa[i-1]) / 2 + 1;
int L2 = (aa[i] + aa[i+1]) / 2 + 1;
L = max(L1,L2);
R = INF;
D = 1;
cnt++;
seg[cnt].L = L;
seg[cnt].R = R;
seg[cnt].delta = D;
}else if(aa[i-1] > aa[i] && aa[i] > aa[i+1]){
L = (aa[i] + aa[i+1]) / 2 + 1;
R = (aa[i] + aa[i-1] + 1) / 2 - 1;
D = 1;
cnt++;
seg[cnt].L = L;
seg[cnt].R = R;
seg[cnt].delta = D;
cnt++;
seg[cnt].L = R+1;
seg[cnt].R = INF;
seg[cnt].delta = -D;
}else{
ans1++;
int L1 = (aa[i] + aa[i-1] + 1) / 2;
int L2 = (aa[i] + aa[i+1] + 1) / 2;
L = min(L1,L2);
R = INF;
D = -1;
cnt++;
seg[cnt].L = L;
seg[cnt].R = R;
seg[cnt].delta = D;
}
}
sort(seg+1,seg+1+cnt);
int ans = ans1;
int delta = 0;
for(int i = 1;i <= cnt;++i){
delta += seg[i].delta;
if(i == cnt || (seg[i].L != seg[i+1].L))ans = min(ans,ans1+delta); // 可以想想为什么要这个if
}
cout << ans << '\n';
}
return 0;
}
H:牛牛看云
题意 | 简单偏中等
- 给定
n
,
a
i
n,a_i
n,ai,求
∑ i = 1 n ∑ j = i n ∣ a i + a j − 1000 ∣ \sum_{i=1}^n \sum_{j=i}^n |a_i+a_j-1000| i=1∑nj=i∑n∣ai+aj−1000∣ -
3
≤
n
≤
1
0
6
3\le n\le 10^6
3≤n≤106
0 ≤ a i ≤ 1000 0\le a_i\le 1000 0≤ai≤1000
思路 | 思维
- 第一个想法, 右侧的
∑
\sum
∑ 中,
a
i
−
1000
a_i-1000
ai−1000 是一个常量,我们可以想做:
∑ i = 1 n ∑ j = i n ∣ a j − x ∣ \sum_{i=1}^n \sum_{j=i}^n |a_j-x| i=1∑nj=i∑n∣aj−x∣
其中 x = a i − 1000 x=a_i-1000 x=ai−1000
可以转化成这 n − i + 1 n-i+1 n−i+1 个点到某个点 x x x 之间的距离和,但是貌似挺复杂的,我们有更简单的方法 - 第二个想法,我们利用好
a
i
a_i
ai 的值域比较小,我们可以把题目化成:
a n s = p a i r x × ∣ x − 1000 ∣ ans=pair_x \times |x-1000| ans=pairx×∣x−1000∣
其中 p a i r x pair_x pairx 表示有多少对 ( i , j ) (i,j) (i,j),满足 1 ≤ i ≤ j ≤ n 1\le i\le j\le n 1≤i≤j≤n 满足 x = a i + a j x=a_i+a_j x=ai+aj
我们可以记 c n t x cnt_x cntx 表示 n n n 个数字中有多少个数字为 x x x - 怎么算呢,如果
a
i
=
a
j
a_i=a_j
ai=aj ,合法对数明显为
c
n
t
(
c
n
t
+
1
)
2
\frac{cnt(cnt+1)}{2}
2cnt(cnt+1)
如果 a i ≠ a j a_i\ne a_j ai=aj ,合法对数也明显为 c n t a i × c n t a j cnt_{a_i} \times cnt_{a_j} cntai×cntaj
代码
- 时间复杂度: O ( 100 0 2 ) O(1000^2) O(10002)
int aa[MAX];
int cnt[1005];
int cal(int x){
return abs(x - 1000);
}
int main()
{
ll ans = 0;
int n = read();
for(int i = 1;i <= n;++i){
aa[i] = read();
cnt[aa[i]]++;
}
for(int i = 0;i <= 2000;++i){
for(int j = 0;j <= 1000;++j){
if(i-j < 0 || i-j > 1000)continue;
if(i - j == j){
ans += 1LL * cnt[j] * (cnt[j] + 1) / 2 * cal(i);
continue;
}else if(i-j < j){
ans += 1LL * cnt[i-j] * cnt[j] * cal(i);
}
}
}
Print(ans);
Write();
return 0;
}
I:B站与各唱各的
题意 | 中等
-
T
T
T 组样例,每组给定
n
,
m
n,m
n,m
表示有 n n n 个人, m m m 句歌词。每个人可以选择每句歌词唱不唱,不能与他人沟通。
若某句歌词被所有人唱了,或者所有人都没唱,那么这句是失败的,否则是成功的。
每个人都十分聪明,求唱成功的句的期望取模 1 e 9 + 7 1e9+7 1e9+7
思路 | 数学
- 我们随便想一想,容易得到一个策略:每个人对每句随机选择
(
1
2
概
率
)
(\frac{1}{2} 概率)
(21概率) 唱与不唱,那么策略是最优的。
每句假设是独立的。如果每个人采取这样的策略,那么唱失败的概率只有 2 2 n \cfrac{2}{2^n} 2n2
容易得到,答案即为:
m × 2 n − 2 2 n m\times \frac{2^n-2}{2^n} m×2n2n−2
代码
- 时间复杂度: O ( T log n ) O(T\log n) O(Tlogn)
ll qpow(ll a,ll n){/* */ll res = 1LL;while(n){if(n&1)res=res*a%MOD;a=a*a%MOD;n>>=1;}return res;}
ll qpow(ll a,ll n,ll p){a%=p;ll res = 1LL;while(n){if(n&1)res=res*a%p;a=a*a%p;n>>=1;}return res;}
ll npow(ll a,ll n){/* */ll res = 1LL;while(n){if(n&1)res=res*a;a=a*a;n>>=1;if(res<0||a<0)return 0;}return res;}
ll inv(ll a){/* */return qpow(a,MOD-2);}
ll inv(ll a,ll p){return qpow(a,p-2,p);}
int main()
{
int T;cin >> T;
while(T--){
int n,m;cin >> n >> m;
cout << 1LL * m * (qpow(2,n) - 2 + MOD) % MOD * inv(qpow(2,n)) % MOD << endl;
}
return 0;
}
J:小朋友做游戏
- 签到。考虑选
a
a
a 个吵的,
b
b
b 个不吵的,满足
a
+
b
=
n
a+b=n
a+b=n 且
a
≤
n
2
a\le \frac{n}{2}
a≤2n
选吵的 x x x 个,只要选幸福度最高的 x x x 个吵的即可。做一个排序然后一个前缀和即可。
K:冒险公社 (天了噜,简单题没人做?)
题意 | 简单
-
n
n
n 个岛,每个岛为
R
G
B
RGB
RGB 三色之一
- 3 ≤ n ≤ 1 0 5 3\le n\le 10^5 3≤n≤105
思路 | 简单dp
- 发现,第
i
i
i 个预测和
[
1
,
i
−
3
]
[1,i-3]
[1,i−3] 的所有岛都没有关系,明显符合 dp 的无后效性
答案求的是绿色岛的最大值。
发现和每个岛的颜色有关,直接设 d p [ i ] [ j ] [ k ] [ p ] dp[i][j][k][p] dp[i][j][k][p] 表示走完前 i i i 个岛,其中岛 i , i − 1 , i − 2 i,i-1,i-2 i,i−1,i−2 的颜色分别为 j , k , p j,k,p j,k,p ,合法方案中绿色岛最多的值 - 然后考虑转移,每次多走一格岛,明显转移是从
d
p
[
i
−
1
]
[
i
2
]
[
i
3
]
[
i
4
]
dp[i-1][i2][i3][i4]
dp[i−1][i2][i3][i4] 转移到
d
p
[
i
]
[
i
1
]
[
i
2
]
[
i
3
]
dp[i][i1][i2][i3]
dp[i][i1][i2][i3]
什么时候转移呢?合法转移,看这个位置的罗盘预测是否满足即可
代码
- 时间复杂度: O ( n × 3 4 ) O(n\times 3^4) O(n×34)
/**
0 1 2
R G B
*/
int dp[MAX][3][3][3];
int main()
{
int n;cin >> n;
string ss;cin >> ss;
for(int i = 2;i < n;++i)
for(int i1 = 0;i1 < 3;++i1)
for(int i2 = 0;i2 < 3;++i2)
for(int i3 = 0;i3 < 3;++i3)
dp[i][i1][i2][i3] = -1;
for(int i = 0;i < 3;++i)
for(int i2 = 0;i2 < 3;++i2)
for(int i3 = 0;i3 < 3;++i3){
dp[1][i][i2][i3] = 0;
if(i == 1)dp[1][i][i2][i3]++;
if(i2 == 1)dp[1][i][i2][i3]++;
}
int ans = -1;
for(int i = 2;i < n;++i)
for(int i1 = 0;i1 < 3;++i1)
for(int i2 = 0;i2 < 3;++i2)
for(int i3 = 0;i3 < 3;++i3)
for(int i4 = 0;i4 < 3;++i4)
if(dp[i-1][i2][i3][i4] != -1){
int pre = dp[i-1][i2][i3][i4];
int R = 0,G = 0;
if(i1 == 0)R++;
else if(i1 == 1)G++;
if(i2 == 0)R++;
else if(i2 == 1)G++;
if(i3 == 0)R++;
else if(i3 == 1)G++;
int now = 0;
if(i1 == 1)now++;
if(ss[i] == 'G' && G > R){
dp[i][i1][i2][i3] = max(dp[i][i1][i2][i3],pre + now);
}else if(ss[i] == 'R' && R > G){
dp[i][i1][i2][i3] = max(dp[i][i1][i2][i3],pre + now);
}else if(ss[i] == 'B' && R == G){
dp[i][i1][i2][i3] = max(dp[i][i1][i2][i3],pre + now);
}
if(i == n-1)ans = max(ans,dp[i][i1][i2][i3]);
}
cout << ans;
return 0;
}
L:牛牛学走路
- 签到