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 /**/
View Code

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 /**/
View Code

反思:这题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 /**/
View Code

 

反思:括号序列问题一定要往前缀和这个方向去想。

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 /**/
View Code

反思:一开始我做这道题只是关心当前位,通过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 }
View Code

反思: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 /*真/假两种情况,可以发现都不需要转移*/
View Code

反思:对于一道dp题目,我们只要能保证dp数组涵盖了答案,并且能够确定转移方式,确定初始值便可以得到答案。另外需要了解这种倒序枚举简化dp的思路,具体也说不太清楚,大概是要求某一个值递减,并且所设的dp数组表示<=i的情况,而且i会对>=i的部分进行更新,这时候可以使用倒序枚举的方法。

posted @ 2020-03-10 18:25  hyghb  阅读(206)  评论(0编辑  收藏  举报