Codeforces Round 882 (Div. 2)
题号:CF1847A~F
A
题意:
给定一个数组 \(\{x_1,x_2,\cdots,x_n\}\) 和一个整数 \(k\),记 \(f(l,r)=\sum_{i=0}^{i < r-l} |x_{l+i}-x_{l+i+1}|\),求将数组划分为 \(k\) 个部分的划分方案,使得对每个部分的 \(f(l,r)\) 之和最小.
题解:
简单题,首先我们注意到,如果将 \(l,l+1\) 隔开,那么 \(|x_l-x_{l+1}|\) 这个数就不会计入答案,那么令 \(b_i=|x_i-x_{i+1}|(i<n)\),那么问题就变为从 \(b\) 中选出最大的 \(k\) 个数减掉。方法很多
read(n);read(k);int ans=0;
for(int i=1;i<=n;i++)read(a[i]);
for(int i=1;i<n;i++)b[i]=abs(a[i+1]-a[i]);
sort(b+1,b+n);reverse(b+1,b+n);
for(int i=k;i<n;i++)ans+=b[i];
cout<<ans<<"\n";
B
乔纳森正在与迪奥的吸血鬼手下战斗。其中有 \(n\) 个吸血鬼,它们的强度分别为 \(a_1, a_2,\cdots, a_n\)。
将 \((l,r)\) 表示由索引 \(l\) 到 \(r\) 的吸血鬼组成的一组。乔纳森意识到每个这样的组的强度取决于它们的最弱环节,即按位与操作。更具体地说,组 \((l,r)\) 的强度等于 \(f(l,r) =\) \(a_l \ \& \ a_{l+1} \ \& \ a_{l+2} \ \& \cdots \& \ a_r\)。这里,\(\&\) 表示按位与操作。
乔纳森希望能快速击败这些吸血鬼手下,因此他会将吸血鬼分成连续的组,使得每个吸血鬼正好属于一组,并且这些组的强度之和尽量小。在所有可能的分组方式中,他希望找到组数最多的方式。
给定每个吸血鬼的强度,找出在所有可能的分组方式中,拥有最小强度之和的组的最大数量。
题解:
注意到,\(\&\) 肯定是单调不增的。那么显然最小强度之和必定是整个序列 \(\&\) 的值,故本质上问题是从前往后或从后往前找到若干个拼在一起,\(\&\) 和为0的段。
read(n);int ans=1;
for(int i=1;i<=n;i++)read(a[i]);
k=(1ll<<33ll)-1ll;int cnt=0;
for(int i=1;i<=n;i++){
k&=a[i];
if(k==0){
k=(1ll<<33ll)-1ll;
++cnt;
}
}
ans=max(ans,cnt);
cnt=0,k=(1ll<<33ll)-1ll;
for(int i=n;i;--i){
k&=a[i];
if(!k){
++cnt;k=(1ll<<33ll)-1ll;
}
}
ans=max(ans,cnt);
cout<<ans<<"\n";
C
DIO 意识到星尘十字军已经知道了他的位置,并且即将要来挑战他。为了挫败他们的计划,DIO 要召唤一些替身来迎战。起初,他召唤了 $ n $ 个替身,第 $ i $ 个替身的战斗力为 $ a_i $。依靠他的能力,他可以进行任意次以下操作:
- 设当前的替身数量为 \(m\)。
- DIO 选择一个序号 \(i \text{ } ( 1 \le i \le m )\)。
- 接着,DIO 召唤一个新的替身,其序号为 \(m+1\),战斗力为 \(a_{m + 1} = a_i \oplus a_{i + 1} \oplus \ldots \oplus a_m\)。其中,运算符 \(\oplus\) 表示按位异或。
- 现在,替身总数就变成了 \(m+1\)。
但对于 DIO 来说,不幸的是,星尘十字军通过隐者之紫的占卜能力,已经知道了他在召唤替身迎战的事情,而且他们也知道初始的 \(n\) 个替身的战斗力。现在,请你帮他们算一算 DIO 召唤的替身的最大可能战斗力(指单个替身的战斗力,并非所有替身战斗力之和)。
题解:
手玩一下,容易发现本质上 \(a_x(x>m)\) 无论怎么选,都必定是 \(a_1\sim a_m\) 的某一个子段的异或值。所以就变成了Trie板子题。
D
<
给出一个长度为 \(n\) 的字符串 \(s\),字符串仅由
0
或1
构成。给出 \(m\) 个区间 \([l_i,r_i]\) (\(1\le i\le m\),\(1\le l_i\le r_i\le n\)),你需要将字符串 \(s\) 的子段 \([l_i,r_i]\) 依次拼接,得到新的字符串 \(t\)。
你可以对字符串 \(s\) 进行操作,每次操作可以交换任意两个字符的位置,注意操作不是实际改变,不会影响后续的询问。定义对于字符串 \(s\),\(f(s)\) 表示最小的操作次数,使得拼接得到的新字符串 \(t\) 的字典序最大。
然后有 \(q\) 次询问,每次询问给出一个位置 \(x_i\),表示将原字符串 \(s\) 的 \(x_i\) 位置取反,注意是实际改变,会影响后续的询问。相应的,\(t\) 字符串也会发生改变。你需要求出每次询问后,\(f(s)\) 的值。
题解:
注意到,这个字典序类似于二进制,满足前一个为1比满足后面所有为1更有效。
那么我们可以从优先级的角度考虑问题,优先级怎么求?倒着用线段树做一次区间覆盖即可(也可以正着用并查集做一次区间覆盖
做完区间覆盖后,将所有值按优先级排序(同色的按下标排序)。并且做一个映射使得可以知道原序列每个下标对应的优先级。
然后,我们再考虑维护答案。记当前序列里有 \(cnt\) 个1。让我们想想,可以任意交换两个数,那么本质上就是求优先级在前 \(cnt\) 的有多少个0了。
这一步可以使用线段树维护区间和。线段树维护优先级数组,当然,与原来的线段树一起就可以了。(我不嫌烦地开了个树状数组)
然后我们用 \(cnt-sum(1,cnt)\) 就可以了。
注意WA on 13的原因:当 \(cnt\ge c\),\(c\) 表示需要被设置为1(优先级不为零的部分)的数的个数,这时候需要用 \(c\) 代替 \(cnt\)。也即 \(ans=\min(c,cnt)-sum(1,\min(c,cnt))\)
启发:遇到明显具有优先满足性质的问题,可以考虑求出每个数的优先级,然后进行操作。
#define N 505050
int a[N],L[N],R[N];
struct node{
int l,r,lz;
}t[N<<1];
void build(int x,int l,int r){
t[x].l=l,t[x].r=r;
if(l==r)return ;
int mid=l+r>>1;
build(lc,l,mid);
build(rc,mid+1,r);
}
void pushdown(int x){
if(t[x].lz==0||t[x].l==t[x].r)return ;
t[lc].lz=t[rc].lz=t[x].lz;t[x].lz=0;
}
void change(int x,int l,int r,int k){
if(l<=t[x].l&&t[x].r<=r){
t[x].lz=k;return ;
}
if(t[x].l>r||t[x].r<l)return ;
pushdown(x);
change(lc,l,r,k);
change(rc,l,r,k);
}
int find(int x,int pos){
if(t[x].l==t[x].r)return t[x].lz;
pushdown(x);
if(t[lc].r>=pos)return find(lc,pos);
return find(rc,pos);
}
struct Node{
int id,x;
bool operator<(const Node b){
return x==b.x?id<b.id:x>b.x;
}
}b[N];int c[N],d[N],mx;
void add(int x,int k){
while(x<=n){
d[x]+=k;x+=lowbit(x);
}
}
int ask(int x){
int ans=0;
while(x){
ans+=d[x];x-=lowbit(x);
}
return ans;
}
signed main(){
ios::sync_with_stdio(false);
cin>>n;int m,q;cin>>m>>q;
for(int i=1;i<=n;i++){
char x;cin>>x;a[i]=x-'0';
}
for(int i=1;i<=m;i++)cin>>L[i]>>R[i];
build(1,1,n);
for(int i=m;i;--i)change(1,L[i],R[i],m-i+1);
for(int i=1;i<=n;i++)b[i].x=find(1,i),b[i].id=i,mx+=(b[i].x!=0);
sort(b+1,b+n+1);int ans=0,cnt=0;
for(int i=1;i<=n;i++)c[b[i].id]=i;
for(int i=1;i<=n;i++)cnt+=a[i];
for(int i=1;i<=n;i++)if(a[i])add(c[i],1);
while(q--){
int x;cin>>x;
if(!a[x]){
a[x]=1;++cnt;add(c[x],1);
}
else {
a[x]=0;--cnt;add(c[x],-1);
}
cout<<min(mx,cnt)-ask(min(mx,cnt))<<"\n";
}
}
E
交互题。 \(n\le 5000\),询问次数不超过 \(5500\)。
序列 \(a\) 长为 \(n\),其中 \(1\le a_i\le 4\),每一次询问给出3个 \(i,j,k(i<j<k)\),输入以 \(a_i,a_j,a_k\) 为边长的三角形的面积的平方乘16的结果,特别地,如果构不成三角形,输入0,求这个序列。
题解:
我发现,交互题的核心思想:简化确定步骤 \(\&\) 一次确定多个
注意,有个东西叫海伦-秦九韶公式,也即三角形面积与三边关系:记 \(p=\frac{a+b+c}{2}\),则有 \(S=\sqrt{p(p-a)(p-b)(p-c)}\)。
所以 \(16S^2=(4S)^2=(a+b+c)(a+b-c)(a-b+c)(-a+b+c)\)
故,本质上我们是得到了三边的关系式。注意到对于 \(a_i\) 而言,只有 \(4\) 种不同的取值,故上式得到的结果最多只有 \(4^3=64\) 种。
手玩一下,大胆猜想在 \(1\le a\le b \le c\le 4\) 的情况下,所组成三角形的面积是相异的。
面对这种问题,注意到可能性很小,可以打表证明该结论。经过穷举,我们证明了以上猜想。
这是一个很有用的性质:除了0之外,一个三角形的面积单射三边长。我们考虑处理出每个面积对应的三角形三边长。
void init(){
for(int i=1;i<=4;i++){
for(int j=i;j<=4;j++){
for(int k=j;k<=4;k++){
if(i+j<=k)continue;
int p=(i+j+k)*(i+j-k)*(i+k-j)*(k+j-i);
s[p]=(node){i,j,k};
}
}
}
}
然后,我们来考虑怎么确定这些值。
注意到 \(5500-5000=500\),意即大部分数必须一次确定。观察询问次数与 \(n\) 的关系,从中推敲结论
怎么一次确定一个数呢?有且只有我们已知两个数 \(a_i,a_j\),然后我们去询问第三个数 \(a_k\)。怎样的两个数 \(a_i,a_j\) 满足条件?
容易发现,对于一个等腰三角形而言,除非底边是两腰之和,否则必定存在该三角形。
则我们假定 \(a_i=a_j\ge 3\),则无论什么数都可以一次问出(因为必定可以组成三角形,\(a_k\le 6\))。
那么,再考虑一下 \(a_i=a_j=2\)?容易发现,有且仅有 \(4\) 不可以确定,其实返回0也变相说明可以确定了
所以本质而言,除了 \(a_i=a_j=1\) 之外,其他都行。
我们先假定 \(a_i=a_j>1\),考虑寻找到这样一对相等的 \(a_i=a_j\)。
根据鸽巢原理,在 \(2\times 4+1=9\) 个数中,必然存在至少三个相等的 \(a\) 值
这启发我们,当 \(n\ge 9\) 时,取出 \(a_1\sim a_9\),使用穷举法尝试找到一对 \(a_i=a_j=a_k=x\)。这样做询问次数为 \({9\choose 3}=84\) 次。
若 \(x\ge 2\),则直接全部扫一遍即可。
void get_9(int tag){
for(int i=0;i<f.size();i++)
for(int j=i+1;j<f.size();j++)
for(int k=j+1;k<f.size();k++){
int x=ask(f[i],f[j],f[k]);
vis[i][j][k]=x;
if(x==0)continue;
if(s[x].a==s[x].c&&n>=9&&(tag==0||s[x].a!=1)){
id=s[x].a;X1=f[i],Y1=f[j],Z1=f[k];
ans[X1]=ans[Y1]=ans[Z1]=id;
return ;
}
}
dfs(0,f.size());//等会解释作用
}
//solve:
if(id>=2){
for(int i=1;i<=n;i++){
if(X1==i||Y1==i||Z1==i)continue;
int k=ask(X1,Y1,i);
for(int j=1;j<=4;j++){
if(k==(id+id+j)*(id+id-j)*j*j){
ans[i]=j;break;
}
}
}
cout<<"! ";for(int i=1;i<=n;i++)cout<<ans[i]<<" ";
cout.flush();
}
但 \(1\) 真的不行吗?注意到若 \(a_i=a_j=1\),则若询问 \(a_k\) 回复为1,则说明 \(a_k=1\),否则说明 \(a_k\ge 2\)
这样的话,我们将剩下的可以确定 \(a_k\ge 2\) 的数找出9个,就必定可以得到 \(a_{i'}=a_{j'}=a_{k'}\ge 2\)了,然后扫一遍未确定的数就可以了。
询问次数?除了两次找三个相等 \(a\) 值之外,所有的答案都只经过一次询问就确定了,当然不会超5500
但是,如果找不到呢?
我们将原本确定的三个一也插入进去,以防止找不到,若找不到就执行下面的dfs直接 \(O(4^c)\) 暴力枚举确定这些数。
\(Code:\)
int a[N],vis[9][9][9],ans[N];
vector<int>f;
struct node{
int a,b,c;
}s[N];
int ask(int x,int y,int z){
if(x>y)swap(x,y);
if(x>z)swap(x,z);
if(y>z)swap(y,z);
cout<<"? "<<x<<" "<<y<<' '<<z<<"\n";cout.flush();
int res;cin>>res;return res;
}
void init(){
for(int i=1;i<=4;i++){
for(int j=i;j<=4;j++){
for(int k=j;k<=4;k++){
if(i+j<=k)continue;
int p=(i+j+k)*(i+j-k)*(i+k-j)*(k+j-i);
s[p]=(node){i,j,k};
}
}
}
}
int cnt=0;
bool check(){
// for(int i=0;i<f.size();++i)cout<<a[i]<<" ";cout<<"\n";
for(int i=0;i<f.size();i++)
for(int j=i+1;j<f.size();j++)
for(int k=j+1;k<f.size();k++){
int x=a[f[i]],y=a[f[j]],z=a[f[k]];
if(x+y<=z||x+z<=y||y+z<=x){
if(!vis[i][j][k])continue;
return 0;
}
if(vis[i][j][k]==(x+y+z)*(x+y-z)*(x-y+z)*(-x+y+z))continue;
return 0;
}
for(int i=0;i<f.size();++i)ans[f[i]]=a[f[i]];
return 1;
}
int X1,Y1,Z1,id;
void dfs(int now,int len){
if(cnt>1)return ;
if(now>=len){
cnt+=check();
return ;
}
for(int i=1;i<=4;i++){
a[f[now]]=i;dfs(now+1,len);
}
return ;
}
void get_9(int tag){
for(int i=0;i<f.size();i++)
for(int j=i+1;j<f.size();j++)
for(int k=j+1;k<f.size();k++){
int x=ask(f[i],f[j],f[k]);
vis[i][j][k]=x;
if(x==0)continue;
if(s[x].a==s[x].c&&n>=9&&(tag==0||s[x].a!=1)){
id=s[x].a;X1=f[i],Y1=f[j],Z1=f[k];
ans[X1]=ans[Y1]=ans[Z1]=id;
return ;
}
}
dfs(0,f.size());
}
void repeat(){
f.clear();
for(int i=1;i<=n;i++){
if(X1==i||Y1==i||Z1==i)continue;
if(ask(X1,Y1,i)!=0){
ans[i]=1;
}
else {
f.push_back(i);if(f.size()==9)break;
}
}
f.push_back(X1),f.push_back(Y1),f.push_back(Z1);
cnt=0;
get_9(1);
if(id==1){
if(cnt!=1)cout<<"! -1\n";
else {
cout<<"! ";for(int i=1;i<=n;i++)cout<<ans[i]<<" ";//the old time:f.size()<9,else id must be 2 or 3 or 4
}
return ;
}
for(int i=1;i<=n;i++){
if(X1==i||Y1==i||Z1==i)continue;
int k=ask(X1,Y1,i);
for(int j=1;j<=4;j++){
if(k==(id+id+j)*(id+id-j)*j*j){
ans[i]=j;break;
}
}
}
cout<<"! ";for(int i=1;i<=n;i++)cout<<ans[i]<<" ";cout.flush();
}
void solve(){
for(int i=1;i<=min(9ll,n);i++)f.push_back(i);
if(n<9){
get_9(0);
if(cnt!=1){
cout<<"! -1\n";cout.flush();
}
else{
cout<<"! ";for(int i=1;i<=n;i++)cout<<ans[i]<<" ";
cout.flush();
}
return ;
}
else {
get_9(0);
if(id>=2){
for(int i=1;i<=n;i++){
if(X1==i||Y1==i||Z1==i)continue;
int k=ask(X1,Y1,i);
for(int j=1;j<=4;j++){
if(k==(id+id+j)*(id+id-j)*j*j){
ans[i]=j;break;
}
}
}
cout<<"! ";for(int i=1;i<=n;i++)cout<<ans[i]<<" ";
cout.flush();
}
else repeat();
}
}
signed main(){
ios::sync_with_stdio(false);
cin>>n;
init();
solve();
}
不得不说,代码有点长,有一点点暴力
F
给定一个无穷长的整数数列的前 \(n\) 项 \(a_1,a_2 \dots a_n\),且对于任意 \(i > n\),\(a_i=a_{i-n}|a_{i-n+1}\).
你需要处理 \(q\) 次询问。每次询问给定一个整数 \(x\),请找出最小的 \(i\) 满足 \(a_i>x\)。如果不存在这样的 \(i\),输出
−1
。
这问题更有意思了。w直接喜提次劣解,不懂为什么我单log却被卡得那么惨,时限3000ms,我直接2994ms
对于有规律的递推数列问题,可以考虑写个十几项找规律。
我们假定 \(n=5\),以 \(345\) 表示 \(a_3|a_4|a_5\),容易得到:
如果用DP式来说,就是 \(f_{i,j}=f_{i-1,j}|f_{i-1,j+1}(j<n)\),\(f_{i,j}=f_{i-1,j}|f_{i,1}(j=n)\)
容易看出,所求 \(i<n^2\)。我们关注矩阵的行内,列内关系。容易发现以下规律:
- 每一个 \(a_i\) 都对应原序列的一段区间的或运算值
- 除 \(a_1\) 外,每一个 \(a_i\) 的对应或区间的末尾都不是1
- 若破环为链,则第 \(i\) 列的第 \(j\) 个对应区间或区间 \([i,i+j-[i+j\le n]]\)
回到本题,从序列上发现性质之后,就应该在运算上发现性质。
结合或运算的性质:\(x|x=x\)。对于二元运算 \(f(a,b)\)(不分顺序),凡是满足 \(f(f(a,b),a)=f(f(a,b),b)=f(a,b)\),就说明具有统计可重性(如或运算,最大值运算,最小值运算等),这时候如果是静态使用,则可以通过ST表迅速维护某一区间的运算和,预处理 \(O(n\log n)\),查询单次 \(O(1)\) !
关于或运算还有与运算,都有性质:对于序列 \(b_1\sim b_n\),无论 \(n\) 多大,它的前缀或运算值最多只有 \(\log_2 V\) 个不同的值,前缀与运算值同理。
为什么?因为做前缀或/与运算,只有在二进制上0/1不断变1/0,至多变化 \(\log V\) 次。
推论:对于序列 \(b_1\sim b_n\),\(n\) 无限制,则其中每一个子区间或运算最多只有 \(n\log V\) 个不同的结果,与运算同理。
由此,我们可以考虑求出这些值,然后将这 \(n\log V\) 个数按权值自大到小排序,就可以二分出符合条件的区间。此时再做一个前缀最小值,就可求得最小 \(a_i\)。
for(int i=1;i<=n;i++){
b[++tot]=(node){a[i],i};
solve(i,2,n);
}
sort(b+1,b+tot+1);//运算符重载
for(int i=1;i<=tot;i++)g[i]=min(g[i-1],b[i].id);
while(q--){
read(k);if(k>=b[1].x){
cout<<"-1\n";continue;
}
int x=lower_bound(b+1,b+tot+1,(node){k,0})-b-1;//注意这里有个坑点,比大小只能与k有关,不可以与id有关,否则会挂掉的。
cout<<g[x]<<"\n";
}
然后我们考虑对于每行怎么求出这些值。
其实是简单的,我们可以对于 每一个值都单独倍增/二分一遍就行。但要特判区间右端点不为 \(n+1\),复杂度 \(O(n\log n\log V)\)
这里我在观看题解后采用的一种更为简单的做法:利用线段树的分治思想,(亦或者可以叫整体二分的某个变种),处理一整段区间,对于整个区间值都相同且已经统计过就直接跳过。
void solve(int now,int l,int r){
int mid=l+r>>1,x=get(now,now+mid-1);
if(l==r){
if(b[tot].x!=x&&now+l-1!=n+1){
b[++tot]=(node){x,(l-1-(now+l-1>n+1))*n+now};
}
return ;
}
if(b[tot].x==get(now,now+l-1)&&b[tot].x==get(now,now+r-1))return ;
solve(now,l,mid);solve(now,mid+1,r);
}
简要分析一下复杂度:递归统计每个答案到底层都要 \(\log n\),而会有 \(\log V\) 个到底层,所以复杂度 \(O(\log n\log V)\)。
这样就做完了,复杂度 \(O(n\log n\log V+q\log (n\log V))\)
这种若干次二分化递归解决,是一个不错的办法。
Trick 总结
- D:如果问题中满足某些限制的数明显具有优先级,那么从优先级的角度考虑问题,求出每个数的优先级后再做处理
- D:注意答案的上下界
- E:存在性问题,鸽巢原理
- E:简化确定步骤
- E:考虑单射的结论
- E:大胆猜测,打表求证
- F:对于有规律的递推数列问题,可以考虑写个十几项找规律
- F:找矩阵的规律,关注行列对角线等,以及运算方式
- F:多次倍增跳跃求变化用区间递归实现