codeforces div_2 936 题解报告
codeforces div_2 936 题解报告
比赛链接:https://codeforces.com/contest/1946
A.Median of an Array
做法tag:签到
题目翻译
给定一个长度为\(n\)的数组\(a\),记数组中非降序排列中第\({\lceil \frac n 2 \rceil}\)个数是数组a的中位数。
我们可以做以下操作。
- 选择一个数\(i \in[1,n]\),使\(a_i:=a_i+1\)
问,对于给定的数组,我们至少要作多少次操作,才能使中位数变大。
- 时限1s,空间限制256MB
- \(n<=1e5,1<=a_i<=1e9\)
解法
先对数组排序,设\(j\)是满足\(a_j=a_{\lceil \frac n 2 \rceil}\)的最大整数,子数组\(a[\lceil \frac n 2 \rceil:j]\)的值都相等,我们要使此子数组的每个数都增大才能使中位数增大,也就要操作\(j-\lceil \frac n 2 \rceil +1\)次。
点我查看代码
#include<bits/stdc++.h>
#define ll long long
//#define int long long
#define mid ((l+r)>>1)
#define lson u<<1,l,mid
#define rson u<<1|1,mid+1,r
#define inf 0x3f3f3f3f
typedef int LL;
const signed maxn=(signed)1e6+5;
inline LL Read(){
char ch=getchar();bool f=0;LL x=0;
for(;!isdigit(ch);ch=getchar())if(ch=='-')f=1;
for(;isdigit(ch);ch=getchar())x=(x<<1)+(x<<3)+(ch^48);
if(f==1)x=-x;return x;
}
int ar[maxn];
signed main()
{
LL T=Read();
while (T--){
int n=Read();
for(int i=1;i<=n;++i) ar[i]=Read();
std::sort(ar+1,ar+1+n);
int md=(n+1)/2,c=1;
for(int i=md+1;i<=n;++i){
if(ar[i]==ar[i-1]) ++c;
else break;
}
printf("%d\n",c);
}
return 0;
}
B.Maximum Sum
做法tag:贪心
题目翻译
给定一个长度为\(n\)的数组\(a\),我们要对数组做以下操作恰好\(k\)次。
- 选择一段连续子数组(可以为空,和为\(0\)),记\(sum\)为这一段子数组中所有数的和,将\(sum\)插入到任意我们想插入的位置。
问,\(k\)次操作后数组\(a\)的和的可能的最大值是多少(输出时对\(1e9+7\)取模)。
- 时限1s,空间限制256MB
- \(n,k<=2e5,-1e9<=a_i<=1e9\)
解法
第一次操作,找到我们能找到的最大\(sum_1\)(找不到正的\(sum_1\)就选空,记\(0\)),然后插入到我们找到的子数组中,第二次的\(sum_2=2\times sum_1\),\(sum_k=2^{k-1} \times sum_1\)。记原数组和是\(sum_a\),最后答案就是$$sum_a+(2^k-1)\times sum_1 $$
点我查看代码
#include<bits/stdc++.h>
#define ll long long
//#define int long long
#define mid ((l+r)>>1)
#define lson u<<1,l,mid
#define rson u<<1|1,mid+1,r
#define inf 0x3f3f3f3f
typedef int LL;
const signed maxn=(signed)1e6+5;
const signed mod=(signed)1e9+7;
inline LL Read(){
char ch=getchar();bool f=0;LL x=0;
for(;!isdigit(ch);ch=getchar())if(ch=='-')f=1;
for(;isdigit(ch);ch=getchar())x=(x<<1)+(x<<3)+(ch^48);
if(f==1)x=-x;return x;
}
int add(int a,int b){
return ((a+=b)>=mod)?a-mod:a;
}
int mul(int x,int y){
return (ll)x*y%mod;
}
int fpow(int a,int b){
int ans=1;
while(b){
if(b&1) ans=mul(ans,a);
a=mul(a,a);
b>>=1;
}
return ans;
}
int ar[maxn];
signed main()
{
LL T=Read();
while (T--){
int n=Read(),k=Read();
for(int i=1;i<=n;++i) ar[i]=Read();
int sum=0;
ll mx=0,c=0;
for(int i=1;i<=n;++i){
int a=ar[i];
if(a<0) sum=add(sum,mod+a);
else sum=add(sum,a);
c+=a;
mx=std::max(c,mx);
if(c<0) c=0;
}
mx%=mod;
sum=add(sum,mod-mx);
mx=mul(mx,fpow(2,k));
sum=add(sum,mx);
printf("%d\n",sum);
}
return 0;
}
C.Tree Cutting
tag:二分答案,贪心check
题目翻译
给定一棵\(n\)个结点的树,给定一个数\(k(k<n)\),要求删去树上\(k\)条边,要求删边后每个独立的树的大小都不小于\(x\)。问\(x\)最大可以取多少。
- 时限3s,空间限制512MB
- \(1<=k<n<=1e5\)
解法
\(x\)肯定越小越能实现。考虑二分答案。\(x \in [1,n]\),二分复杂度\(O(\log n)\)。
对于特定\(x\),检查方法如下,随意定一个树根,对树\(dfs\),如果某一个子树大小超过\(x\),就贪心的把子树剪出来,剪出来之后这棵子树的\(siz\)就不会上传给父亲。如果已经剪了\(k\)颗子树出来,也就是已经删去\(k\)条边,不用再剪子树了。最后在是否删去足够的边,并判断原树被剪去子树后大小(记为\(siz_0\))是否满足\(siz_0>=x\)。
- tips:不用考虑要不要挑小的子树删,因为当可以选择的时候,也就是有两颗子树\(siz>=x\),哪一颗留在原树上都能满足\(siz_0>=x\)。
点我查看代码
#include<bits/stdc++.h>
#define ll long long
//#define int long long
#define mid ((l+r)>>1)
#define lson u<<1,l,mid
#define rson u<<1|1,mid+1,r
#define inf 0x3f3f3f3f
typedef int LL;
const signed maxn=(signed)1e6+5;
inline LL Read(){
char ch=getchar();bool f=0;LL x=0;
for(;!isdigit(ch);ch=getchar())if(ch=='-')f=1;
for(;isdigit(ch);ch=getchar())x=(x<<1)+(x<<3)+(ch^48);
if(f==1)x=-x;return x;
}
int n,k,x;
struct Edge{
int to,nxt;
}edge[maxn<<1];
int head[maxn],ecnt;
void Adde(int u,int v){
edge[++ecnt]=(Edge){v,head[u]};
head[u]=ecnt;
}
int ans;
int siz[maxn];
void dfs(int u,int Fa){
siz[u]=1;
for(int i=head[u];i;i=edge[i].nxt){
int v=edge[i].to;
if(v==Fa) continue;
dfs(v,u);
if(siz[v]>=x&&ans<k) ++ans;
else siz[u]+=siz[v];
}
if(u==1&&siz[1]<x) ans=0;
}
signed main()
{
LL T=Read();
while (T--){
n=Read(),k=Read();
for(int i=1;i<=n;++i) head[i]=0;ecnt=0;
for(int i=1;i<n;++i){
int u(Read()),v(Read());
Adde(u,v),Adde(v,u);
}
int l=1,r=n/k;
while(l<=r){
x=mid;ans=0;
dfs(1,0);
if(ans<k) r=x-1;
else l=x+1;
}
printf("%d\n",r);
}
return 0;
}
D.Birthday Gift
做法tag:位运算,按位贪心
题目翻译
给定一个数组\(a\)和整数\(x\),我们要给出\(k\)个区间$$[l_1,r_1],[l_2,r_2],...,[l_k,r_k]$$
满足
- \(l_1=1,r_k=n\)
- \(\forall i\in N,(i\in [1,k-1])\to (l_{i+1}=r_i+1)\)
-
\[(a_{l_1} \oplus a_{l_1+1}\oplus ... \oplus a_{r_1})|(a_{l_2} \oplus a_{l_2+1}\oplus ... \oplus a_{r_2})|...|(a_{l_k} \oplus a_{l_k+1}\oplus...a_{r_k}) <=x \]
问,\(k\)最大可以取多少,若不存在满足条件的\(k\),输出\(-1\)。
- 时限2s,空间限制256MB
- \(n<=1e5,0<=a_i<2^{30}\)
解法
前两个条件就是把数组恰好划分为\(k\)个连续子数组。
先划分为\(n\)个区间,每个区间只有一个数。然后按位由高到低考虑第三个条件。若是或的结果太大,就把一些连续区间异或起来减小结果。我们只关心区间个数,并且我们对区间只有合并操作,我们只记录一个异或和,用一个数(异或和)代表这个区间,最后看看有几个数即可。具体如下。
记目前\(a\)中有\(f\)个数第\(i\)位为\(1\)。
- 若\(x\)第\(i\)位为\(0\),\(f\)是奇数,那么异或也不会使奇数个\(1\)变为\(0\),无法满足条件,退出。
- 若\(x\)第\(i\)位为\(0\),\(f\)是偶数,那么我们按照就近原则分为\(\frac f 2\)组,每一组两个数和中间所有数合并到一个区间里去。
- 若\(x\)第\(i\)位为\(1\),\(f\)是偶数,可以尝试合并区间,这样这一位就是小于关系,可以得到满足条件的答案,但也有可能不合并,这一位双方取等号,看低位会不会有更小的答案。
所以处理起来就是,尝试合并,更新答案,但是原数组不变,接着去处理低位。
4.若\(x\)第\(i\)位为\(1\),\(f\)是奇数,这一位注定是取等号,所以不做任何处理。看下一位。
这样基本就完成了,但是一直把相等的情况留给低位处理,没有实际处理完全相等的情况。所以在最低位要特殊处理。
当\(i=0\),也就是最低位的处理逻辑
- 若\(x\)第\(i\)位为\(0\),\(f\)是奇数,那么异或也不会使奇数个\(1\)变为\(0\),无法满足条件,退出。
- 若\(x\)第\(i\)位为\(0\),\(f\)是偶数,合并完后记录答案。
- 若\(x\)第\(i\)位为\(1\),无需合并直接记录答案。
这样就彻底完结了。
点我查看代码
#include<bits/stdc++.h>
#define ll long long
//#define int long long
#define mid ((l+r)>>1)
#define lson u<<1,l,mid
#define rson u<<1|1,mid+1,r
#define inf 0x3f3f3f3f
typedef int LL;
const signed maxn=(signed)1e6+5;
inline LL Read(){
char ch=getchar();bool f=0;LL x=0;
for(;!isdigit(ch);ch=getchar())if(ch=='-')f=1;
for(;isdigit(ch);ch=getchar())x=(x<<1)+(x<<3)+(ch^48);
if(f==1)x=-x;return x;
}
std::vector<int> arr1,arr2;
int n,x;
int merge(int bit,bool f){//f=1 true merge,f=0 do a test
int val=0,newn=0;
int bound=1<<bit;
for(int i=1;i<=n;++i){
val^=arr1[i];
if(val<bound){
arr2[++newn]=val;val=0;
}
}
if(f) std::swap(arr1,arr2);
return newn;
}
signed main()
{
LL T=Read();
while (T--){
n=Read(),x=Read();
arr1.resize(n+5);
arr2.resize(n+5);
for(int i=1;i<=n;++i) arr1[i]=Read();
int ans=-1;
for(int i=29;~i;--i){
int f=0;
bool xf=(x>>i)&1;
for(int j=1;j<=n;++j)
if((arr1[j]>>i)&1) ++f;
if(i==0){
if(xf) ans=std::max(ans,n);
else if(f%2==0)
ans=std::max(ans,merge(i,0));
else break;
}
else if(f%2==1&&xf==0) break;
else if(f%2==0&&xf) ans=std::max(ans,merge(i,0));
else if(f%2==0&&xf==0) n=merge(i,1);
for(int j=1;j<=n;++j){
if((arr1[j]>>i)&1) arr1[j]-=(1<<i);
}
}
printf("%d\n",ans);
}
return 0;
}
E.Girl Permutation
做法tag:组合数,分治?
题目翻译
给定一个数\(n\),有一个未知的长度为\(n\)的排列,排列就是\(1\) ~ \(n\)中每个数在排列中恰好出现一次。
给出两个定义
- 前缀最大数。当\(\forall j,(1<=j<i) \to a_j<a_i\),称\(a_i\)是前缀最大数。
- 后缀最大数。当\(\forall j,(i<j<=n) \to a_j<a_i\),称\(a_i\)是后缀最大数。
给出未知排列前缀最大数的个数\(m_1\)和所有的前缀最大数的下标(升序给出),还有后缀最大数的个数\(m_2\)和所有的后缀最大数的下标(升序给出)。
问,有多少种排列可能是未知排列,输出个数(对\(1e9+7\)取模)。
- 时限2s,空间限制256MB
- \(1<=m_1,m_2<=n<=2e5\)
解法
用数组\(ar\)记录前缀最大数下标,\(br\)记录后缀最大数下标。
一个显然的性质,最后一个前缀最大数和后缀最大数是最大的数\(n\)。也同时要求下标数组\(ar_{m1}=br_1\)。还有\(ar_1=1,br_{m2}=n\)
再先给出一个结论:我们只关心数之间的相对大小,绝对大小无意义,又因为数字两两不同,所以数字数量相同,对于同一种题目条件,得到的个数是一致的。
这说明了什么呢?
一个独立的区间,需要的数字数量是固定的,如果我们能够把全局前缀最大和全局后缀最大给改成区间条件,那么这个区间就可以是一个独立问题。我们记录区间左右端点是\(l,r\),排列个数用\(solve(l,r)\)表示。
那么,对于其他数字而言,\(n\)是不可跨越的大山,\(n\)左端的区间和右端的区间互相独立,左区间不会再出现后缀最大数,左区间要满足的全局前缀最大数也可以转化为区间内的前缀最大(后面的数不影响前缀)。左区间可以成为独立问题。右区间同理。
假设\(n\)被放在了\(pos_n\)s上。那么$$solve(1,n)=C \dbinom{pos_n-1}{n-1}\times solve(1,pos_n-1)\times(pos_n+1,n)$$
组合数\(C \dbinom{pos_n-1}{n-1}\)就是把数分成特定大小的两组的可能数。
对于区间\([1,pos_n-1]\),这是一个只需要满足一些前缀最大的区间。假设其中最大的数是\(x\),\(x\)就是最大的,最后面的那个前缀最大数,位置记作\(pos_x\),对于\([1,pos_x-1]\)仍然是只有前缀最大数的区间,可以递归求解,直到\([1,1]\)返回\(1\)。对于\([pos_x+1,pos_n-1]\),这个区间怎样都不会出前缀最大和后缀最大,也不需要出,随意排序即可。
又\(solve(pos_x+1,pos_n-1)=(pos_n-pos_x-1)!\)
所以
之后递归求解即可。后缀也同理。
放代码(我写的是非递归版本,只要搞清楚每一个前缀最大数的下标的贡献即可)。
点我查看代码
#include<bits/stdc++.h>
#define ll long long
//#define int long long
#define mid ((l+r)>>1)
#define lson u<<1,l,mid
#define rson u<<1|1,mid+1,r
#define inf 0x3f3f3f3f
typedef int LL;
const signed maxn=(signed)1e6+5;
const signed mod=1e9+7;
inline LL Read(){
char ch=getchar();bool f=0;LL x=0;
for(;!isdigit(ch);ch=getchar())if(ch=='-')f=1;
for(;isdigit(ch);ch=getchar())x=(x<<1)+(x<<3)+(ch^48);
if(f==1)x=-x;return x;
}
int jc[maxn],inv[maxn];
int mul(int x,int y){
return (ll)x*y%mod;
}
int A(int m,int n){
//printf("m=%d n=%d\n",m,n);
return mul(jc[m],inv[m-n]);
}
int C(int m,int n){
//printf("m=%d n=%d\n",m,n);
return mul(jc[m],mul(inv[m-n],inv[n]));
}
int fpow(int a,int b){
int ans=1;
while(b){
if(b&1) ans=mul(ans,a);
b>>=1;a=mul(a,a);
}
return ans;
}
int ar[maxn],br[maxn];
signed main()
{
int mx=3e5;
jc[0]=inv[0]=1;
for(int i=1;i<=mx;++i) jc[i]=mul(jc[i-1],i);
inv[mx]=fpow(jc[mx],mod-2);
for(int i=mx-1;i;--i) inv[i]=mul(inv[i+1],i+1);
//printf("qwq\n");
LL T=Read();
while (T--){
int n=Read(),m1=Read(),m2=Read();
for(int i=1;i<=m1;++i) ar[i]=Read();
for(int i=1;i<=m2;++i) br[i]=Read();
if(ar[m1]!=br[1]||ar[1]!=1||br[m2]!=n){
printf("0\n");continue;
}
int ans=C(n-1,ar[m1]-1);
for(int i=1;i<m1;++i){
ans=mul(ans,A(ar[i+1]-2,ar[i+1]-ar[i]-1));
}
for(int i=1;i<m2;++i){
ans=mul(ans,A(n-br[i]-1,br[i+1]-br[i]-1));
}
printf("%d\n",ans);
}
return 0;
}
F.Nobody is needed
做法tag:树状数组,离线
题目翻译
给定一个长度为\(n\)的排列\(a\)。给询问次数\(q\),每一次询问给两个参数\(l,r\)。要求输出合法序列数。下面数合法序列条件。
序列长度记为\(k(k>=1)\),记序列为\((t_1,t_2,...t_k)\)
1.\(l<=t_1<t_2<...<t_k<=r\)
2.对于整数\(i \in [1,k-1],a_{t_i+1}\)是\(a_{t_i}\)的倍数。
- 时限6s,空间限制512MB
- \(n,q<=1e6,1<=l<=r<=n\)
解法
首先我们得知道怎么计算合法序列数。
举例的时候我用数字而非下标表示合法序列,一样的。
以样例为例。排列\(2\ 1\ 6\ 3\ 5\ 4\ 8\ 7\),对于询问\(l=1,r=8\),合法序列有很多,我们考虑\(8\)作为结尾的\((8),(4,8),(1,4,8),(2,4,8),(1,8),(2,8)\)
我们对这些做一个分类\((8)\times 1,(...,4,8)\times 3,(..,1,8)\times 1,(...,2,8)\times 1\)
而我们可以发现以\(4\)结尾的序列数为\((4),(1,4),(2,4)\)恰好是\(3\)个,而\((...,4,8)\)也是\(3\)个。
这显然是因为加入\(8\)做结尾对\(4\)毫无影响,所以\(4\)的序列数全部被\(8\)继承了。
所以,我们知道对于给定的序列,某一个数\(x\)位置记录为\(pos_x\),以\(x\)结尾的序列数记录为\(cnt_x\)。
\([Exp]=\begin{cases}1& if\ Exp=true \\ 0 & if\ Exp = false \end{cases}\)
位置前面的不会继承后面的,所以从前往后做即可。
这样子的话就是对于一个给定区间,我们通过记录每个数作为序列结尾的贡献得到合法序列数。但是区间变化的话就只能重新遍历一遍重做了。复杂度\(O(n^2\log n)\)
序列尾作贡献的话就只能固定序列尾,如果固定一个序列尾不能确定序列是否在所需要的区间里面,所以我们还得固定序列头。
假设序列尾是\(z\),那么序列头\(x\)形成的二元组\(<x,z>\)贡献为(必须满足\(x|z\)否则没有贡献)
特别的\(cnt_{<x,x>}=1\)
那么接下来,问题就变成了,给定一个区间,求所有被包含的二元组的贡献和。
正常在线一个一个做的复杂度合格做法我没有想到,所以我就先把所有询问离线下载,按照\(r\)升序,记序列头尾形成的二元组\(<x,z>\),满足\(z<=r\)的序列尾满足要求,有可能产生贡献,把贡献记录在序列头的位置上。因为\(r\)升序,所以只会添加记录而不会删除记录,然后对于每一次询问,\(l<=x<=r\)的序列头合法,把这部分贡献加起来。区间查询加单点修改,树状数组和线段树随便选一个即可。
复杂度分析
记录数也就是因子数之和,\(O(n\log n)\),再修改\(O(\log n)\),再加\(O(\log n)\)查询,\(n,q\)同级
时间复杂度\(O(n\log^2 n)\)
因为要记录每个数的因子,空间复杂度\(O(n\log n)\),序列贡献可以询问来了当场加进去,可以不存。
点我查看代码
#include<bits/stdc++.h>
#define ll long long
//#define int long long
#define mid ((l+r)>>1)
#define lson u<<1,l,mid
#define rson u<<1|1,mid+1,r
#define inf 0x3f3f3f3f
typedef int LL;
const signed maxn=(signed)1e6+5;
inline LL Read(){
char ch=getchar();bool f=0;LL x=0;
for(;!isdigit(ch);ch=getchar())if(ch=='-')f=1;
for(;isdigit(ch);ch=getchar())x=(x<<1)+(x<<3)+(ch^48);
if(f==1)x=-x;return x;
}
int n,q;
std::vector<int> d[maxn];
ll cnt[maxn];
int pos[maxn],ar[maxn];
struct Q{
int l,r,id;
}qr[maxn];
ll ans[maxn];
bool operator <(Q a,Q b){
if(a.r!=b.r) return a.r<b.r;
return a.l<b.l;
}
ll tr[maxn];
void Modify(int x,ll w){
while(x<=n){
tr[x]+=w;
x+=x&-x;
}
}
ll Query(int x){
ll ans=0;
while(x){
ans+=tr[x];
x-=x&-x;
}
return ans;
}
void solve(int posi){
int x=ar[posi];
cnt[x]=1;
Modify(posi,1);
for(auto j:d[x]){
cnt[j]=(pos[j]<posi);
for(auto z:d[x/j]){
if(pos[z*j]>pos[j]) cnt[j]+=cnt[z*j];
}
//printf("r=%d,ar[r]=%d,l=%d,ar[l]=%d,c=%lld\n",posi,x,pos[j],j,cnt[j]);
Modify(pos[j],cnt[j]);
}
}
signed main()
{
int mx=1e6;
for(int i=(mx>>1);i;--i)
for(int j=2;i*j<=mx;++j) d[i*j].push_back(i);
LL T=Read();
while (T--){
n=Read(),q=Read();
for(int i=1;i<=n;++i) ar[i]=Read();
for(int i=1;i<=n;++i) pos[ar[i]]=i;
for(int i=1;i<=n;++i) tr[i]=0;
for(int i=1;i<=q;++i){
int l(Read()),r(Read());
qr[i]=(Q){l,r,i};
}
std::sort(qr+1,qr+1+q);
int cnt=0;
for(int i=1;i<=q;++i){
int l=qr[i].l,r=qr[i].r;
while(cnt<r) solve(++cnt);
ans[qr[i].id]=Query(r)-Query(l-1);
}
for(int i=1;i<=q;++i) printf("%lld ",ans[i]);putchar('\n');
}
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现