GJOI 10.7/10.8 题解
已经进入每天都有模拟赛的阶段了,不过快到 wzr 的出题 round 了。其实两位大佬的题目都“很有意思”呢!
1.BZOJ4927 / LOJ6065 2017山东一轮集训Day3 第一题
题意
给定 nnn 根直的木棍,要从中选出 666 根木棍,满足:能用这 666 根木棍拼出一个正方形。注意木棍不能弯折。问方案数。
n≤5000n \leq 5000n≤5000,1≤ai≤1071 \leq a_i \leq 10 ^ 71≤ai≤107。
思路
先分配每条边的木棍数:1+1+2+21+1+2+21+1+2+2 或 1+1+1+31+1+1+31+1+1+3。
方案不同在于,选的木棍的下标不同,不同下标的木棍长度可能相同,方案内部没有顺序区分。所以我们先对木棍去重并排序,得到 bib_ibi 数组,iii 种木棍各自的数量为 cnticnt_icnti。
对于第一种情况,我们从小到大枚举 111 根边长 bib_ibi。然后枚举 bl+br=bib_l+b_r=b_ibl+br=bi 的每对 (l,r)(l,r)(l,r),解决第一个 222 根边,再与 222 根长度和亦为 bib_ibi 的方案进行配对。
枚举到一对 (l,r)(l,r)(l,r) 后,记 fif_ifi 表示,用 222 根组成 aia_iai 的方案数,理应是 cntl,cntrcnt_l,cnt_rcntl,cntr 中各选一个,再 ×fi\times f_i×fi。
reti←cntl×cntr×firet_i\leftarrow cnt_l\times cnt_r\times f_ireti←cntl×cntr×fi
我们发现 (l,r)(l,r)(l,r) 可能会并入 fif_ifi 被统计,又因为没有顺序区分,于是动态维护 fif_ifi,表示从小到大枚举到 blb_lbl,用 <bl<b_l<bl 的一条边组成和为 bib_ibi 的方案数,然后再单独考虑 (l,r)(l,r)(l,r) 和 (l,r)(l,r)(l,r) 配对的情况,即各选两个相乘。于是可以做到不重复。
reti←(cntl2)×(cntr2)ret_i\leftarrow\binom{cnt_l}{2}\times \binom{cnt_r}{2}reti←(2cntl)×(2cntr)
注意特判 l=rl=rl=r 的情况,这个要一种边里面选 2/42/42/4 个:
reti←(cntl2)×fi+(cntl4),l=rret_i\leftarrow \binom{cnt_l}{2}\times f_i+\binom{cnt_l}{4},l=rreti←(2cntl)×fi+(4cntl),l=r
再实时更新 fif_ifi 即可,因为只要组成一对,所以新增方案数为 l,rl,rl,r 各选一个:
fi←{cntl×cntrcntl×(cntl−1)l≠rl=rf_i\leftarrow \begin{cases} cnt_l\times cnt_r \\ cnt_l\times (cnt_l-1) \end{cases} \begin{align*} l\neq r \\ l=r \end{align*}fi←{cntl×cntrcntl×(cntl−1)l=rl=r
枚举 l,rl,rl,r 可以双指针 O(n)O(n)O(n) 扫,就不用 lower_bound
多一个 log\loglog 了。注意 retiret_ireti 外面乘上 iii 种木棍选 222 个。
for(int i=1;i<=tot;i++)//边长
{if(cnt[i]>=2){ll ret=0;for(int l=1,r=i-1;l<=r;l++)//小边12{//r=lower_bound(b+1,b+i,b[i]-b[l])-b;while(l<=r&&b[l]+b[r]>b[i])r--;if(b[l]+b[r]<b[i])continue;if(l>r)break;//b[l]+b[r]=b[i]ll lc=cnt[l],rc=cnt[r];if(l!=r){ret+=Cx2(lc)*Cx2(rc)+lc*rc*f[i];f[i]+=lc*rc;}else {ret+=Cx4(lc)+Cx2(lc)*f[i];f[i]+=lc*(lc-1);}}ans+=ret*Cx2(cnt[i]);}
}
对于第二种情况,我们依然想要从小到大枚举边长,然后枚举 333 根边其中一边 bjb_jbj。令 gleng_{len}glen 表示,用 222 根木棍组成长度为 lenlenlen 的边的方案数:
reti←1×gbi−bj×(cnti3)ret_i\leftarrow 1\times g_{b_i-b_j}\times \binom{cnt_i}{3}reti←1×gbi−bj×(3cnti)
然后再更新使用 bib_ibi 对 ggg 数组其他长度带来的新贡献,每次只用更新 bib_ibi,因为 bi+1b_{i+1}bi+1 要使用的 gleng_{len}glen 满足 len<bi+1len<b_{i+1}len<bi+1。
gbi+bj←cnti×cntj,j<ig_{b_i+b_j}\leftarrow cnt_i\times cnt_j,j<igbi+bj←cnti×cntj,j<i
但是这样更新会出现一个问题:gbi−bjg_{b_i-b_j}gbi−bj 可能包含使用 bjb_jbj 边的情况,即 bj+(bj+x)=bib_j+(b_j+x)=b_ibj+(bj+x)=bi,gbi−bjg_{b_i-b_j}gbi−bj 包含 bjb_jbj 的 cntjcnt_jcntj 条边匹配 xxx 对应的 cntcntcnt,但是实际应为 cntj−1cnt_j-1cntj−1,而且还会漏掉 bj+(bj+bj)=bib_j+(b_j+b_j)=b_ibj+(bj+bj)=bi 的情况。
(错误示范)
for(int i=1;i<=m;i++)//边长
{if(cnt[i]>=3){for(int j=i-1;j>=1;j--)ans+=cnt[j]*Cx3(cnt[i])*g[b[i]-b[j]];}for(int j=1;j<i;j++)g[b[i]+b[j]]+=cnt[i]*cnt[j];
}
我们发现并不好去重,因为 ggg 不能维护所有的和的方案数,不然用 unordered_map
都开不下 n2n^2n2 个和!
于是考虑摒弃每种边之前匹配的情况,我们单独考虑每一根木棍 aia_iai,作为 333 根边的最长木棍,带来的贡献。枚举比 aia_iai 大的 bjb_jbj 作为边长:
reti′←1×gbj−ai×(cntj3)ret'_i\leftarrow 1\times g_{b_j-a_i}\times \binom{cnt_j}{3}reti′←1×gbj−ai×(3cntj)
因为对于单独一个下标为 iii 的木棍,此时 iii 还未参与 ggg 的方案,因此不会算重。计算完木棍 iii 带来的贡献之后,就更新 ggg 即可。
其实第一种情况也可以用类似的方法去做:先枚举 blb_lbl 再枚举 bib_ibi,lower_bound
得到一个 brb_rbr,
具体细节见代码:
#pragma GCC optimise(2)
#pragma GCC optimise(3,"Ofast","inline")
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=5003,S=1e7+2;
int n,a[N],R;
int tot,b[N],cnt[N],ori[N];
ll f[N];
int g[S];
ll Cx2(ll x)
{if(x<2)return 0;return x*(x-1)/2;
}
ll Cx3(ll x)
{if(x<3)return 0;return x*(x-1)*(x-2)/6;
}
ll Cx4(ll x)
{if(x<4)return 0;return x*(x-1)*(x-2)*(x-3)/24;
}
int main()
{freopen("stick.in","r",stdin);freopen("stick.out","w",stdout);scanf("%d",&n);for(int i=1;i<=n;i++)scanf("%d",&a[i]);sort(a+1,a+n+1);R=a[n];for(int i=1;i<=n;i++){if(a[i]!=a[i-1])b[++tot]=a[i];cnt[tot]++;ori[i]=tot;}ll ans=0;
/* 情况1的第二种写法 for(int l=1;l<=tot;l++){for(int i=l+1;i<=tot;i++){ll r=lower_bound(b+1,b+i,b[i]-b[l])-b;if(b[l]+b[r]!=b[i])continue;if(l>r)continue;if(cnt[i]>=2){ll ret=0;ll lc=cnt[l],rc=cnt[r];if(l!=r){ret+=Cx2(lc)*Cx2(rc)+lc*rc*f[i];f[i]+=lc*rc;}else {ret+=Cx4(lc)+Cx2(lc)*f[i];f[i]+=lc*(lc-1);}ans+=ret*Cx2(cnt[i]);}}}*/for(int i=1;i<=tot;i++)//边长 {if(cnt[i]>=2){ll ret=0;for(int l=1,r=i-1;l<=r;l++)//小边12{//r=lower_bound(b+1,b+i,b[i]-b[l])-b;//必须同推 while(l<=r&&b[l]+b[r]>b[i])r--;if(b[l]+b[r]<b[i])continue;if(l>r)break;//b[l]+b[r]=b[i]ll lc=cnt[l],rc=cnt[r];if(l!=r){ret+=Cx2(lc)*Cx2(rc)+lc*rc*f[i];f[i]+=lc*rc;}else {ret+=Cx4(lc)+Cx2(lc)*f[i];f[i]+=lc*(lc-1);}}ans+=ret*Cx2(cnt[i]);}}
// cout<<ans<<endl;for(int i=1;i<=n;i++)//最长小边1{for(int j=tot;j>ori[i];j--)//边长 ans+=1ll*Cx3(cnt[j])*g[b[j]-a[i]];for(int j=1;j<i;j++)if(a[i]+a[j]<=R)g[a[i]+a[j]]++;}printf("%lld",ans);return 0;
}
2.洛谷 P10299 CCC2024S5 Chocolate Bar Partition
3.洛谷 P9312 EGOI2021 Lanterns / 灯笼
4.洛谷 P5642 人造情感
这后面三题,第二题实在难理解,后两题两道大黑呢qwq。接下来是 GJOI 10.8.
1.AT_arc132_d Between Two Binary Strings
题意
给定 n,mn,mn,m,以及两个由 nnn 个 0
和 mmm 个 1
组成的字符串 s1,s2s_1,s_2s1,s2。记 f(s1,s2)f(s_1,s_2)f(s1,s2) 表示每次交换 s1s_1s1 的两个相邻字符,变成 s2s_2s2 的最少交换次数。
对于长度为 n+mn+mn+m 的字符串 sss,记 g(s)=∑i=1n+m−1[si=si+1]g(s)=\displaystyle\sum_{i=1}^{n+m-1}[s_i=s_{i+1}]g(s)=i=1∑n+m−1[si=si+1],即 sss 相邻字符相同的对数。求最大的 g(s3)g(s_3)g(s3),满足 f(s1,s2)=f(s1,s3)+f(s3,s2)f(s_1,s_2)=f(s_1,s_3)+f(s_3,s_2)f(s1,s2)=f(s1,s3)+f(s3,s2)。
思路
虽然计算 fff 看起来比较经典,但是我本身是不会的,还是赛时大佬一眼秒的性质。
先考虑计算 f(s1,s2)f(s_1,s_2)f(s1,s2),举个例子:
n=4,m=5
s1 = 111000110
s2 = 011011001
首先我们不会交换相邻的 0/10/10/1,因为是无用的。我们想要把 s1s_1s1 第 111 个 1
从下标 111 换到下标 222 去。这里我们需要把下标 444 的 0
换到下标 111 去,我们发现第 1∼31\sim 31∼3 个 1
距离各自的 111 都少 111 的单位,而 0
恰好换了 333 次。
于是这个结论是:s1,s2s_1,s_2s1,s2 的第 iii 个 1
的下标之差的绝对值之和。如上述例子 f(s1,s2)=1+1+2+1+1=6f(s_1,s_2)=1+1+2+1+1=6f(s1,s2)=1+1+2+1+1=6。
由此我们可以确定 s3s_3s3 的形态:s3s_3s3 是 s1s_1s1 向 s2s_2s2 转化过程中的某个字符串。s3s_3s3 的第 iii 个 1
,应恰在 s1,s2s_1,s_2s1,s2 的第 iii 个 1
之间。
于是就产生了,s3s_3s3 的第 iii 个 1
的放置区间 [l,r][l,r][l,r],问题转化为如何放置 1
使得连续 1
的长度尽可能长(相同的数尽量放在一起)。
因为有了超速检测的经验教训,我们想到右端点排序(本题区间的左右端点天然有序),然后尽量把点往右放——尽可能进入后面的区间,使得可以接续。
那第一个 1
自然根据这个经典的贪心,填到右端点去。
ll sol(ll st)
{ll las;memset(b,0,sizeof(b));b[las=st]=1;for(int i=2;i<=m;i++){if(s3[i].l<=las+1&&las+1<=s3[i].r)b[++las]=1;else b[las=s3[i].r]=1;}ll ret=0;for(int i=1;i<o;i++)ret+=(b[i]==b[i+1]);return ret;
}
...
ans=max(ans,sol(s3[1].r));//s3第1段1的右端点
但是我们发现一个反例:
n=4,m=3
s1 = 0100011
s2 = 1000101
按照上面的贪心,第 111 个 1
要填到下标 222 去。但是这个 1
不仅后面没有 1
和它相连,而且还切断了原本较长的一段 0
:
s3 = 0100011 //原贪心
s3 = 1000011 //实际最优
于是我们应该防止 1
把 0
切断,即把 1
扔到下标 1
去,即多一个判断从第 111 区间左端点的情况(实际只需 s3[1].l==1
时)。
为了保险,我还正反都做了一边。
代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=6e5+9;
ll n,m,o;
char s1[N],s2[N];
ll p1[N],p2[N],t1,t2;
struct node
{ll l,r;
}s3[N],s3r[N];
bool b[N];
ll sol(ll st)
{ll las;memset(b,0,sizeof(b));b[las=st]=1;for(int i=2;i<=m;i++){if(s3[i].l<=las+1&&las+1<=s3[i].r)b[++las]=1;else b[las=s3[i].r]=1;}ll ret=0;for(int i=1;i<o;i++)ret+=(b[i]==b[i+1]);return ret;
}
int main()
{freopen("magic.in","r",stdin);freopen("magic.out","w",stdout);scanf("%lld%lld",&n,&m);o=n+m;scanf("%s%s",s1+1,s2+1);for(int i=1;i<=o;i++){if(s1[i]=='1')p1[++t1]=i;if(s2[i]=='1')p2[++t2]=i;}for(int i=1;i<=m;i++)s3[i]=(node){min(p1[i],p2[i]),max(p1[i],p2[i])};ll ans=0;ans=max(ans,max(sol(s3[1].l),sol(s3[1].r)));
// if(s3[2].l<=s3[1].r&&s3[1].l<s3[2].l)ans=max(ans,sol(s3[2].l-1));//左端点,实则不优 for(int i=1;i<=m;i++){s3r[i].l=o-s3[i].l+1;s3r[i].r=o-s3[i].r+1;}for(int i=1;i<=m;i++)s3[i]=s3r[m-i+1];ans=max(ans,max(sol(s3[1].l),sol(s3[1].r)));
// if(s3[2].l<=s3[1].r&&s3[1].l<s3[2].l)ans=max(ans,sol(s3[2].l-1));//左端点,实则不优 printf("%lld",ans);return 0;
}
2.P6294 eJOI 2017 游戏
题意
3≤n≤1053\le n\le 10^53≤n≤105,1≤m≤1031\le m\le 10^31≤m≤103,ai∈[1,109]a_i\in[1,10^9]ai∈[1,109]。
洛谷 P6294 题意:Alice 的总和减去 Bob 的总和。
思路
赛时 O(nmlogn)O(nm\log n)O(nmlogn) 写堆维护,狂写 64pts。没想到正解就是在暴力的基础上小优化一下。
设已经加入的数的多重有序集为 SSS,当前新加入 aia_iai。若 ai≥Smaxa_i\ge S_{max}ai≥Smax,aia_iai 会被直接拿掉,是不用加入排序的;否则就加入 SSS。等到加完数的时候,或者没法把新加入的数拿走,就降序遍历 SSS,拿走 SSS 的元素。
如果我们维护一个指向 SmaxS_{max}Smax 的指针 curcurcur(SSS 的最后一个元素),我们发现 curcurcur 不会增只会降。因为新加入的数比它大就会直接拿走,curcurcur 不会往大了跑。
具体地,我们将原数组离散化,将离散化后的 a1∼p−1a_{1\sim p-1}a1∼p−1 加入桶,cur=max{ai}cur=\max\{a_i\}cur=max{ai} 作为桶上存在 >0>0>0 的最大值。依次加入 i∈[p,n]i\in[p,n]i∈[p,n],如果 ai≥cura_i\ge curai≥cur 直接拿走。
否则把 aia_iai 加入桶。然后在桶上,往小移动指针 curcurcur 直到出现出现次数 >0>0>0 的就是当前最大值了。
代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=1e5+9;
ll n,Q;
ll a[N];
ll aa[N],nn;
ll cnt[N];
int main()
{freopen("game.in","r",stdin);freopen("game.out","w",stdout);scanf("%lld%lld",&n,&Q);for(int i=1;i<=n;i++){scanf("%lld",&a[i]);aa[i]=a[i];}sort(aa+1,aa+n+1);nn=unique(aa+1,aa+n+1)-aa-1;for(int i=1;i<=n;i++)a[i]=lower_bound(aa+1,aa+nn+1,a[i])-aa;while(Q--){ll p,sa=0,sb=0,cur=0;scanf("%lld",&p);for(int i=1;i<=p;i++){cur=max(cur,a[i]);cnt[a[i]]++;}sa+=aa[cur];cnt[cur]--;for(int i=2;i<=n;i++){if(i+p-1<=n)//加数 {cnt[a[i+p-1]]++;if(a[i+p-1]>=cur){if(i&1)sa+=aa[a[i+p-1]];else sb+=aa[a[i+p-1]];cnt[a[i+p-1]]--;continue;}}while(!cnt[cur])cur--;if(i&1)sa+=aa[cur];else sb+=aa[cur];cnt[cur]--;}printf("%lld\n",sa);}return 0;
}
洛谷题目代码敬请自己修改。
3.AT_arc119_f AtCoder Express 3
题意
2≤n≤40002\le n\le 40002≤n≤4000,1≤k≤n1\le k\le n1≤k≤n。
AT_arc119_f:nnn 为当前题面 +1+1+1。本篇博客按照 nnn 比原题面多 111 的数据来讲解。
思路
考虑平推每一位,分别判断 A
、B
和 ?
填哪一个。
因为这里是计数,所以维护到每一位最短距离没什么用,于是把距离放到状态去。又因为一个点肯定会从距离其最近的白点或黑点转移过来,所以设 fi,j,k,opf_{i,j,k,op}fi,j,k,op 表示,走到第 iii 个位置,op=0/1op=0/1op=0/1 表示填白 / 黑,距离起点最远的白点 / 黑点的(最短)距离分别为 j/kj/kj/k 的方案数。
先看路径两个维度的变化,假若第 iii 位填白色:
- 若 i−1i-1i−1 为白色:
- 最远白点距离为 j+1j+1j+1(均指最短距离,下同);
- 最远黑点距离不变;
- 若 i−1i-1i−1 为黑色:
- 最远白点可以从上一块白色走过来:j+1j+1j+1,也可以隔壁黑色走一步过来,即 min(j+1,k+1)\min(j+1,k+1)min(j+1,k+1);
- 最远黑点距离 kkk 就在 i−1i-1i−1,对应上一状态,但是现在多了一个白点在后面,其实可以从 iii 走回来即 min(j+2,k)\min(j+2,k)min(j+2,k)。
所以最终有:
{fi,j+1,k,0←fi−1,j,k,0fi,min(j+1,k+1),min(j+2,k),0←fi−1,j,k,1si=A/?\begin{cases} f_{i,j+1,k,0}\leftarrow f_{i-1,j,k,0} \\ f_{i,\min(j+1,k+1),\min(j+2,k),0}\leftarrow f_{i-1,j,k,1} \end{cases}\begin{align*} s_i=\text{A/?} \end{align*}{fi,j+1,k,0←fi−1,j,k,0fi,min(j+1,k+1),min(j+2,k),0←fi−1,j,k,1si=A/?
同理有第 iii 位填黑色的转移:
{fi,j,k+1,1←fi−1,j,k,1fi,min(j,k+2),min(j+1,k+1),1←fi−1,j,k,0si=B/?\begin{cases} f_{i,j,k+1,1}\leftarrow f_{i-1,j,k,1} \\ f_{i,\min(j,k+2),\min(j+1,k+1),1}\leftarrow f_{i-1,j,k,0} \end{cases}\begin{align*} s_i=\text{B/?} \end{align*}{fi,j,k+1,1←fi−1,j,k,1fi,min(j,k+2),min(j+1,k+1),1←fi−1,j,k,0si=B/?
最后答案就是 ∑j=0m+2∑k=j−2j+2fn−1,j,k−j,0/1×[min(j,k)+1≤m]\displaystyle\sum_{j=0}^{m+2}\sum _{k=j-2}^{j+2}f_{n-1,j,k-j,0/1}\times [\min(j,k)+1\le m]j=0∑m+2k=j−2∑j+2fn−1,j,k−j,0/1×[min(j,k)+1≤m]。jjj 都要顶到 m+1m+1m+1 给 k=j−2=m−1k=j-2=m-1k=j−2=m−1 预留。
但是这样转移是时空复杂度 O(nm2)O(nm^2)O(nm2) 的,我们发现两个距离维度有点冗余了,考虑优化状态。
对于 jjj:设 iii 为白,那么 jjj 应当 ≥k−1\ge k-1≥k−1,因为最远黑点的 kkk,会有和 iii 同一块白色的第一个白色点 +1+1+1 更新回去,极限情况就是 iii 是第一个白色点。
如果 jjj 比 kkk 大很多,就不太会用 jjj 来参与两个距离维度的更新,于是 jjj 又会有上界。又若 j>k+2j>k+2j>k+2,填一个黑就会在 min(j,k+2)\min(j,k+2)min(j,k+2),jjj 被“淘汰掉”。
反过来思考的 kkk 的上下界亦然,k≥j−1k\ge j-1k≥j−1,k>j+2k>j+2k>j+2,于是 ∣j−k∣≤2|j-k|\le 2∣j−k∣≤2,即 k−j∈[−2,2]k-j\in[-2,2]k−j∈[−2,2]。我们将 kkk 那一维改成 k−jk-jk−j 差值,就可以消掉一个 O(m)O(m)O(m)。于是时空复杂度 O(4nm)O(4nm)O(4nm)。
代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=4003,mod=1e9+7,D=2;
ll n,m;
char s[N];
ll f[N][N][6][2];
void upd(ll i,ll j,ll k,ll op,ll ad)
{j=min(j,k+2);k=min(k,j+2);f[i][j][k-j+D][op]=(f[i][j][k-j+D][op]+ad)%mod;
}
int main()
{
// freopen("path.in","r",stdin);
// freopen("path.out","w",stdout);scanf("%lld%lld%s",&n,&m,s+1);f[0][0][0+D][0]=1;
// n++;for(int i=1;i<n;i++){for(int j=0;j<=m+1;j++){for(int k=j-2;k<=j+2;k++){if(s[i]!='B'){upd(i,j+1,k,0,f[i-1][j][k-j+D][0]); upd(i,min(j+1,k+1),min(j+2,k),0,f[i-1][j][k-j+D][1]);}if(s[i]!='A'){upd(i,j,k+1,1,f[i-1][j][k-j+D][1]);upd(i,min(j,k+2),min(j+1,k+1),1,f[i-1][j][k-j+D][0]);}}}}ll ans=0;for(int j=0;j<=m+1;j++){for(int k=j-2;k<=j+2;k++){if(min(j+1,k+1)<=m){ans=(ans+f[n-1][j][k-j+D][0])%mod;ans=(ans+f[n-1][j][k-j+D][1])%mod;}}}printf("%lld",ans);return 0;
}
南海云课堂处题面敬请自行修改。