比赛情况:
- A 00:14 +
- B 01:02 +
- C 00:42 +
- D 00:36 +
- E 00:49 HACKED
F,G 赛时未提交
A
题意简述
定义两个字符串\(a,b\)是相似的,当且仅当这两个字符串长度相同,且存在\(i\in [1,|a|]\),使得\(a_i=b_i\)。
给定一个长度为\(2n-1\)的01字符串\(S\),你需要构造出一个长度为\(n\)的字符串,使得它与\(S\)的每一个长度为\(n\)的子串都是相似的。
题目保证有解。
多测,\(1\le n\le 50\)。
算法考察
构造
算法分析
因为题目保证有解,因此我们可以如下构造。
构造\(a_1,a_2,\cdots,a_n\)=\(s_1,s_2,\cdots,s_n\)。
然后向后枚举\(s\)的每一个长度为\(n\)的子串,如果对应位置\(a_i\neq s_j\),则\(a_i\)任意。
因为保证有解,所以不会出现最后所有的\(a\)都任意的情况。
代码实现
#include<bits/stdc++.h>
using namespace std;
#define maxn 1000005
#define maxm 2000005
#define inf 0x3f3f3f3f
#define LL long long
#define mod 1000000007
#define local
template <typename Tp> void read(Tp &x){
int fh=1;char c=getchar();x=0;
while(c>'9'||c<'0'){if(c=='-'){fh=-1;}c=getchar();}
while(c>='0'&&c<='9'){x=(x<<1)+(x<<3)+(c&15);c=getchar();}x*=fh;
}
int n,m;
int s[105],a[105];
signed main(){
int CasT;
read(CasT);
while(CasT--){
read(n);
for(int i=1;i<2*n;i++)scanf("%1d",&s[i]);
for(int i=1;i<=n;i++)a[i]=s[i];//初始化
for(int i=2;i<=n;i++){
for(int j=i;j<=n+i-1;j++){
if(a[j-i+1]!=s[j])a[j-i+1]=-1;//任意标记
}
}
for(int i=1;i<=n;i++){
if(a[i]==-1)a[i]=0;//任意取
}
for(int i=1;i<=n;i++)printf("%d",a[i]);
puts("");
}
return 0;
}
B
题意简述
有2个人去买武器,两个人的最大承重分别为\(p,f\),武器店有剑和战斧,有\(cnts\)把剑,每一把剑的重量为\(s\),有\(cntw\)把战斧,每一把战斧的重量为\(w\)。两个人购买的武器总重量不能超过自生的最大承重,两个人购买的剑的总数不能超过\(cnts\),购买战斧的总数不能超过\(cntw\),求在满足条件下两人能买到的武器总数的 最大值。
多测,\(1\le p,f,s,w\le 10^9\),\(1\le cnts,cntw\le 2\times 10^5\),\(1\le \sum cnts,\sum cntw\le 2\times 10^5\)。
算法考察
贪心
算法分析
本问题与传统的多重背包问题很类似,但是本问题有\(3\)个特殊性。
- 背包容量很大
- 有\(2\)个背包
- 只有\(2\)个物品,且价值为1
因为只有\(2\)个物品,我们可以直接贪心,尽可能选用重量小的物品,证明显然(可以考虑拿出一个重量大的物品一定可以再装下至少一个重量小的物品,一定不会使答案变差)。
以下假设\(s<w\)(如果输入数据\(s>w\)直接交换调整)。
因为这一题有\(2\)个背包,所以我们可以枚举第一个背包选了\(i\)把剑,我们不难计算出第二个背包选了多少剑,剩余空间我们用战斧填充。
代码实现
#include<bits/stdc++.h>
using namespace std;
#define maxn 1000005
#define maxm 2000005
#define inf 0x3f3f3f3f
#define int long long//其实本题不必要开long long
#define mod 1000000007
#define local
template <typename Tp> void read(Tp &x){
int fh=1;char c=getchar();x=0;
while(c>'9'||c<'0'){if(c=='-'){fh=-1;}c=getchar();}
while(c>='0'&&c<='9'){x=(x<<1)+(x<<3)+(c&15);c=getchar();}x*=fh;
}
int p,f,cnts,cntw,s,w;
int ans;
int calc(int x){
if(x*s>p)return 0;//买x个s已经负担不起了
int ret=x;p-=x*s;
int yy=min(cnts-x,f/s);//第二个人购买的s
ret+=yy;f-=yy*s;
ret+=min(cntw,p/w+f/w);//两个人购买的w
p+=x*s;f+=yy*s;//注意修改撤销
return ret;
}
signed main(){
int CasT;
read(CasT);
while(CasT--){
read(p);read(f);read(cnts);read(cntw);read(s);read(w);
if(s>w)swap(s,w),swap(cnts,cntw);//默认s<w
ans=0;
for(int i=0;i<=cnts;i++){
ans=max(ans,calc(i));
}
printf("%lld\n",ans);
}
return 0;
}
C
题意简述
已知一个长度为\(n\)的01字符串\(s\)由长度为\(n\)的01字符串和一个正整数\(x\)生成。字符串下标从\(1\)开始。\(s_i\)生成规则如下:
- 如果\(i>x\)且\(w_{i-x}=1\),则\(s_i=1\);
- 如果\(i+x\le n\)且\(w_{i+x}=1\),则\(s_i=1\);
- 否则\(s_i=0\)。
输入\(s\)和\(x\),输出一种可行的\(w\),如果不存在这样的\(w\),输出\(-1\)。
多测,\(2\le |s|\le 10^5\),\(1\le x\le |s|-1\),\(\sum|s|\le 10^5\)。
算法考察
模拟
算法分析
我们先改写一下条件:
- \(s_i=1\),则\(w_{i-x}=1(i>x)\)或\(w_{i+x}=1(i+x\le n)\);
- \(s_i=0\),则\(w_{i-x}=0(i>x)\)且\(w_{i+x}=0(i+x\le n)\);
我们发现\(s_i=0\)的限制条件比较确定,因此我们可以确定\(w\)的哪些位置一定是\(0\)。
此时如果要使第一个条件成立,我们只需要检验每一个\(s_i=1\)的位置,如果对应的\(w\)的位置都是确定的\(0\),那一定无解,否则我们让剩余不确定的位置全部填\(1\),可以证明这样的构造一定合法。
代码实现
#include<bits/stdc++.h>
using namespace std;
#define maxn 100005
#define maxm 200005
#define inf 0x3f3f3f3f
#define int long long
#define mod 1000000007
#define local
template <typename Tp> void read(Tp &x){
int fh=1;char c=getchar();x=0;
while(c>'9'||c<'0'){if(c=='-'){fh=-1;}c=getchar();}
while(c>='0'&&c<='9'){x=(x<<1)+(x<<3)+(c&15);c=getchar();}x*=fh;
}
int n,m;
int pos[maxn];
char s[maxn];
signed main(){
int CasT;
read(CasT);
int X;
while(CasT--){
memset(pos,0,sizeof(pos));//此处实际可以更加精细处理,但本身已经能过
scanf("%s",s+1);
read(X);
n=strlen(s+1);
for(int i=1;i<=n;i++){//处理0
if(s[i]=='0'){
if(i-X>0)pos[i-X]=1;
if(i+X<=n)pos[i+X]=1;
}
}
int ffl=1;
for(int i=1;i<=n;i++){
if(s[i]=='1'){//处理1
int fla=0,flb=0;
if(i-X>0){
if(pos[i-X]==1)fla=1;
}
else fla=1;
if(i+X<=n){
if(pos[i+X]==1)flb=1;
}
else flb=1;
if(fla&&flb){//无法满足1,一定无解
puts("-1");
ffl=0;
break;
}
}
}
if(ffl){
for(int i=1;i<=n;i++){
if(pos[i])putchar('0');
else putchar('1');
}
puts("");
}
}
return 0;
}
D
题意简述
给定一个长度为\(n\)的数列\(\{a_n\}\),计算满足如下条件的四元组\((i,j,k,l)\)个数:
- \(1\le i<j<k<l\le n\);
- \(a_i=a_k\)且\(a_j=a_l\)。
多测,\(4\le n\le 3000\),\(1\le a_i\le n\),\(\sum n\le 3000\)。
算法考察
计数原理,前缀和,二分,尺取法
算法分析
不难想到按照权值计数。
我们预处理出每一个权值出现的位置,这里可以使用\(vector\)实现。
为了减少讨论,我们可以先处理出\(a_i=a_j=a_k=a_l\)的情况,枚举每一个权值,直接组合数计算。
下面假设我们枚举的权值为\(x,y\),且\(a_i=a_k=y\),\(a_j=a_l=x\)。
下面我们枚举\(k,l\),问题转化为给定\(x,y\),多次询问\(k\),求有多少个二元组\((i,j)\),满足:
- \(1\le i<j<k\)
- \(a_i=y\)且\(a_j=x\)
我们可以枚举\(j\),在\(y\)的位置数组中二分计算出小于\(j\)有多少个位置,记为\(b_j\)。
然后再对\(b\)求前缀和,完成预处理。
对于每一个询问,我们可以在\(x\)的位置数组中二分,找到对应的\(b\),累加答案即可。
如上实现的复杂度为\(O(n^2\log n)\),类比图论中有关边数的证明,可以证明对\(k,l\)的枚举是均摊\(O(1)\)的。
上述算法已经可以通过本题。如果扩大数据范围,可以利用枚举\(j\)的单调和枚举\(k\)的单调,用指针的移动代替二分。
同时我们发现,其实我们没有必要枚举\(l\),我们只需要枚举\(k\),不难发现对于一个确定的\(k\),每一个\(l\)答案都是一样的,因此可以直接乘上\(l\)的个数。
如上优化后的复杂度为\(O(n^2)\),但考场上不建议使用上述优化,耗时耗力,还容易打挂。
代码实现
#include<bits/stdc++.h>
using namespace std;
#define maxn 3005
#define maxm 2000005
#define inf 0x3f3f3f3f
#define int long long//注意long long
#define mod 1000000007
#define local
template <typename Tp> void read(Tp &x){
int fh=1;char c=getchar();x=0;
while(c>'9'||c<'0'){if(c=='-'){fh=-1;}c=getchar();}
while(c>='0'&&c<='9'){x=(x<<1)+(x<<3)+(c&15);c=getchar();}x*=fh;
}
int n,m;
vector<int>p[maxn];//位置数组
vector<int>h[maxn];//前缀和数组
int sum1,sum2;
int C4(int x){//求C(x,4)
if(x<4)return 0;
else return x*(x-1)*(x-2)*(x-3)/24;
}
int calc(int x,int y){
h[x].clear();
for(unsigned i=0;i<p[x].size();i++){
int tt=lower_bound(p[y].begin(),p[y].end(),p[x][i])-p[y].begin();
h[x].push_back(tt);
}
for(unsigned i=1;i<p[x].size();i++){
h[x][i]+=h[x][i-1];
}//二分并求前缀和
int ret=0;
for(unsigned i=0;i<p[x].size();i++){
for(unsigned j=0;j<p[y].size()&&p[y][j]<p[x][i];j++){
int k=lower_bound(p[x].begin(),p[x].end(),p[y][j])-p[x].begin()-1;//二分对应前缀和位置
if(k!=-1)ret+=h[x][k];
}
}
return ret;
}
signed main(){
int CasT;
read(CasT);
while(CasT--){
read(n);
for(int i=1,x;i<=n;i++)read(x),p[x].push_back(i);
sum1=sum2=0;
for(int i=1;i<=n;i++){
sum1+=C4(p[i].size());
}//处理四个全相等
for(int i=1;i<=n;i++)if(p[i].size()<2)p[i].clear();
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
if(i==j)continue;
if(p[i].size()<2||p[j].size()<2)continue;//小剪枝,也是为了避免讨论
sum2+=calc(i,j);
}
}
printf("%lld\n",sum1+sum2);
for(int i=1;i<=n;i++)p[i].clear();//注意多测清空
}
return 0;
}
E
题意简述
给定一个长度为\(n\)数列\(\{a_n\}\),你可以进行如下操作:
- 任意选择一个区间\([l,r](l\le r)\),使区间内的每一个数减\(1\);
- 任意选择一个点\(p\)和一个正整数\(x(x\ge 1\),使\(a_p\)减去\(x\)。
求把原数列全部变为\(0\)的最少的操作次数。
\(1\le n\le 5000\),\(0\le a_i\le 10^9\)。
算法考察
贪心,分治
算法分析
CF自己出自己的原题可还行(CF448C)。
CF出NOIp的原题的原题的改编题可还行(铺设道路/积木大赛)。
如果只有第二种操作,答案显然是\(n\)。
如果只有第一种操作,就是铺设道路/积木大赛,答案也容易求。
现在我们把两种操作综合起来。
我们不难想到一个贪心,要么直接\(n\)次操作2解决,要么尽可能使用操作1,使得整个序列不能再整体减少(否则会出现负数),再对被\(0\)分隔开的每一个区间采取类似的方法操作。
这个贪心的正确性可以归纳证明,显然边界条件是剩余部分序列的值相等,这时可以直接按照上述方法操作,这里的最优性容易证明。
下面我们假设分开的每一个区间都是最优情况。
我们只要证明尽可能使用操作1一定不会比不完全使用操作1答案差即可。我们假设给这个区间少减去1,这种调整对后面全部使用操作2的区间没有影响,对原来为0的区间需要多1次操作,对原来先使用操作1的区间也需要多使用至少1次操作,因此一定不如原来的方案优。
至此我们得出了这样一个分治的贪心算法,复杂度\(O(n^2)\),可以证明处理一个长度为\(len\)的区间的额外开销是\(O(len)\),每一次分治至少使总区间长度\(-1\),因此\(T(n)=T(n-1)+O(n)=O(n^2)\),常数很小。
另:本题和\(CF448C\)有一点点区别,\(CF448C\)中\(a_i>0\),本题\(a_i\ge 0\),要注意如下的数据:
1
0
代码实现
#include<bits/stdc++.h>
using namespace std;
#define maxn 5005
#define maxm 2000005
#define inf 0x3f3f3f3f
#define int long long
#define mod 1000000007
#define local
template <typename Tp> void read(Tp &x){
int fh=1;char c=getchar();x=0;
while(c>'9'||c<'0'){if(c=='-'){fh=-1;}c=getchar();}
while(c>='0'&&c<='9'){x=(x<<1)+(x<<3)+(c&15);c=getchar();}x*=fh;
}
int n,m;
int a[maxn];
//实际实现中并不需要真正区间减
int solve(int l,int r,int h){
if(l==r){
if(a[l]==h)return 0;
else return 1;//注意特判,否则HACKED
}
int mh=inf;
for(int i=l;i<=r;i++)mh=min(mh,a[i]);//求解
int ans=mh-h,j;
for(int i=l;i<=r;i++){
if(a[i]==mh)continue;
for(j=i;j<=r;j++){
if(j==r||a[j+1]==mh)break;
}//找到被分割开的区间
ans+=solve(i,j,mh);
i=j+1;
}
return min(ans,r-l+1);//与全操作2比较
}
signed main(){
read(n);
for(int i=1;i<=n;i++)read(a[i]);
printf("%lld\n",solve(1,n,0));
return 0;
}
F
题意简述
给定一个数字字符串\(s\),定义\(f(l,r)\)为\(s_l,s_{l+1},\cdots,s_{r}\)的数字和。
定义一个区间\([l,r]\)是\(x-prime\)的,当且仅当满足如下条件:
- \(f(l,r)=x\)
- 不存在\([l,r]\)的子区间\([l_2,r_2]\),使得\(f(l_2,r_2)\)是\(x\)的真因数。
求最少要删除\(s\)中的字符个数,使得删除后的\(s\)不存在\(x-prime\)区间。
\(1\le |s|\le 1000\),\(1\le x\le 20\)。
算法考察
特殊性质观察,AC自动机上DP
算法分析
通过某种方法我们可以发现\(x\le 20\)的范围内\(x-prime\)的字符串很少,最多的是\(x=19\)时,共有2399个。
因此我们可以把所有的\(x-prime\)生成出来,问题就转化为对给定的字符串\(s\),求其最长子序列,使得这个子序列不存在最多\(2399\)个非法字符串。
这是一个经典问题,我们考虑AC自动机上DP。
设计状态dp[i][j]表示考虑前\(i\)个字符的子序列,其中如果放在AC自动机上匹配,当前状态处于\(j\)的最长子序列长度。
考虑下一步的转移,不难写出状态转移方程:
其中ch[k][s[i]]=j,且\(j\)不是某一个非法字符串的终末状态。
可以使用滚动数组优化空间,总时间复杂度为\(O(nm)\),其中\(m\)为建出的AC自动机总状态数。
代码实现
#include<bits/stdc++.h>
using namespace std;
#define maxn 1005
#define maxm 2505
#define inf 0x3f3f3f3f
#define LL long long
#define mod 1000000007
#define local
template <typename Tp> void read(Tp &x){
int fh=1;char c=getchar();x=0;
while(c>'9'||c<'0'){if(c=='-'){fh=-1;}c=getchar();}
while(c>='0'&&c<='9'){x=(x<<1)+(x<<3)+(c&15);c=getchar();}x*=fh;
}
int n,m,X;
string s0;
int ch[maxm*10][10],fail[maxm*10],edp[maxm*20],tot;
void ins(string ss){
int p=0;
for(unsigned i=0;i<ss.size();i++){
int dir=ss[i]-'0';
if(!ch[p][dir])ch[p][dir]=++tot;
p=ch[p][dir];
}
edp[p]++;
}
void build(){//AC自动机初始化
queue<int>q;int p=0;
for(int i=1;i<10;i++)
if(ch[0][i])q.push(ch[0][i]),fail[ch[0][i]]=0;
while(!q.empty()){
p=q.front();q.pop();
for(int i=1;i<10;i++){
if(ch[p][i])fail[ch[p][i]]=ch[fail[p]][i],q.push(ch[p][i]);
else ch[p][i]=ch[fail[p]][i];
}
}
}
int dp[2][maxm*10];
int check(string ss){//暴力检验,可以通过
int sm=0;
for(int l=0;l<ss.size();l++){
sm=0;
for(int r=l;r<ss.size();r++){
sm+=ss[r]-'0';
if(sm!=X&&X%sm==0)return 0;
}
}
return 1;
}
void generate(string st,int sm){
if(sm>X)return;
if(sm==X){
if(check(st))ins(st);
return;
}
for(int i=1;i<10;i++){
generate(st+(char)(i+'0'),sm+i);
}
}
signed main(){
cin>>s0;
n=s0.size();
read(X);
generate("",0);//生成所有x-prime
build();
memset(dp,0x3f,sizeof(dp));
dp[0][0]=0;
int p=1,q=0;
for(int i=0;i<n;i++){
swap(p,q);
for(int j=0;j<=tot;j++)dp[q][j]=inf;
for(int j=0;j<=tot;j++){
dp[q][j]=min(dp[q][j],dp[p][j]+1);//转移1
if(!edp[ch[j][s0[i]-'0']])dp[q][ch[j][s0[i]-'0']]=min(dp[q][ch[j][s0[i]-'0']],dp[p][j]);//转移2
}
}
int ans=inf;
for(int i=0;i<=tot;i++)ans=min(ans,dp[q][i]);
printf("%d\n",ans);
return 0;
}
G
题意简述
有\(n\)个人和\(m\)对敌对关系,每一个人有一个条件区间\([l_i,r_i]\)。
定义一个合法的选择\(\{S\}\),当且仅当对于\(\{S\}\)中的所有人\(i\),\(l_i\le |S| \le r_i\),且\(\{S\}\)中所有人互不敌对。
求有多少种合法的选择,答案对\(998244353\)取模。
\(1\le n\le 3\times 10^5\),\(0\le m\le \min\{20,\frac{n(n-1)}{2}\}\),\(1\le l_i\le r_i\le n\)。
算法考察
计数,容斥,组合数,前缀和
算法分析
看到\(m\)的范围很小,不难想到容斥。
容斥敌对关系的集合,在这个集合内的敌对关系的人必选,其余人任意,乘上容斥系数即为最后答案。
现在我们要解决的问题转变为:一部分人必选,且最后选择的总人数符合要求的方案数。
先考虑特殊情况,如果没有人必选,那么我们枚举最后选择的总人数\(i\),枚举每一个人讨论是否可以被选择,假设有\(x_i\)个人可以被选择,则答案为\(C_{x_i}^i\),这个做法可以通过前缀和优化\(O(1)\)求出。
再考虑一般情况,假设有\(a\)个人必选,不难发现最后可能合法的选择大小一定是这\(a\)个人条件区间的交集,若干个区间的交集最多一个区间,因此我们枚举再这个区间内枚举最后选择的总人数\(i\),假设总共有\(x_i\)个人可以被选择,则答案为\(C_{x_i-a}^{i-a}\)。我们发现\(x_i\)是一个固定的值,而因为敌对关系最多\(20\)对,因此最多有\(40\)个人可能会必选,因此我们可以对每一个\(a\)预处理组合数,再使用前缀和优化,\(O(1)\)求出答案。
本算法时间复杂度为\(O(mn+2^m)\)。
代码实现
#include<bits/stdc++.h>
using namespace std;
#define maxn 300005
#define maxm 2000005
#define inf 0x3f3f3f3f
#define int long long
#define mod 998244353
#define local
template <typename Tp> void read(Tp &x){
int fh=1;char c=getchar();x=0;
while(c>'9'||c<'0'){if(c=='-'){fh=-1;}c=getchar();}
while(c>='0'&&c<='9'){x=(x<<1)+(x<<3)+(c&15);c=getchar();}x*=fh;
}
int ksm(int B,int P,int Mod){int ret=1;while(P){if(P&1)ret=1ll*ret*B%Mod;B=1ll*B*B%Mod;P>>=1;}return ret;}
struct node{
int l,r;
}a[maxn];
int d1[25],d2[25];
int n,m;
int ans;
int frac[maxn],frinv[maxn];
int C(int x,int y){//求组合数,注意可能出现x<0,y<0的情况
if(x<y)return 0;
if(x<0||y<0)return 0;
return frac[x]*frinv[y]%mod*frinv[x-y]%mod;
}
map<int,int>mp;
int siz[1<<21],LG[1<<21];
int s[maxn][45];
int calc(int S){
mp.clear();
int l=1,r=n;
int ai;
for(int ss=S;ss;ss-=(ss&(-ss))){
int x=LG[(ss&(-ss))]+1;
mp[d1[x]]=1;mp[d2[x]]=1;
l=max(l,max(a[d1[x]].l,a[d2[x]].l));
r=min(r,min(a[d1[x]].r,a[d2[x]].r));
}
ai=mp.size();//求区间和必选人数
if(l>r)return 0;
else return s[r][ai]-s[l-1][ai];
}
signed main(){
read(n);read(m);
frac[0]=1;
for(int i=1;i<=n;i++)frac[i]=1ll*frac[i-1]*i%mod;
frinv[n]=ksm(frac[n],mod-2,mod);
for(int i=n-1;~i;i--)frinv[i]=1ll*frinv[i+1]*(i+1)%mod;
LG[1]=0;
for(int i=2;i<(1<<20);i++)LG[i]=LG[i>>1]+1;
siz[0]=1;
for(int i=1;i<(1<<20);i++)siz[i]=-siz[i-(i&(-i))];
//一堆预处理,frac,frinv用于快速求组合数,siz求容斥系数,LG求log向下取整
for(int i=1;i<=n;i++)read(a[i].l),read(a[i].r);
for(int i=1,x,y;i<=m;i++){
read(x);read(y);
d1[i]=x;d2[i]=y;
}
for(int i=1;i<=n;i++){
s[a[i].l][0]++;s[a[i].r+1][0]--;
}
for(int i=1;i<=n;i++)s[i][0]+=s[i-1][0];//一次前缀和求出所有x_i
for(int i=1;i<=n;i++){
for(int j=1;j<=40;j++){
s[i][j]=C(s[i][0]-j,i-j);//计算所有答案
}
s[i][0]=C(s[i][0],i);
}
for(int j=0;j<=40;j++){
for(int i=1;i<=n;i++){
s[i][j]=(s[i-1][j]+s[i][j])%mod;//再一次前缀和求出答案
}
}
for(int i=0;i<(1<<20);i++){
ans=(ans+siz[i]*calc(i))%mod;//容斥计算
}
printf("%lld\n",(ans+mod)%mod);
return 0;
}