Codeforces Round #626 (Div. 2, based on Moscow Open Olympiad in Informatics)
A. Even Subset Sum Problem
题意:给n个数,找个非空子集使其和为偶数,输出子集大小和元素下标,不存在输出-1。
思路:如果有偶数就取一个偶数,否则取两个奇数,否则不合法。
1 #include<bits/stdc++.h> 2 #define LL long long 3 #define dl double 4 void rd(int &x){ 5 x=0;int f=1;char ch=getchar(); 6 while(ch<'0' || ch>'9'){if(ch=='-')f=-1;ch=getchar();} 7 while(ch<='9' && ch>='0')x=x*10+ch-'0',ch=getchar();x*=f; 8 } 9 void lrd(LL &x){ 10 x=0;int f=1;char ch=getchar(); 11 while(ch<'0' || ch>'9'){if(ch=='-')f=-1;ch=getchar();} 12 while(ch<='9' && ch>='0')x=x*10+ch-'0',ch=getchar();x*=f; 13 } 14 const int INF=1e9; 15 const LL LINF=1e18; 16 const int N=105; 17 using namespace std; 18 int T; 19 int n,a[N]; 20 int main(){ 21 // freopen("in.txt","r",stdin); 22 rd(T); 23 while(T--){ 24 rd(n);for(int i=1;i<=n;i++)rd(a[i]); 25 int id1=0,id2=0; 26 for(int i=1;i<=n;i++)if(a[i]%2==0)id1=i; 27 if(id1)printf("%d\n%d\n",1,id1); 28 else { 29 for(int i=1;i<=n;i++) 30 if(a[i]%2){ 31 if(id1)id2=i;else id1=i; 32 } 33 if(id1 && id2)printf("%d\n%d %d\n",2,id1,id2); 34 else printf("-1\n"); 35 } 36 } 37 return 0; 38 } 39 /**/
B. Count Subrectangles
题意:给两个01数组a,b,长度分别为n,m(4e4),记录cij=ai*bj,c形成一个n*m的矩阵,问这个矩阵里面所有大小为k(n*m)的全为1的子矩阵有多少个。
思路:对k分解质因数来得到子矩阵的长和宽,k=p*q,找这样的子矩阵本质上就是找a中长度为p的连续1子串和b中长度为q的连续1子串。一个长度为p+r的连续1串可以有r+1个长度为p的连续1串。以a为例,预处理出所有连续1串的长度到vector中,排序,再记录后缀和,每次查询lower_bound,整个后缀都会贡献答案。
1 #include<bits/stdc++.h> 2 #define LL long long 3 #define dl double 4 void rd(int &x){ 5 x=0;int f=1;char ch=getchar(); 6 while(ch<'0' || ch>'9'){if(ch=='-')f=-1;ch=getchar();} 7 while(ch<='9' && ch>='0')x=x*10+ch-'0',ch=getchar();x*=f; 8 } 9 void lrd(LL &x){ 10 x=0;int f=1;char ch=getchar(); 11 while(ch<'0' || ch>'9'){if(ch=='-')f=-1;ch=getchar();} 12 while(ch<='9' && ch>='0')x=x*10+ch-'0',ch=getchar();x*=f; 13 } 14 const int INF=1e9; 15 const LL LINF=1e18; 16 const int N=4e4+10; 17 using namespace std; 18 int n,m,k; 19 int a[N],b[N]; 20 vector<int>S1,S2; 21 int s1[N],s2[N]; 22 LL ans; 23 void work(int x,int y){ 24 if(lower_bound(S1.begin(),S1.end(),x) == S1.end())return ; 25 if(lower_bound(S2.begin(),S2.end(),y) == S2.end())return ; 26 int id1=lower_bound(S1.begin(),S1.end(),x)-S1.begin(),id2=lower_bound(S2.begin(),S2.end(),y)-S2.begin(); 27 ans+=(s1[id1]-(S1.size()-id1)*(x-1))*(s2[id2]-(S2.size()-id2)*(y-1)); 28 } 29 int main(){ 30 // freopen("in.txt","r",stdin); 31 rd(n);rd(m);rd(k); 32 for(int i=1;i<=n;i++)rd(a[i]); 33 for(int i=1;i<=m;i++)rd(b[i]); 34 int now=0; 35 for(int i=1;i<=n+1;i++){ 36 if(a[i])now++; 37 else { 38 if(now)S1.push_back(now); 39 now=0; 40 } 41 } 42 for(int i=1;i<=m+1;i++){ 43 if(b[i])now++; 44 else { 45 if(now)S2.push_back(now); 46 now=0; 47 } 48 } 49 sort(S1.begin(),S1.end());sort(S2.begin(),S2.end()); 50 for(int i=S1.size()-1;i>=0;i--)s1[i]=s1[i+1]+S1[i]; 51 for(int i=S2.size()-1;i>=0;i--)s2[i]=s2[i+1]+S2[i]; 52 int t=sqrt(k); 53 for(int i=1;i<=t;i++){ 54 if(k % i)continue; 55 work(i,k/i); 56 if(i != k/i)work(k/i,i); 57 } 58 printf("%lld\n",ans); 59 return 0; 60 } 61 /**/
反思:这题wa了n次,具体原因是没开longlong,为什么没开longlong呢?因为我以为k=1是极限情况,那个不爆int,而事实上k=1并不是极限情况。以后多个心眼看看是不是算错了上界。
C. Unusual Competitions
题意:给长度为n(1e6)的括号序列,每次可以选择一个区间,将它们按任意顺序重新排列,操作的代价为区间长度,问最少需要多少代价可以使得最终的括号序列合法,做不到输出-1。
思路:考虑左括号和右括号数目一不一样,不一样一定不合法,一样一定合法,因为至少可以对整个区间进行重新排列。计'('为1,')'为-1,然后求出前缀和s,如果所有的s都非负,那么序列合法(常用结论),那么对于每个负数区间,我们都可以通过一次操作使其合法,区间左端点的s为-1,右端点为0。而我们也必须这么做,否则永远存在负数的s,答案便很容易统计。
1 #include<bits/stdc++.h> 2 #define LL long long 3 #define dl double 4 void rd(int &x){ 5 x=0;int f=1;char ch=getchar(); 6 while(ch<'0' || ch>'9'){if(ch=='-')f=-1;ch=getchar();} 7 while(ch<='9' && ch>='0')x=x*10+ch-'0',ch=getchar();x*=f; 8 } 9 void lrd(LL &x){ 10 x=0;int f=1;char ch=getchar(); 11 while(ch<'0' || ch>'9'){if(ch=='-')f=-1;ch=getchar();} 12 while(ch<='9' && ch>='0')x=x*10+ch-'0',ch=getchar();x*=f; 13 } 14 const int INF=1e9; 15 const LL LINF=1e18; 16 const int N=1e6+10; 17 using namespace std; 18 int n; 19 char s[N]; 20 int ans; 21 int cnt; 22 int main(){ 23 // freopen("in.txt","r",stdin); 24 rd(n);scanf("%s",s+1); 25 for(int i=1;i<=n;i++)if(s[i] == '(')cnt++; 26 if(cnt*2 != n)printf("-1\n"); 27 else { 28 bool flg=0; 29 int now=0; 30 cnt=0; 31 for(int i=1;i<=n;i++){ 32 if(s[i] == '(')now++; 33 if(s[i] == ')')now--; 34 if(now < 0)flg=1; 35 cnt++; 36 if(now == 0){ 37 if(!flg){ 38 cnt=0; 39 } 40 else ans+=cnt,flg=0,cnt=0; 41 } 42 } 43 printf("%d\n",ans); 44 } 45 return 0; 46 } 47 /**/
反思:括号序列问题一定要往前缀和这个方向去想。
D. Present
题意:给n(4e5)个数字ai(1~1e7),求两两和的异或和。
思路:异或相关题目一般都是按位处理,从低位到高位,发现加和时对当前位造成影响的只有低位,于是把当前位(第k位)及更低的位全部取出来,这些数加和是有上限的,也就是2^(k+1)-2,可以发现使得当前位为1的只能是2^(k-1)~2^k-1和2^(k-1)+2^k~2^(k+1)-2这两个区间内的数,问题也就转换为找出有多少个两两组合落在这个区间里面,二分即可。
1 #include<bits/stdc++.h> 2 #define LL long long 3 #define dl double 4 void rd(int &x){ 5 x=0;int f=1;char ch=getchar(); 6 while(ch<'0' || ch>'9'){if(ch=='-')f=-1;ch=getchar();} 7 while(ch<='9' && ch>='0')x=x*10+ch-'0',ch=getchar();x*=f; 8 } 9 void lrd(LL &x){ 10 x=0;int f=1;char ch=getchar(); 11 while(ch<'0' || ch>'9'){if(ch=='-')f=-1;ch=getchar();} 12 while(ch<='9' && ch>='0')x=x*10+ch-'0',ch=getchar();x*=f; 13 } 14 const int INF=1e9; 15 const LL LINF=1e18; 16 const int N=4e5+10; 17 using namespace std; 18 int n,a[N]; 19 vector<int>S; 20 int ans; 21 int main(){ 22 // freopen("in.txt","r",stdin); 23 rd(n);for(int i=1;i<=n;i++)rd(a[i]); 24 for(int now=1;now<=25;now++){ 25 S.clear(); 26 for(int i=1;i<=n;i++)S.push_back(a[i] % (1<<now)); 27 sort(S.begin(),S.end());LL cnt=0; 28 int L1=(1<<now-1),R1=(1<<now)-1,L2=(1<<now-1)+(1<<now),R2=(1<<now+1)-2; 29 for(int i=0;i<n;i++){ 30 int l1=lower_bound(S.begin(),S.end(),L1-S[i])-S.begin(),r1=upper_bound(S.begin(),S.end(),R1-S[i])-S.begin()-1; 31 int l2=lower_bound(S.begin(),S.end(),L2-S[i])-S.begin(),r2=upper_bound(S.begin(),S.end(),R2-S[i])-S.begin()-1; 32 l1=max(l1,i+1);if(l1 <= r1)cnt+=(r1-l1+1); 33 l2=max(l2,i+1);if(l2 <= r2)cnt+=(r2-l2+1); 34 } 35 if(cnt & 1)ans|=(1<<now-1); 36 } 37 printf("%d\n",ans); 38 return 0; 39 } 40 /**/
反思:一开始我做这道题只是关心当前位,通过1,0的个数算出答案,然而这没有考虑低位造成的影响,于是我记录了当前位1和1组合的情况对下一位造成的影响,然而这仍是不对的,因为造成影响并不只是相邻位之间的,而是所有低位都可能对高位造成影响,有"连锁反应"。
E. Instant Noodles
题意:给一张二分图,n(5e5)*2个点,m(5e5)条边,右半部分的n个点有权值,对于左面的点的某个集合S,记录N(S)为与其相邻的右面点的集合,f(S)为N(S)中所有点的权值和,空集记为0,求所有f(S)的gcd是多少。
思路:先给出结论,将右面的点按其相连的点的集合进行分组,完全一样的分到一组,每组记录权值和,合并成一个新的点,不与任何点相连的点无意义,被丢出集合,这些新的点的权值的gcd就是答案。下面给出证明。首先分到一个组的点必定是同时选同时不选的,所以完全可以合并成一个点。N(S)必是这些点的组合,那么这些点的gcd,设其为p,一定可以整除所有f(S)(注意到这里并不能得出结论,因为最终的N(S)可能不包含单个点的情况,而目前情况无法判断所有单个点都不是N(S)的情况下gcd是否还是p,也就是说此时的p只能说是最终gcd的一个因子)。接下来证明这些点除以p后f(S)的gcd为1,便可以证明最终的gcd为p。用了一种很常用的证明方法,即证明对于任意q,都存在一个f(S)使得q不能整除f(S),这样便可以说明所有f(S)的gcd为1。考虑把左面所有的点都选上,那么N(S)就是右面所有点的集合,如果它不能被q整除,那么就证明完毕了,接下来考虑它能被q整除,我们选取一个度数最小的且不能被q整除的点v,这样的点一定存在,因为所有的点除以p之后gcd为1。我们选取S集合为不和这个点相邻的所有点,那么不存在于N(S)的点有两种,v以及相邻的点为v相邻的点的子集的点,而这些点度数一定小于v的度数,所以能被k整除,而v这个点不能被k整除,所以不存在于N(S)的点的和不能被k整除,因为所有点之和能被k整除,所以N(S)部分的点不能被k整除。证毕。
1 #include<bits/stdc++.h> 2 #define LL long long 3 #define dl double 4 void rd(int &x){ 5 x=0;int f=1;char ch=getchar(); 6 while(ch<'0' || ch>'9'){if(ch=='-')f=-1;ch=getchar();} 7 while(ch<='9' && ch>='0')x=x*10+ch-'0',ch=getchar();x*=f; 8 } 9 void lrd(LL &x){ 10 x=0;int f=1;char ch=getchar(); 11 while(ch<'0' || ch>'9'){if(ch=='-')f=-1;ch=getchar();} 12 while(ch<='9' && ch>='0')x=x*10+ch-'0',ch=getchar();x*=f; 13 } 14 const int INF=1e9; 15 const LL LINF=1e18; 16 const int N=5e5+10; 17 const int mod=1e9+7; 18 using namespace std; 19 int T; 20 int n,m; 21 map<int,LL>S; 22 LL c[N]; 23 vector<int>f[N]; 24 LL gcd(LL x,LL y){return !y?x:gcd(y,x%y);} 25 int main(){ 26 // freopen("in.txt","r",stdin); 27 rd(T); 28 while(T--){ 29 rd(n);rd(m);S.clear(); 30 for(int i=1;i<=n;i++)lrd(c[i]),f[i].clear(); 31 for(int i=1;i<=m;i++){ 32 int x,y;rd(x);rd(y); 33 f[y].push_back(x); 34 } 35 for(int i=1;i<=n;i++)sort(f[i].begin(),f[i].end()); 36 for(int i=1;i<=n;i++){ 37 if(!f[i].size())continue; 38 int hs=0; 39 for(int j=0;j<f[i].size();j++)hs=(1ll*hs*(n+1)+f[i][j])%mod; 40 if(S.find(hs) == S.end())S[hs]=c[i]; 41 else S[hs]+=c[i]; 42 } 43 map<int,LL>::iterator it; 44 LL ans=0; 45 for(it=S.begin();it != S.end();it++)if(ans)ans=gcd(ans,it->second);else ans=it->second; 46 printf("%lld\n",ans); 47 } 48 return 0; 49 }
反思:0与x的gcd就是x而不是0,这一点看gcd的求解过程应该就可以理解。另外就是这种证明gcd为1的思想,之前证明本原多项式已经用过类似的方法了,就是转化为证明对于任意q都存在一个数字不能被q整除。
F. Reality Show
题意:有n(2e3)个人,每个人有一个好斗值li,以及招募这个人的花费si,初始好斗值最高为m(2e3),每个好斗值对应一个数字ci(+-5e3),给出1~n+m的好斗值对应的ci。每个人按顺序进入面试,如果这个人的好斗值严格大于前面所有被录取的人的好斗值,那么他一定被淘汰,否则淘汰与否由你决定。选好人之后按以下方式进行,依次进入舞台,根据自己的好斗值得到c的收益,任何时候台上如果有两个好斗值相同的人,那么这两个人其中一个退出舞台,另外一个好斗值+1,并再获得当前好斗值对应的c的收益,问如何选择录取的人使得最终收益最大。
思路:考虑dp。整个过程有点像2048,两个碰一个再获得额外收益。由于初始好斗值最高为m,最终好斗值最多只会略大于m。所以完全可以把好斗值放入dp的一维中。可以发现选好人之后的入场顺序完全不会影响结果,所以没必要顺序选人。我们考虑倒序选人,好处会在后面体现出来。这里是借鉴榜一大佬的思路,因为答案真的没看明白。首先,可以把si改成cli-si,每个入选的人都至少会带来表演的收益cli,这样si就表示这个人表演带来的净收益,方便后面计算。倒序选人的话,好斗值必须满足不减。f[i][j]表示到当前位,选择的所有人中好斗值小于等于i且等于i的有j个人时的最大收益,枚举完所有人之后对f数组中所有数字取最大值即可。这里f[i][j]中包含两种情况。一种是真的有好斗值为i的人,一种是通过"碰"可以得到好斗值为i的人,不妨称之为真和假。对于当前枚举的人,如果不选择他,那么对所有f没有任何影响。如果选择他,他会更新哪些值呢?不妨设u为他的好斗值。fij中i<u的情况完全不需要考虑,因为与fij的定义不符,而对于i>u的情况,对于里面"真"的部分,不符合好斗值不减这一条件,所以不会更新,而对于"假"的部分,比如f[u+1][1]里面有"假"的部分,并且这个"假"的部分是由一堆好斗值<=u的人凑出来的,那么他一定会有相同的值在f[u][2]这个地方,而如果"假"的部分都是由>u的凑出来的,便和真没有什么区别了。所以事实上我们只需要更新i=u的情况。然后便可以枚举所有可能的j,这里通过记录截至目前为止好斗值为i的人最多有多少个来得到枚举的上界,一定要倒序枚举,因为枚举f[u][j]后会对f[u][j+1]进行更新。除了对f[i][j+1]进行更新,我们还需要考虑他"碰"了之后得到的"假"的情况,也就是继续更新f[i+1][(j+1)/2],f[i+2][(j+1)/4]...而这种方式最多更新log次,总体复杂度是控制在n^2log以内的,并且跑不满。这部分更新完之后,还需要对所有的f[i][0]进行更新,只需要用f[i-1][0]和f[i-1][1]来更新,因为f[i][2]只能对应假的f[i][1]而不能对应f[i][0]了。这部分是n^2。总体复杂度是n^2log,上界很松。cf上只跑了70ms。我们不妨再看一下如果不是倒序枚举会出现什么情况,至少以同样的dp方式来看的话,对于u需要更新>=u的部分中“真”的部分或是尽管假但仍然是由大于等于u的数字碰出来的部分,而我们并不能将这些部分分开来计算。至于是否有其他方法能使得正序可做不得而知,但至少这种倒序应该是一种常用的固定套路。应该是出现在这种"碰"的情况的时候使用,因为倒序时需要的是i>u中假的部分,而这部分可以在=u处找到,反观正序,需要的是i>u中真的部分,这部分却无法单独被拿出来更新,也无法在=u处找到。
1 #include<bits/stdc++.h> 2 #define LL long long 3 #define dl double 4 void rd(int &x){ 5 x=0;int f=1;char ch=getchar(); 6 while(ch<'0' || ch>'9'){if(ch=='-')f=-1;ch=getchar();} 7 while(ch<='9' && ch>='0')x=x*10+ch-'0',ch=getchar();x*=f; 8 } 9 void lrd(LL &x){ 10 x=0;int f=1;char ch=getchar(); 11 while(ch<'0' || ch>'9'){if(ch=='-')f=-1;ch=getchar();} 12 while(ch<='9' && ch>='0')x=x*10+ch-'0',ch=getchar();x*=f; 13 } 14 const int INF=1e9; 15 const LL LINF=1e18; 16 const int N=2050; 17 using namespace std; 18 int n,m; 19 int l[N],s[N],c[N<<1]; 20 int f[N<<1][N]; 21 int ans,mx[N]; 22 int main(){ 23 // freopen("in.txt","r",stdin); 24 rd(n);rd(m); 25 for(int i=1;i<=n;i++)rd(l[i]); 26 for(int i=1;i<=n;i++)rd(s[i]); 27 for(int i=1;i<=n+m;i++)rd(c[i]); 28 for(int i=1;i<=n;i++)s[i]=c[l[i]]-s[i];//直接招募这个人可以赚到多少钱 29 memset(f,-0x7f,sizeof(f)); 30 for(int i=1;i<=m+20;i++)f[i][0]=0; 31 for(int i=n;i>=1;i--){ 32 int u=l[i]; 33 for(int j=mx[u];j>=0;j--){//必须倒序 34 int x=f[u][j]+s[i],y=j+1; 35 for(int o=u;y;){ 36 mx[o]=max(mx[o],y); 37 f[o][y]=max(f[o][y],x); 38 o++;y/=2;x+=c[o]*y; 39 } 40 ans=max(ans,x); 41 } 42 for(int i=1;i<=m+20;i++)f[i][0]=max(f[i][0],max(f[i-1][0],f[i-1][1])); 43 } 44 printf("%d\n",ans); 45 return 0; 46 } 47 /*真/假两种情况,可以发现都不需要转移*/
反思:对于一道dp题目,我们只要能保证dp数组涵盖了答案,并且能够确定转移方式,确定初始值便可以得到答案。另外需要了解这种倒序枚举简化dp的思路,具体也说不太清楚,大概是要求某一个值递减,并且所设的dp数组表示<=i的情况,而且i会对>=i的部分进行更新,这时候可以使用倒序枚举的方法。