比赛链接
半天才做出B,更半天才做出C——因为少考虑了一种情况,C还WA了两次,最后才过的……
A
分析:
相当于两边同时(+x),然后一边(+k)、一边(-k)。稍微判断一下就好了。
代码如下
#include<iostream>
using namespace std;
int T,c,d;
int main()
{
scanf("%d",&T);
while(T--)
{
scanf("%d%d",&c,&d);
if((c+d)&1){puts("-1"); continue;}
int x=(c+d)/2,ans=0;
if(x)ans++;
if(c==x&&d==x){printf("%d
",ans); continue;}
if(x-c==d-x)ans++; else ans+=2;
printf("%d
",ans);
}
return 0;
}
B
分析:
首先,最终状态一种是奇数位置是奇数、偶数位置是偶数;一种是奇数位置是偶数、偶数位置是奇数。用这个判断一下不合法情况就行了。然后两种取优。
至于怎么挪……一开始想了半天(set)、链表之类,为了维护挪了以后的序列。但是后来发现不用,因为每个数最终在的位置是确定的,比如奇数就是按它们的相对顺序排列在奇数/偶数位置上(保持相对顺序使操作次数最少)。所以按顺序直接朝目标位置挪就行。
那么怎么考虑奇数和偶数挪的时候彼此的影响呢?实际上只考虑把奇数挪到合适位置上就行,挪完以后会发现偶数也自然在合适位置上了。
我们从前往后按顺序挪,那会不会出现前面的一个奇数要挪到后面去,挪的过程中把后面的那个奇数挪前了,导致答案算错而且不优?实际上也不会。因为这种可以看作是先挪了后面的、再挪前面的(因为它们都要挪到后面去,所以这样更优)。
代码如下
#include<iostream>
#define ll long long
using namespace std;
int const N=1e5+5;
ll const inf=1e12;
int T,n,a[N];
int ab(int x){return x<0?-x:x;}
int main()
{
scanf("%d",&T);
while(T--)
{
scanf("%d",&n); int num0=0,num1=0;
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]); a[i]%=2;
if(!a[i])num0++; else num1++;
}
if(n&1)
{
if(ab(num0-num1)!=1){puts("-1"); continue;}
ll ans=0;
if(num0>num1)
{
for(int i=1,p0=1,p1=2;i<=n;i++)
{
if(!a[i])ans+=ab(i-p0),p0+=2;
// else ans+=(i-p1),p1+=2;
}
}
else
{
for(int i=1,p1=1,p0=2;i<=n;i++)
{
if(!a[i])ans+=ab(i-p0),p0+=2;
// else ans+=(i-p1),p1+=2;
}
}
printf("%lld
",ans);
}
else
{
if(num0!=num1){puts("-1"); continue;}
ll a1=0,a2=0;
for(int i=1,p0=1,p1=2;i<=n;i++)
{
if(!a[i])a1+=ab(i-p0),p0+=2;
// else a1+=(i-p1),p1+=2;
}
for(int i=1,p1=1,p0=2;i<=n;i++)
{
if(!a[i])a2+=ab(i-p0),p0+=2;
// else a2+=(i-p1),p1+=2;
}
printf("%lld
",min(a1,a2));
}
}
return 0;
}
C
分析:
这个也想了好久……我们两个两个枚举,也就是当前(c_i)是一群左括号,(c_{i+1})是一群右括号。然后我们用一个栈记录两个值:(rem[i])和(pre[i]),分别表示前面剩下的左括号,以及那个左括号后面跟了多少个合法的最小括号序列。下面我们考虑每个合法子串的右端点。
若(c_i>c_{i+1}),说明当前左括号多于右括号,那么(ans += c_{i+1}),而(c_{i+1})这些右括号作为右端点也再不能往前走了。栈增加一个元素,(res[i] = c_i - c_{i+1}, pre[i]=1)。
若(c_i<c_{i+1}),说明当前右括号多于左括号,那么首先$ans += c_i, c_{i+1} -= c_i $ ,然后右括号继续往前走,也就是提取栈里的元素,每次 $ ans += pre[i], ans += rem[i], c_{i+1} -= rem[i] $ ,直到栈空了或者 $ c_{i+1} leq rem[i] $ 。
如果是 $ c_{i+1} < rem[i] $ ,说明右括号在这里用完了,那么 $ ans += c_{i+1}, rem[i] -= c_{i+1} $ ,然后 $ pre[i]=1 $ ,表示当前最右的括号与栈内此元素的左括号形成了一个合法子串。
如果是 $ c_{i+1} = rem[i] $ ,那么左右括号在这里恰好匹配完了。对应操作一番即可。这里要注意!!匹配完以后 $ ans $ 还要加上栈内前一个元素的 $ pre $ ,表示匹配后的子串还可以连上那些合法子串计入答案!!一开始写的时候没注意这个,调了一小时。
若 $ c_i=c_{i+1} $ ,和上面类似。
因为每个位置至多入栈出栈一次,所以时间复杂度是 $ O(n) $ 的。
代码如下
#include<iostream>
#define ll long long
using namespace std;
int const N=1005;
int n,c[N],cnt;
ll ans,rem[N],pre[N];
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d",&c[i]);
for(int i=1;i<=n;i+=2)
{
if(i==n)continue;//!
if(c[i]>c[i+1])
{
ans+=c[i+1];
rem[++cnt]=c[i]-c[i+1]; pre[cnt]=1;
}
else if(c[i]==c[i+1])
{
ans+=c[i];
if(cnt)ans+=pre[cnt],pre[cnt]++;//rem[cnt]=0 only at head
else rem[++cnt]=0,pre[cnt]=1;
}
else
{
ans+=c[i]; int rt=c[i+1]-c[i];
while(cnt&&rt>rem[cnt])//rt!=0
{
ans+=pre[cnt]; ans+=rem[cnt];
rt-=rem[cnt]; cnt--;
}
if(cnt)
{
/*
if(rt==0)//! rt=0&&rem=0
{
ans+=pre[cnt]; pre[cnt]++;
continue;
}
*/
if(rt==rem[cnt])
{
ans+=pre[cnt]; ans+=rem[cnt];
cnt--;
if(cnt)
{
ans+=pre[cnt];//!!
pre[cnt]++;
}
else rem[++cnt]=0,pre[cnt]=1;
}
else
{
ans+=pre[cnt]; ans+=rt;
rem[cnt]-=rt; pre[cnt]=1;
}
}
}
// printf("i=%d ans=%lld
",i,ans);
}
printf("%lld
",ans);
return 0;
}
D
分析:
又忘记了那个重要的式子: $ (x | y) + (x $ & $ y) = (x+y) $
所以可以问六次得到前三个数的值;知道一个数的值以后就可以 $ 2n $ 次询问把后面 $ n $ 个数都得到。然后排序即可。
如此简单粗暴……
代码如下
#include<iostream>
#include<algorithm>
#define ll long long
using namespace std;
int const N=1e4+5;
int n,k,a[N];
int main()
{
scanf("%d%d",&n,&k);
ll s1,s2,s3,sum,x,y;
puts("or 1 2"); fflush(stdout);
scanf("%lld",&x);
puts("and 1 2"); fflush(stdout);
scanf("%lld",&y);
s1=x+y;
puts("or 2 3"); fflush(stdout);
scanf("%lld",&x);
puts("and 2 3"); fflush(stdout);
scanf("%lld",&y);
s2=x+y;
puts("or 1 3"); fflush(stdout);
scanf("%lld",&x);
puts("and 1 3"); fflush(stdout);
scanf("%lld",&y);
s3=x+y;
sum=(s1+s2+s3)/2;
a[1]=sum-s2; a[2]=sum-s3; a[3]=sum-s1;
for(int i=4;i<=n;i++)
{
printf("or 1 %d
",i); fflush(stdout);
scanf("%lld",&x);
printf("and 1 %d
",i); fflush(stdout);
scanf("%lld",&y);
a[i]=x+y-a[1];
}
sort(a+1,a+n+1);
printf("finish %d
",a[k]);
return 0;
}
E
分析:
巧妙地把这个序列做两个转换:
首先,令 $ c_i = a_i - b_i $
然后,对 $ c_i $ 做前缀和 $ s_i $
于是问题就转化成使 $ s_l $ 到 $ s_r $ 都相等,每次操作可以把一些子段整体 $ +1 $ 。所以去掉不合法情况后,直接输出 $ s_{l-1} - min ( s_l, ..., s_r ) $ 即可!
代码如下
#include<iostream>
#include<cmath>
#define ll long long
using namespace std;
int const N=1e5+5;
int n,q;
ll a[N],s[N],mx[N][20],mn[N][20];
int rd()
{
int ret=0,f=1; char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1; ch=getchar();}
while(ch>='0'&&ch<='9')ret=(ret<<3)+(ret<<1)+ch-'0',ch=getchar();
return ret*f;
}
ll getmn(int l,int r)
{
int k=log2(r-l+1);
return min(mn[l][k],mn[r-(1<<k)+1][k]);
}
ll getmx(int l,int r)
{
int k=log2(r-l+1);
return max(mx[l][k],mx[r-(1<<k)+1][k]);
}
int main()
{
n=rd(); q=rd();
for(int i=1;i<=n;i++)a[i]+=rd();
for(int i=1;i<=n;i++)a[i]-=rd();
for(int i=1;i<=n;i++)
{
s[i]=s[i-1]+a[i];
mx[i][0]=mn[i][0]=s[i];
}
for(int j=1;j<20;j++)
for(int i=1;i<=n&&i+(1<<j)-1<=n;i++)
mx[i][j]=max(mx[i][j-1],mx[i+(1<<(j-1))][j-1]),
mn[i][j]=min(mn[i][j-1],mn[i+(1<<(j-1))][j-1]);
for(int i=1,l,r;i<=q;i++)
{
l=rd(); r=rd();
if(s[r]!=s[l-1]||getmx(l,r)>s[l-1]){puts("-1"); continue;}
printf("%lld
",s[l-1]-getmn(l,r));
}
return 0;
}
F
分析:
容斥!又是美丽的容斥。
设 $ F[X] $ 表示集合 $ X $ 为胜利者。 $ ans = sum F[X] * |X| $
设 $ G[X] $ 表示集合 $ X $ 的子集为胜利者。 $ G[X] = g(X,U-X) $ ,其中 $ U $ 是全集, $ g(A,B) $ 表示 $ A $ 中所有人都直接赢了 $ B $ 中所有人,即 $ g(A,B) = prod_{x in X, y in Y} frac{a_x}{a_x+a_y} $ 。可以想到,如果 $ A $ 中所有人都赢了 $ B $ 中所有人,那么 $ B $ 中的人无论怎样也无法赢回 $ A $ ,所以胜利者一定在 $ A $ 之中。
然后, $ F[X] = G[x] - sum_{T subseteq X} F[T] * g(X-T,U-X) $ 。减去的是在此种情况下, $ X $ 中只有一部分人( $ T $ )是真正的胜利者的概率;“此种情况”即 $ X $ 所有人都赢了 $ U-X $ 所有人,所以对于一个要减去的 $ F[T] $ 来说,它还乘了 $ X-T $ 那部分人也赢了 $ U-X $ 的概率。
还专门想了想 $ F[0] $ 为什么等于 $ 0 $ (虽然没用到)。也就是在这样一个有向完全图中,一定会有点能够走到所有点。可以这样考虑:如果存在一个点能走到 $ n-1 $ 个点,那么它走不到的那个点一定能直接走到它(因为是完全图),进而从它走到其他点;也就是那个点能够走到所有点。如果存在一个点能走到 $ n-2 $ 个点,那么它走不到的两个点一定能直接走到它,也就是它们能走到 $ n-1 $ 个点,再根据之前的讨论,可知存在能走到所有点的点。以此类推,总是会有点至少能走到一个点的,所以总是会有点能够走到所有点。
所以cf的Tutorial公式果然写得有点问题?直接那样写了的我调了好久,才自己写出正确的公式……(Tutorial后来改了……)
计算 $ G[X] $ 可以预处理每个人 $ i $ 对各种集合 $ S $ 的胜率,记为 $ H[i] = prod_{j in S} frac{a_i}{a_i+a_j} $ ;之后求 $ g(X,Y) $ 就可以 $ O(n) $ 枚举 $ X $ 中的元素得到了。Tutorial还进一步给出了 $ O(1) $ 算 $ g(X,Y) $ 的方法,似乎是把一个集合裂成两半……感觉有点麻烦,再者原来那样时间也可以,就没优化了。
时间复杂度 $ O(3^n * n) $ , 其中 $ 3^n $ 是枚举每个数在 $ X $ 中的 $ T $ ,在 $ X $ 中而不在 $ T $ ,还是不在 $ X $ ; $ n $ 是每次计算 $ g(X,Y) $ 的时间。
代码如下
#include<iostream>
#include<cstring>
#define ll long long
using namespace std;
int const N=15,M=(1<<15),md=1e9+7;
int n,a[N],U;
ll ans,H[N][M],F[M];
ll pw(ll x,ll y)
{
ll ret=1,nx=x;
while(y)
{
if(y&1)ret=ret*nx%md;
nx=nx*nx%md;
y>>=1;
}
return ret;
}
ll fr(ll x,ll y){return x*pw(y,md-2)%md;}
void init()
{
for(int i=1;i<=n;i++)
for(int s=0;s<=U;s++)
{
H[i][s]=1;
for(int j=1;j<=n;j++)
if(s&(1<<(j-1)))H[i][s]=H[i][s]*fr(a[i],a[i]+a[j])%md;
// printf("H[%d][%d]=%lld
",i,s,H[i][s]);
}
}
ll getg(int X,int Y)
{
ll ret=1;
for(int i=1;i<=n;i++)
if(X&(1<<(i-1)))ret=ret*H[i][Y]%md;
// printf("g(%d,%d)=%lld
",X,Y,ret);
return ret;
}
ll cal(int X)
{
if(F[X]!=-1)return F[X];
ll ml=0;
for(int T=X;T;T=(T-1)&X)
if(T!=X)ml=(ml+cal(T)*getg(X-T,U-X)%md)%md;
// if(T!=X)ml=(ml+cal(T)*getg(T,X-T)%md)%md;//+
// printf("ml=%lld F[%d]=%lld
",ml,X,(getg(X,U-X)*(1-ml)%md+md)%md);
// return F[X]=(getg(X,U-X)*(1-ml)%md+md)%md;
// printf("ml=%lld F[%d]=%lld
",ml,X,((getg(X,U-X)-ml)%md+md)%md);
return F[X]=((getg(X,U-X)-ml)%md+md)%md;
}
void dfs(int nw,int X,int cnt)
{
if(nw>n)
{
// printf("nw=%d X=%d cnt=%d f=%lld
",nw,X,cnt,F[X]);
ans=(ans+(ll)cnt*F[X]%md)%md;
return;
}
dfs(nw+1,X|(1<<(nw-1)),cnt+1);
dfs(nw+1,X,cnt);
}
int main()
{
scanf("%d",&n); U=(1<<n)-1;
for(int i=1;i<=n;i++)scanf("%d",&a[i]);
init();
memset(F,-1,sizeof F); F[0]=0; cal(U);
dfs(1,0,0);
printf("%lld
",ans);
return 0;
}