桐柏邀请赛 S10 题解
Enchanted Love
记 \(S=a_1+a_2+\cdots+a_n\),那么:
- 若 \(S\) 为偶数,则答案为 \(\frac{S}{2}\)。
- 否则,我们找到 \(a\) 中最小的奇数(显然此时 \(a\) 中必然有至少一个奇数),设为 \(a_x\),则答案为 \(\frac{S-a_x}{2}\)。
记得开 \(\texttt{long long}\)。
#include<bits/stdc++.h>
using namespace std;
int n;
long long ans,tmp,minn=1e10;
int main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>tmp;
ans+=tmp;
if(tmp%2)minn=min(minn,tmp);//如果是个奇数那么保存最小奇数
}
if(ans%2==0){
cout<<ans/2;
}else{
cout<<(ans-minn)/2;
}
return 0;
}
Robotic Girl 2
算法一
直接暴力模拟计算答案,时间复杂度 \(O(n+L)\)。期望得分 \(50\)。
算法二
我会倍增!
设计状态:
- \(F(i,j)\) 表示一开始有 \(i\) 块巧克力,进行 \(2^j\) 次操作之后增加的巧克力数量(\(\bmod p\) 后的值)。
- \(G(i,j)\) 表示一开始有 \(i\) 块巧克力,进行 \(2^j\) 操作之后的巧克力数量 \(\bmod n\) 的值。
那么有
初值为 \(F(i,0)=C_i,G(i,0)=(C_i+i)\bmod n\)。
最后只需要 \(O(\log L)\) 统计一下答案就行了。时空复杂度均为 \(O(n\log L)\),期望得分 \(80\)。
注意到空间复杂度可以优化到 \(O(n)\),这样一来如果常数小一点就可以 AC 了。下面给出空间复杂度 \(O(n\log L)\) 的代码:
#include<bits/stdc++.h>
#define int long long
using namespace std;
inline int read(){
int x=0,f=1;char c=getchar();
for(;(c<'0'||c>'9');c=getchar()){if(c=='-')f=-1;}
for(;(c>='0'&&c<='9');c=getchar())x=x*10+(c&15);
return x*f;
}
const int MN=2e5+5;
const int LOG=34;
const int MOD=1e9;
int f[MN][LOG],g[MN][LOG],n,L,x,P,C[MN],X,Y,Z;
signed main(void){
n=read(),L=read(),x=read(),P=read();int now=x%n,ans=x;
for(int i=0;i<n;i++)C[i]=read();
for(int i=0;i<n;i++)f[i][0]=C[i]%P,g[i][0]=(i+C[i])%n;
for(int j=1;j<LOG;j++){
for(int i=0;i<n;i++){
f[i][j]=f[i][j-1]+f[g[i][j-1]][j-1],f[i][j]%=P;
g[i][j]=g[g[i][j-1]][j-1];
}
}
for(int i=0;i<LOG;i++){
if(L&1)ans+=f[now][i],now=g[now][i],ans%=P;
L>>=1;
}
cout<<ans<<endl;
return 0;
}
算法三
我们考虑 \(n\) 个点 \(0,1,\cdots,n-1\),对每个点 \(i\) 我们连边 \(i\to (i+C_i)\bmod n\),边权为 \(C_i\)。
那么每个点出度为 \(1\),我们要求的就是从 \(x\bmod n\) 开始走,走 \(L\) 步之后的边权之和。
不妨设这张图是弱连通的(否则可以考虑 \(x\bmod n\) 所在的连通块),那么,从任意一个点 \(u\) 开始走必然能在 \(n\) 步以内走到一个环。
证明:
考虑从 \(u\) 开始的一条路径 \(p_0(=u),p_1,\cdots,p_n\),一共经过了 \(n+1\) 个点,而图中只有 \(n\) 个点,因此由抽屉原理可知必然存在 \(p_i=p_j\)。那么 \(p_i\to p_{i+1}\to \cdots\to p_j\) 就构成了一个环。
显然,一旦走到了一个环上,后面都会在这个环上一直走下去。因此只需要找到这个环就可以直接算了。
时间复杂度 \(O(n)\)。
#include<bits/stdc++.h>
#define int long long
using namespace std;
inline int read(){
int x=0,f=1;char c=getchar();
for(;(c<'0'||c>'9');c=getchar()){if(c=='-')f=-1;}
for(;(c>='0'&&c<='9');c=getchar())x=x*10+(c&15);
return x*f;
}
const int MN=1e6+5;
const int MOD=1e9;
bool vis[MN];
int C[MN],p[MN],n,L,st,mod;
signed main(void){
n=read(),L=read(),x=read(),P=read();int now=x%n,ans=x;
for(int i=0;i<n;i++)C[i]=read();
for(int i=0;i<n;i++)p[i]=(i+C[i])%n;
int ans=st,u=st%n;
while(L-->0){
vis[u]=1,ans+=C[u],ans%=mod;
int v=p[u];
if(!vis[v]){u=v;continue;}
int sum=C[u],cnt=1;
while(v!=u)sum+=C[v],sum%=mod,v=p[v],cnt++;
ans+=((int)(L/cnt))%mod*sum%mod,ans%=mod;
L%=cnt,u=p[u];break;
}
while(L-->0)ans+=C[u],ans%=mod,u=p[u];
cout<<ans<<endl;
return 0;
}
String Theocracy
欢迎大家去听 mili 的歌qwq
下面的讨论默认 \(n,m\) 同阶。
算法一
我会暴力!直接模拟,复杂度 \(O(nm\log n)\sim O(nm)\),期望得分 \(20\)。
算法二
如果 \(a\) 是排列,那么去重就没有用了,所以直接随便写一个主席树求区间第 \(k\) 大的板子就行了。
时间复杂度 \(O(n\log n)\),期望得分 \(20\)。与算法一结合可以得到 \(40\)。
算法三
如果 \(1\le a_i\le 15\),那么去重后至多只有 \(15\) 个数。
考虑维护前缀和 \(S_x(i)\) 表示前 \(i\) 个数中 \(x\) 的出现次数,那么就能 \(O(1)\) 判断区间中是否存在某个数。
这样求第 \(k\) 大就只需要从大到小枚举答案就行了。时间复杂度 \(O(nV),V=15\) 为值域。
期望得分 \(15\),与上面的算法结合可以得到 \(55\) 分。这 \(55\) 分应该是比较好拿的(
算法四
我会莫队!
考虑离线下来之后,维护 \(c_x\) 表示当前区间内 \(x\) 的出现次数。
再维护一个支持查询第 \(k\) 大的数据结构,每次插入删除的时候特判一下就可以了。
如果使用线段树/平衡树之类的 \(\log\) 数据结构,时间复杂度 \(O(n\sqrt{n}\log n)\)。
然而实际上插入删除是很难卡满 \(O(n\sqrt{n})\) 次的,所以如果常数够好甚至可以拿到 \(75\sim 85\) 的高分。
甚至一开始这个算法在卡常之后跑得比 std 还快,云浅卡了两天才卡掉
#include<bits/stdc++.h>
#define int long long
using namespace std;
inline int read(){
int x=0,f=1;char c=getchar();
for(;(c<'0'||c>'9');c=getchar()){if(c=='-')f=-1;}
for(;(c>='0'&&c<='9');c=getchar())x=x*10+(c&15);
return x*f;
}
const int MN=250000+5;
struct Node{
int l,r,k,id;
Node(int L,int R,int I):l(L),r(R),id(I){}
Node(){}
};
Node q[MN];
int bl[MN],a[MN],n,m,sq,ans[MN];
int cnt[MN];
struct BIT{
int c[MN];
inline int lowbit(int x){return x&(-x);}
inline void add(int x,int k){for(int i=x;i<=n;i+=lowbit(i))c[i]+=k;}
inline int query(int k){
int p=0,now=0;
for(int i=18;i>=0;i--){
int q=p+(1<<i);
if(q<n&&now+c[q]<k)p=q,now+=c[q];
}
return p+1;
}
}T;
int now=0,all=0;
void ins(int x){
if(cnt[x]==0)T.add(x,1),now++;
cnt[x]++;all++;
}
void del(int x){
cnt[x]--;all++;
if(cnt[x]==0)T.add(x,-1),now--;
}
signed main(void){
n=read(),m=read();sq=(int)(n/(int)(sqrt(m)));
for(int i=1;i<=n;i++)a[i]=read();
for(int i=1;i<=m;i++)q[i].l=read(),q[i].r=read(),q[i].k=read(),q[i].id=i;
for(int i=1;i<=n;i++)bl[i]=(i-1)/sq+1;
sort(q+1,q+m+1,[](const Node &x,const Node &y){
if(bl[x.l]!=bl[y.l])return bl[x.l]<bl[y.l];
if(bl[x.l]&1)return x.r<y.r;
else return x.r>y.r;
});
int l=1,r=0;
for(int i=1;i<=m;i++){
int ql=q[i].l,qr=q[i].r;
while(l>ql)ins(a[--l]);
while(r<qr)ins(a[++r]);
while(l<ql)del(a[l++]);
while(r>qr)del(a[r--]);
ans[q[i].id]=T.query(now-q[i].k+1);
}
for(int i=1;i<=m;i++)cout<<ans[i]<<endl;
return 0;
}
算法五
去重就相当于只计算每个元素第一次出现时的贡献。
设 \(p_i\) 为 \(a_i\) 上一次出现的位置(如果不存在则为 \(0\)),那么相当于只对区间内 \(p_i<l\) 的这些 \(a_i\) 计算第 \(k\) 大。
那么整体二分之后就是一个简单的二维数点了,离线下来用树状数组做即可。
时间复杂度 \(O(n\log ^2n)\)。期望得分 \(100\)。
在没有刻意卡常的情况下,对于 \(n,m=2.5\times 10^5\) 的数据,该算法最慢大概在 \(1.5\text{s}\) 左右,平均约为 \(1.3\text{s}\)。
#include<bits/stdc++.h>
#define int long long
using namespace std;
inline int read(){
int x=0,f=1;char c=getchar();
for(;(c<'0'||c>'9');c=getchar()){if(c=='-')f=-1;}
for(;(c>='0'&&c<='9');c=getchar())x=x*10+(c&15);
return x*f;
}
const int MN=250005;
int n,m,pre[MN];
struct qry{int op,l,r,k,id;}q[MN<<1],lq[MN<<1],rq[MN<<1];
struct BIT{
int c[MN];
void clear(){memset(c,0,sizeof(c));}
int lowbit(int x){return x&(-x);}
void add(int x,int k){for(int i=x;i<=n+1;i+=lowbit(i))c[i]+=k;}
int sum(int x){int r=0;for(int i=x;i;i-=lowbit(i))r+=c[i];return r;}
}F,T;
struct Node{
int lim,f,id;
Node(int L,int F,int I):lim(L),f(F),id(I){}
Node(){}
};
vector<Node>vec[MN];
vector<int>S,rec;
int ans[MN],res[MN<<1];
void solve(int L,int R,int st,int ed){
if(st>ed)return ;
if(L==R){
for(int i=st;i<=ed;i++)if(q[i].op==2)ans[q[i].id]=L;
return ;
}
int x=(L+R)>>1,lc=0,rc=0;
for(int i=st;i<=ed;i++){
if(q[i].op==1){
if(q[i].r>x)vec[q[i].l].push_back(Node(pre[q[i].l],0,233)),rq[++rc]=q[i],S.push_back(q[i].l);
else lq[++lc]=q[i];
}
if(q[i].op==2){
vec[q[i].r].push_back(Node(q[i].l-1,1,i)),S.push_back(q[i].r);rec.push_back(i);
if(q[i].l>1)vec[q[i].l-1].push_back(Node(q[i].l-1,-1,i)),S.push_back(q[i].l-1);
}
}
sort(S.begin(),S.end());int len=unique(S.begin(),S.end())-S.begin();S.resize(len);
for(int p:S){
for(auto t:vec[p])if(t.f==0)T.add(t.lim+1,1);
for(auto t:vec[p]){
if(t.f==0)continue;
res[t.id]+=t.f*T.sum(t.lim+1);
}
}
for(int i:rec){
if(res[i]>=q[i].k)rq[++rc]=q[i];
else q[i].k-=res[i],lq[++lc]=q[i];
}
for(int i=st;i<=st+lc-1;i++)q[i]=lq[i-st+1];
for(int i=st+lc;i<=ed;i++)q[i]=rq[i-st-lc+1];
for(int p:S)for(auto t:vec[p])if(t.f==0)T.add(t.lim+1,-1);
for(int p:S)vec[p].clear();
for(int i:rec)res[i]=0;S.clear(),rec.clear();
solve(L,x,st,st+lc-1),solve(x+1,R,st+lc,ed);
}
int mp[MN],mx;
signed main(void){
n=read(),m=read();
for(int i=1;i<=n;i++){
int v=read();mx=max(mx,v);
pre[i]=mp[v],mp[v]=i,q[i].op=1,q[i].l=i,q[i].r=v;
}
for(int i=1;i<=m;i++){
int l=read(),r=read(),v=read();
q[i+n].op=2,q[i+n].l=l,q[i+n].r=r,q[i+n].k=v,q[i+n].id=i;
}
solve(1,mx,1,n+m);
for(int i=1;i<=m;i++)cout<<ans[i]<<endl;
return 0;
}
算法六
但是莫队其实还能优化!
由于 \(n,m\) 同阶,考虑根号平衡。
在值域上分块,对每个块维护一下块内元素和,那么插入删除就能做到 \(O(1)\),查询就是 \(O(\sqrt{n})\)。
时间复杂度 \(O(n\sqrt{n})\),实际表现快到飞起。。把 std 吊着打.jpg
效率:最慢点 \(962\text{ms}\),在 \(n,m=2.5\times 10^5\) 时平均 \(800\text{ms}\)。
#include<bits/stdc++.h>
#define int long long
using namespace std;
inline int read(){
int x=0,f=1;char c=getchar();
for(;(c<'0'||c>'9');c=getchar()){if(c=='-')f=-1;}
for(;(c>='0'&&c<='9');c=getchar())x=x*10+(c&15);
return x*f;
}
const int MN=2e5+5e4+5;
const int MC=(int)(sqrt(MN))+10;
int a[MN],n,m;
struct Node{
int l,r,k,id;
Node(int L,int R,int K,int I):l(L),r(R),k(K),id(I){}
Node(){}
}q[MN];
int sum[MC],bl[MN],len,tot,ans[MN],cnt[MN],val[MN],L[MC],R[MC],now;
void add(int x){
if(cnt[x]==0)val[x]++,now++,sum[bl[x]]++;
cnt[x]++;
}
void del(int x){
cnt[x]--;
if(cnt[x]==0)val[x]--,now--,sum[bl[x]]--;
}
int kth(int k){
int p=1;
while(p<=tot){
if(k<=sum[p])break;
k-=sum[p++];
}
for(int i=L[p];i<=R[p];i++){
k-=val[i];
if(k<=0)return i;
}
return 404;//>_<
}
signed main(void){
n=read(),m=read();len=(int)(n/sqrt(m));
for(int i=1;i<=n;i++)a[i]=read();
for(int i=1;i<=m;i++)q[i].l=read(),q[i].r=read(),q[i].k=read(),q[i].id=i;
for(int i=1;i<=n;i++)bl[i]=(i-1)/len+1;tot=bl[n];memset(L,63,sizeof(L));
for(int i=1;i<=n;i++)L[bl[i]]=min(L[bl[i]],i),R[bl[i]]=max(R[bl[i]],i);
sort(q+1,q+m+1,[](const Node &x,const Node &y){
if(bl[x.l]!=bl[y.l])return bl[x.l]<bl[y.l];
if(bl[x.l]&1)return x.r<y.r;
else return x.r>y.r;
});
int l=1,r=0;
for(int i=1;i<=m;i++){
int ql=q[i].l,qr=q[i].r;
while(l>ql)add(a[--l]);
while(r<qr)add(a[++r]);
while(l<ql)del(a[l++]);
while(r>qr)del(a[r--]);
q[i].k=now-q[i].k+1;
ans[q[i].id]=kth(q[i].k);
}
for(int i=1;i<=m;i++)cout<<ans[i]<<endl;
return 0;
}
Pink Sunrise
『粉色朝霞』
算法一
暴力枚举所有方案即可。期望得分 \(15\)。
算法二
考虑加个剪枝:如果当前位置的值已经可以确定,那么就不进行枚举。这样就能拿到 \(30\) 了。
算法三
可以发现,特殊性质 A 的意思就是:所有的 \(h_i\) 均相等。
当 \(h_i\) 全相等时,问题转化为对对一个 \(m(=h_i)\) 行 \(n\) 列的矩形如何计算答案。
我们发现,对于矩形的第 \(i\) 行:
- 如果存在相邻两个方格的颜色相同,那么下面一行唯一的方案就是把当前行取反。
- 否则,这一行必然是黑白交错,那么下面一行可以把这一行复制下来或者取反。
考虑一个 DP:设 \(f(i,0)\) 表示前 \(i\) 行,最后一行是黑白交错时的方案数,\(f(i,1)\) 表示总的方案数,有
- \(f(i,0)=2\times f(i-1,0)\),因为上一行必然也是黑白交错,且最后一行有两种选择。
- \(f(i,1)=f(i,0)+2^n-2\),因为如果最后一行并非黑白交错,那么只有一种方案。而并非黑白交错的方案数为 \(2^n-2\) 种(即 \(2^n\) 减去 \(\texttt{0101010...}\) 与 \(\texttt{1010101...}\) 两种方案)。
期望得分 \(20\),与上面的算法结合可以得到 \(35\) 分。
算法四
特殊性质 C 的意思就是所有 \(h_i\) 互不相同。
对一个区间 \([l,r]\),我们设计状态:
- \(f(l,r,0)\) 表示只考虑区间 \([l,r]\) ,且最下面一行为黑白交错时的答案。
- \(f(l,r,1)\) 表示只考虑区间 \([l,r]\) 时的答案。
- \(g(l,r)=f(l,r,0)+f(l,r,1)\)。
设该区间内最小值为 \(h_x=H\),那么有
其含义为:
- 若第 \(H\) 行为黑白交错,那么每一层都必须是黑白交错,而最下面的 \(H\) 行有 \(2^H\) 种选择。
- 如果第 \(H\) 行并非黑白交错,那么第 \(x\) 个数可以随便填,对于其余的位置,一种方案是直接将上一行取反,而如果上一行是黑白交错那么还能多出一种方案,因此是 \(f(l,r,0)+f(l,r,1)=g(l,r)\)。然后再考虑算入下面 \(H\) 行为黑白交错的情况,这部分已经有 \(2\) 种算过了,因此方案数为 \((2^H-2)f(l,x-1,0)\times f(x+1,r,0)\)。
看上去复杂度是 \(O(n^2)\),但你仔细分析一下可以发现只会访问到 \(O(n)\) 个状态,写个记忆化搜索就可以通过 \(n=4\times 10^5\) 的测试点了。注意找最小值不能直接暴力找,否则有可能退化成 \(O(n^2)\)。
算法五
仔细思考一下可以发现其实算法三就是建出了笛卡尔树,然后在笛卡尔树上做 DP。
那么当 \(h_i\) 可以相同的时候,我们定义广义的「笛卡尔树」为:
对于当前节点 \(u\) 所代表的区间 \([l,r]\),设区间内有 \(k\) 个最小值,其位置分别为 \(p_1,p_2,\cdots,p_k\),那么我们认为 \(u\) 有 \(k+1\) 个儿子,第 \(i\) 个儿子代表的区间为 \([p_{i-1},p_i]\)。其中 \(p_0=l,p_{k+1}=r\)。
那么设 \(f(u,0)\) 表示 \(u\) 子树代表的区域内为黑白交错的方案数,\(f(u,1)\) 表示 \(u\) 子树代表的区域内随便填的方案数,\(u\) 所代表的区间内最小值为 \(H\),我们有:
相信只要理解了算法三就不难理解这个转移方程。
现在考虑如果建出这个笛卡尔树。
如果直接暴力每次找最小值,那么复杂度可能会退化成 \(O(n^2)\),不过当 \(K=2\) 时可以将序列看做随机序列,此时这个算法的表现十分优秀,可以通过符合特殊性质 B 的测试点。
如果使用某些分治数据结构配合建树,那么复杂度是 \(O(n\log n)\),常数小或许可以通过。
但其实本题存在线性的做法!考虑仿照普通的二叉笛卡尔树的单调栈建树过程,不难发现此时只需要判断一下是否分裂出一棵新子树就可以做到类似地 \(O(n)\) 建树。
然后注意到快速幂也需要带个 \(\log\),我们只需要使用 \(O(\sqrt{V})-O(1)\) 的光速幂即可。总的时间复杂度为 \(O(n+\sqrt{V})\)。
算法六
实际上经过分析可以发现这个转移方程是兼容高度为 \(0\) 的矩形的。
因此并没有必要讨论分裂不分裂子树之类的情况。。按普通笛卡尔树来做也是可以的。。
具体来说我们建出二叉笛卡尔树,然后类似地做个 DP 就行了。时间复杂度仍然是 \(O(n+\sqrt{V})\)。
#include<bits/stdc++.h>
#define ll long long
using namespace std;
inline ll read(){
ll x=0,f=1;char c=getchar();
for(;(c<'0'||c>'9');c=getchar()){if(c=='-')f=-1;}
for(;(c>='0'&&c<='9');c=getchar())x=x*10+(c&15);
return x*f;
}
const int N=5e6;
const int C=1048576;
const int mod=998244353;
int p1[C+5],p2[C+5],n;
void init(){
p1[0]=1;for(int i=1;i<=C;i++)p1[i]=(p1[i-1]+p1[i-1])%mod;
p2[0]=1;for(int i=1;i<=C;i++)p2[i]=(1ll*p2[i-1])*(1ll*p1[C])%mod;
}
int getpow(ll x){
return (1ll*p1[x&(C-1)])*(1ll*p2[x>>20])%mod;
}
const ll INF=1e12+1;
ll h[N+5];
int stk[N+5],top;
int f[N+5][2],tot=1,ls[N+5],rs[N+5];
void build(){
for(int i=1;i<=n;i++){
int cur=top;
while(cur>0&&h[stk[cur]]>h[i])cur--;
if(cur)rs[stk[cur]]=i;
if(cur!=top)ls[i]=stk[cur+1];
stk[++cur]=i,top=cur;
}
}
void DP(int u,ll d){
f[u][0]=f[u][1]=1;
if(!u)return ;
ll mn=h[u],H=mn-d;
DP(ls[u],mn),DP(rs[u],mn);
if(ls[u])f[u][0]=1ll*f[u][0]*1ll*f[ls[u]][0]%mod;
if(rs[u])f[u][0]=1ll*f[u][0]*1ll*f[rs[u]][0]%mod;
if(ls[u])f[u][1]=1ll*f[u][1]*1ll*(f[ls[u]][0]+f[ls[u]][1])%mod;
if(rs[u])f[u][1]=1ll*f[u][1]*1ll*(f[rs[u]][0]+f[rs[u]][1])%mod;
f[u][1]=f[u][1]*2%mod;
f[u][1]=(f[u][1]+1ll*(getpow(H)-2+mod)%mod*1ll*f[u][0]%mod)%mod;
f[u][0]=1ll*f[u][0]*1ll*getpow(H)%mod;
}
int K;
ll X,Y,Z,P;
ll mul(ll x,ll y,ll p){
__int128 xx=x,yy=y,pp=p;
return (ll)(xx*yy%pp);
}
signed main(void){
n=read(),K=read(),X=read(),Y=read(),Z=read(),P=read();
for(int i=1;i<=K;i++)h[i]=read();
for(int i=K+1;i<=n;i++)h[i]=(mul(h[i-2],mul(h[i-2],X,P),P)+mul(h[i-1],Y,P)+Z)%P;
init();build();
int pos=0;ll mn=INF;
for(int i=1;i<=n;i++)if(mn>h[i])mn=h[i],pos=i;
DP(pos,0);
cout<<f[pos][1]<<endl;
return 0;
}