HNOI2017 Day2

2023-06-10

注:Day2T2换为BJOI2017Day2T1,以匹配学习进度


A 大佬

你需要用 \(n\) 天挑战一名大佬。大佬的自信值为 \(C\)。你的目标是在 \(n\) 天内使大佬的自信值恰好为 \(0\)。

用 \(\mathrm{mc}\) 来表示你的自信值上限。在第 \(i \ (i\ge 1)\) 天,大佬会对你发动一次嘲讽,使你的自信值减小 \(a_i\),如果这个时刻你的自信值小于 \(0\) 了,那么你就失败了。否则,你能且仅能选择如下的行为之一:

  1. 还一句嘴,大佬的自信值 \(C\) 减小 \(1\)。
  2. 做一天的水题,使得自己的当前自信值增加 \(w_i\),如果超过 \(\mathrm{mc}\) 就变为 \(\mathrm{mc}\)
  3. 让自己的等级值 \(L\) 加 \(1\)。
  4. 让自己的讽刺能力 \(F\) 乘以自己当前等级 \(L\),使讽刺能力 \(F\) 更新为 \(F\cdot L\)。
  5. 怼大佬,让大佬的自信值 \(C\) 减小 \(F\)。并在怼完大佬之后,你自己的等级 \(L\) 自动降为 \(0\),讽刺能力 \(F\) 降为 \(1\)。这个操作只能做不超过两次。

注意,在任何时候,不能大佬的自信值为负。在第 \(1\) 天被攻击之前,你的自信值等于 \(\mathrm{mc}\),讽刺能力 \(F\) 是 \(1\),等级 \(L\) 是 \(0\)。

一共有 \(m\) 个大佬,他们的嘲讽时间都是 \(n\) 天,而且第 \(i\) 天的嘲讽值都是 \(a_i\)。不管和哪个大佬较量,你在第 \(i\) 天做水题的自信回涨都是 \(w_i\)。对每个大佬,问能否战胜。

\(1\le n,\mathrm{mc},a_i,w_i \le 100,1\le m \le 20,1 \le C \le 10^8。\)


tag:DP,枚举

首先,注意到提升自己的自信值与降低大佬的自信值相互独立,所以可以考虑先求出最多能够花多少天来攻击大佬。这只需要做一个简单的DP,状态为天数和自信值。

然后,考虑操作“怼大佬”。预处理出怼大佬所有可能的“伤害”,这样的数不超过 \(10^8\),所有素因子不超过 \(100\),数目并不太多,约为 \(9.2 \times 10^5\)。可以直接dfs。

接下来,求出每个“伤害”所需要的步数,这也可以DP。将一个数拆成若干个数的积,枚举最大值,不断更新拼出某个数的最少因数个数即可。

对于每个询问,先用不怼大佬和怼一次大佬更新答案。怼两次大佬的情况,先枚举第一次怼的伤害,然后在大佬的剩余自信值附近(距离不超过100)枚举第二次伤害,更新最小回合数。比较最小回合数与可用回合数即得答案。

难度Easy+,成功场切。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n,m,mc,a[105],w[105],ans,dp[105][105],cnt;
int p[30]={0,2,3,5,7,11,13,17,19,23,29,31,37,41,43,47
,53,59,61,67,71,73,79,83,89,97,101};
vector<int> val;
void dfs(int x,int f,int op){
if(op)val.push_back(f);
if(p[x]>n)return;
dfs(x+1,f,0);
if(f<1e8/p[x])dfs(x,f*p[x],1);
}
int len,f[1000005],g[1000005];
void init(){
dfs(1,1,1);
sort(val.begin(),val.end());
memset(f,0x3f,sizeof(f));
memset(g,0x3f,sizeof(g));
f[0]=g[0]=0;len=val.size();
for(int i=2;i<=n;++i)
for(int j=0;j<len;++j)if(val[j]%i==0){
int p=lower_bound(val.begin(),val.end(),val[j]/i)-val.begin();
f[j]=min(f[j],f[p]+1);g[j]=min(g[j],f[j]+i);
}
for(int i=0;i<len;++i)g[i]=g[i]-val[i]+1;
}
void solve(){
memset(dp,-0x3f,sizeof(dp));
dp[0][mc]=0;
for(int i=1;i<=n;i++)
for(int j=a[i];j<=mc;j++){
dp[i][j-a[i]]=max(dp[i][j-a[i]],dp[i-1][j]+1);
int c=min(j-a[i]+w[i],mc);
dp[i][c]=max(dp[i][c],dp[i-1][j]);
}
for(int i=1;i<=n;i++)
for(int j=0;j<=mc;j++)
cnt=max(cnt,dp[i][j]);
}
int main(){
ios::sync_with_stdio(false);
cin>>n>>m>>mc;
for(int i=1;i<=n;i++)cin>>a[i];
for(int i=1;i<=n;i++)cin>>w[i];
init();solve();
for(int i=1,c;i<=m;i++){
cin>>c;ans=c;
for(int j=0;j<len;j++)if(val[j]<=c){
ans=min(ans,g[j]+c);
int x=lower_bound(val.begin(),val.end(),c-val[j]-100)-val.begin(),
y=lower_bound(val.begin(),val.end(),c-val[j]+1)-val.begin();
for(int k=x;k<y;k++)ans=min(ans,g[j]+g[k]+c);
}
printf(ans<=cnt?"1\n":"0\n");
}
return 0;
}

B 抛硬币

一枚均匀的硬币,小A抛\(a\)次,小B抛\(b\)次,求小A抛出正面的次数大于小B的情况数。

\(1\le a,b\le 10^{15},b\le a \le b+10000\)。


tag:组合恒等式,exLucas

数学题,直接推式子,第三个等号用到了范德蒙恒等式。

\[ans=\sum_{i=1}^a \sum_{j=0}^{i-1} C_a^iC_b^j=\sum_{k=1}^{a} \sum_{i} C_a^{a-i}C_b^{i-k}=\sum_{k=1}^{a}C_{a+b}^{a-k}=2^{a+b-1}-\frac{1}{2} \cdot \sum_{k=b+1}^{a-1} C_{a+b}^{k}
\]

用扩展lucas定理求后面的一堆组合数即可。

难度:Medium-,考场exLucas没有实现好,丢了20分。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll a,b,ans,k,c2[1025],c5[1953126],r1,r2,x2,x5,d2,d5,pwr[15][15];string res;
const ll m1=1024,m2=1953125,l1=787109376,l2=212890625,val=976563;
inline void exgcd(ll a,ll b,ll&x,ll&y){
if(b==0){x=1;y=0;return;}
exgcd(b,a%b,x,y);ll x0=x,y0=y;
x=y0;y=x0-a/b*y0;
}
inline ll power(ll a,ll b,ll mod){
register ll c=1;
for(;b;b>>=1){
if(b&1)c=c*a%mod;
a=a*a%mod;
}
return c;
}
inline void f(ll n,ll p,ll k,ll&x,ll&y){
register ll m,c,cnt=0;
if(p==2)m=m1,c=c2[m];
else m=m2,c=c5[m];
while(n){
register ll q=n/m,r=n-m*q;cnt+=q;
x=x*(p==2?c2[r]:c5[r])%m;
n/=p;y+=n;
}x=x*power(c,cnt,m)%m;
}
inline ll C(ll n,ll m,ll p,ll k){
register ll x,y=1,z=1,t,d,e=0,h=0;
if(p==2)x=x2,d=d2;else x=x5,d=d5;
f(m,p,k,y,e);f(n-m,p,k,z,h);d-=e+h;
if(d>=k)return 0;t=pwr[p][k-d];
register ll r,s,u,v;exgcd(y,t,r,s);exgcd(z,t,u,v);
r=(r%t+t)%t;u=(u%t+t)%t;
t=x*r%t*u%t;t=t*pwr[p][d];
return t;
}
int main(){
c2[0]=c5[0]=pwr[2][0]=pwr[5][0]=1;
for(int i=1;i<=10;i++)pwr[2][i]=2*pwr[2][i-1],pwr[5][i]=5*pwr[5][i-1];
for(register int i=1;i<=1024;++i)
if(i%2!=0)c2[i]=1ll*c2[i-1]*i%m1;else c2[i]=c2[i-1];
for(register int i=1;i<=1953125;++i)
if(i%5!=0)c5[i]=1ll*c5[i-1]*i%m2;else c5[i]=c5[i-1];
ios::sync_with_stdio(false);
while(cin>>a>>b>>k){
// double t1=clock()*1000/CLOCKS_PER_SEC;
r1=power(2,a+b,m1);r2=power(2,a+b,m2);
ans=0;res="";x2=x5=1;d2=d5=0;
f(a+b,2,10,x2,d2);f(a+b,5,9,x5,d5);
if(a==b){
r1=(r1-C(a*2,a,2,10)+m1)%m1;
r2=(r2-C(a*2,a,5,9)+m2)%m2;
}
else if(a>b+1){
ll d=(a+b+1)/2;
for(register ll m=b+1;m<=d;++m){
register ll x=C(a+b,m,2,10),y=C(a+b,m,5,9);
r1=(r1+x)%m1;r2=(r2+y)%m2;
if(a+b-m>d&&a+b-m<a)r1=(r1+x)%m1,r2=(r2+y)%m2;
}
}
ans=r1/2*l2+r2*val%m2*l1;
while(k--){res=char(ans%10+'0')+res;ans/=10;}
cout<<res<<endl;
// double t2=clock()*1000/CLOCKS_PER_SEC;
// cout<<(t2-t1)<<"ms\n";
}
return 0;
}

C 喷式水战改

一堆玩意儿,每个玩意儿有三个状态0,1,2,每个状态有一个权值。维护这些玩意儿构成的序列,支持在某个位置插入若干个,以及询问:将序列分为4段,每段状态相同,依次为0,1,2,0,总权值和的最大值。

操作数 \(\le 10^5\),权值 \(\le 10^4\),每次插入数目 \(\le 10^9\)。


tag:Splay

如果每次只插入一个,那么可以很容易用Splay维护。每个结点维护8种分割的最大值,即目标分割的8个子串。

现在每次插入多个,只要在插入时(如果需要)拆点即可。

难度Medium,考场接近做出,因为Splay刚学印象深。但其实也挺板的?

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=4e5+5;
int n;ll ans,lstans;
int rt,tot,fa[N],ch[N][2];
ll siz[N],cnt[N],val[N][3],sum[N][9];
void init(){rt=1;tot=2;cnt[1]=cnt[2]=1;siz[1]=2;siz[2]=1;fa[2]=1;ch[1][1]=2;}
ll max(ll a,ll b,ll c){return max(max(a,b),c);}
ll max(ll a,ll b,ll c,ll d){return max(max(a,b,c),d);}
void push_up(int p){
int lc=ch[p][0],rc=ch[p][1];
siz[p]=siz[lc]+siz[rc]+cnt[p];
for(int op=0;op<3;op++)
sum[p][op]=sum[lc][op]+val[p][op]*cnt[p]+sum[rc][op];
for(int op=3;op<6;op++)
sum[p][op]=max(sum[lc][op]+val[p][(op+1)%3]*cnt[p]+sum[rc][(op+1)%3],
sum[lc][op%3]+val[p][op%3]*cnt[p]+sum[rc][op]);
sum[p][6]=max(sum[lc][6]+val[p][2]*cnt[p]+sum[rc][2],
sum[lc][3]+val[p][1]*cnt[p]+sum[rc][4],
sum[lc][0]+val[p][0]*cnt[p]+sum[rc][6]);
sum[p][7]=max(sum[lc][7]+val[p][0]*cnt[p]+sum[rc][0],
sum[lc][4]+val[p][2]*cnt[p]+sum[rc][5],
sum[lc][1]+val[p][1]*cnt[p]+sum[rc][7]);
sum[p][8]=max(sum[lc][8]+val[p][0]*cnt[p]+sum[rc][0],
sum[lc][6]+val[p][2]*cnt[p]+sum[rc][5],
sum[lc][3]+val[p][1]*cnt[p]+sum[rc][7],
sum[lc][0]+val[p][0]*cnt[p]+sum[rc][8]);
}
bool get(int p){return p==ch[fa[p]][1];}
void rotate(int x){
int y=fa[x],z=fa[y],op=get(x)^1;
ch[y][op^1]=ch[x][op];if(ch[x][op])fa[ch[x][op]]=y;
ch[x][op]=y;fa[y]=x;fa[x]=z;if(z)ch[z][y==ch[z][1]]=x;
push_up(y);push_up(x);
}
void splay(int x,int goal=0){
for(int p=fa[x];p!=goal;p=fa[x]){
if(fa[p]!=goal)rotate(get(p)==get(x)?p:x);
rotate(x);
}if(!goal)rt=x;
}
int kth(ll&k){
int p=rt;
while(1){
if(siz[ch[p][0]]>=k)p=ch[p][0];
else{k-=siz[ch[p][0]]+cnt[p];if(k<=0)return p;p=ch[p][1];}
}
}
void ins(ll x,ll a,ll b,ll c,ll m){
ll k=x+1;int p=kth(k),q;
if(k==0){
k=x+2;q=kth(k);splay(p);splay(q,p);
ch[q][0]=++tot;fa[tot]=q;cnt[tot]=m;
val[tot][0]=a;val[tot][1]=b;val[tot][2]=c;
push_up(tot);push_up(q);push_up(p);
}
else{
ll l=x+1-k-cnt[p];q=kth(l);splay(q);splay(p,q);
int y=ch[p][0]=++tot;fa[y]=p;cnt[y]=m;
val[y][0]=a;val[y][1]=b;val[y][2]=c;
ch[y][0]=++tot;fa[tot]=y;cnt[tot]=k+cnt[p];cnt[p]=-k;
val[tot][0]=val[p][0];val[tot][1]=val[p][1];val[tot][2]=val[p][2];
push_up(tot);push_up(y);push_up(p);push_up(q);
}
}
int main(){
scanf("%d",&n);
init();
for(int i=1;i<=n;i++){
ll p,a,b,c,x;
scanf("%lld%lld%lld%lld%lld",&p,&a,&b,&c,&x);
ins(p,a,b,c,x);
splay(1);splay(2,1);ans=sum[ch[2][0]][8];
printf("%lld\n",ans-lstans);
lstans=ans;
}
return 0;
}

HNOI2017 Day1

2023-06-09


A 单旋

考虑一棵单旋实现的splay,支持以下操作:插入;单旋最小值;单旋最大值;单旋并删除最小值;单旋并删除最大值。其中单旋表示将点转到根。每次操作的代价是操作的点的深度(插入后或单旋前)。\(m\)次操作,求每次操作的代价。

\(m \le 10^5\),所有结点的关键码互不相同。


tag:Splay

事实上这个题肯定不能用Splay做,但分析这样一棵Splay的性质还是很关键的。

模拟几个操作可以发现,单旋最值其实只是把最值换到根,然后最值的儿子(唯一)代替它原来的位置。那么它对深度的影响就非常明确:对于最小值\(u\),设它的父亲权值是\(v\),那么单旋使\(u\)的深度变为\(1\),\(v,...,n\)的深度+1;删除使\(u,...,v-1\)的深度-1。

我们可以把所有点权离散化,然后在线段树上修改。对于不在树上的点,可以一并修改,只要插入的时候用赋值方式即可。

至于插入操作,容易发现插入一个点,要么成为它前驱的右儿子,要么成为它后继的左儿子,且应选取二者中深度较大的一个。用一个set维护在树上的点权即可。

难度Medium,难点在于插入。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n,m,q[N][2],x[N],rt,fa[N],ch[N][2];
struct SegmentTree{
int tag[N<<2];
#define mid ((l+r)>>1)
void modify(int p,int l,int r,int L,int R,int v){
if(l>=L&&r<=R){tag[p]+=v;return;}
if(L<=mid)modify(p<<1,l,mid,L,R,v);
if(R>mid)modify(p<<1|1,mid+1,r,L,R,v);
}
int query(int p,int l,int r,int x){
if(l==r)return tag[p];
if(x<=mid)return query(p<<1,l,mid,x)+tag[p];
else return query(p<<1|1,mid+1,r,x)+tag[p];
}
#undef mid
}seg;
set<int> S;
int main(){
scanf("%d",&m);
for(int i=1;i<=m;i++){
scanf("%d",&q[i][0]);
if(q[i][0]==1){
scanf("%d",&q[i][1]);
x[++n]=q[i][1];
}
}
sort(x+1,x+n+1);
for(int i=1;i<=m;i++){
int op=q[i][0];
if(op==1){
int v=lower_bound(x+1,x+n+1,q[i][1])-x,res;
if(S.empty()){rt=v;res=1;}
else{
auto it=S.lower_bound(v);
if(it==S.end()){
int u=*--it;ch[u][1]=v;fa[v]=u;
res=seg.query(1,1,n,u)+1;
}
else if(it==S.begin()){
int u=*it;ch[u][0]=v;fa[v]=u;
res=seg.query(1,1,n,u)+1;
}
else{
int u1=*it,u2=*--it;
int du1=seg.query(1,1,n,u1),
du2=seg.query(1,1,n,u2);
if(du1>du2)ch[u1][0]=v,fa[v]=u1,res=du1+1;
else ch[u2][1]=v,fa[v]=u2,res=du2+1;
}
}
S.insert(v);
int dv=seg.query(1,1,n,v);
seg.modify(1,1,n,v,v,res-dv);
printf("%d\n",res);
}
if(op==2){
int v=*S.begin(),res;
printf("%d\n",res=seg.query(1,1,n,v));
seg.modify(1,1,n,v,v,1-res);
if(fa[v]){
seg.modify(1,1,n,fa[v],n,1);
ch[fa[v]][0]=ch[v][1];if(ch[v][1])fa[ch[v][1]]=fa[v];
ch[v][1]=rt;fa[rt]=v;fa[v]=0;rt=v;
}
}
if(op==3){
int v=*--S.end(),res;
printf("%d\n",res=seg.query(1,1,n,v));
seg.modify(1,1,n,v,v,1-res);
if(fa[v]){
seg.modify(1,1,n,1,fa[v],1);
ch[fa[v]][1]=ch[v][0];if(ch[v][0])fa[ch[v][0]]=fa[v];
ch[v][0]=rt;fa[rt]=v;fa[v]=0;rt=v;
}
}
if(op==4){
int v=*S.begin(),res;
printf("%d\n",res=seg.query(1,1,n,v));
seg.modify(1,1,n,v,!fa[v]?n:fa[v]-1,-1);
if(fa[v]){ch[fa[v]][0]=ch[v][1];if(ch[v][1])fa[ch[v][1]]=fa[v];}
else{rt=ch[v][1];ch[v][1]=fa[rt]=0;}
S.erase(S.begin());
}
if(op==5){
int v=*--S.end(),res;
printf("%d\n",res=seg.query(1,1,n,v));
seg.modify(1,1,n,fa[v]+1,v,-1);
if(fa[v]){ch[fa[v]][1]=ch[v][0];if(ch[v][0])fa[ch[v][0]]=fa[v];}
else{rt=ch[v][0];ch[v][0]=fa[rt]=0;}
S.erase(--S.end());
}
}
return 0;
}

B 影魔

给定\(1,2,...,n\)的排列\(a\),对一个区间\([l,r](l<r)\),定义\(m=\max_{i=l+1}^{r-1}a_i\),其贡献为:

  • 若\(m\)不存在或\(m\lt a_l\)且\(m \lt a_r\),则贡献为\(p1\);
  • 若\(a_l\lt m \lt a_r\)或\(a_r\lt m \lt a_l\),则贡献为\(p2\)。
  • 其余情况贡献为\(0\)。

给定\(m\)个询问,每次询问要求一个区间的所有子区间的贡献之和。

\(n,m\le 2\times 10^5\)。


tag:线段树,树状数组

本题有一个部分分\(p1=2p2\),对于该部分分,容易发现可以把最大值和\(l,r\)的关系分开考虑。具体地,对每个\(l\),若\(\max_{i=l+1}^{r-1}a_i \lt a_l\),则\([l,r]\)提供\(p2\)的贡献。对\(r\)同理,两部分贡献可以直接累加。

那么,用单调栈求出\(a_i\)前一个,后一个比它大的位置\(lst_i,nxt_i\)。对询问按右端点排序,从左往右扫描\(r\),给区间\([r+1,nxt_i]\)的值加\(p2\),并处理右端点为\(r\)的所有询问即可。

一般情况,我们只要再求出贡献为\(p1\)的区间即可。考虑最大值\(a_i\),它能作为最大值的区间\([l,r]\)满足\(lst_i\lt l,r\lt nxt_i\)。又要满足\(a_i \le a_l,a_i \le a_r\)的条件,所以题中\(m=a_i\)的区间只有\([lst_i,nxt_i]\)。

为统计某个询问区间中有多少个上述区间,可以使用树状数组套线段树,比较套路。

难度Medium,考虑最大值的位置是基本的,但是考场上脑子一抽想统计贡献为 \(p2\) 甚至没有贡献的区间数,然后做不出来。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=2e5+10;
int n,m,a[N],st[N],top,x[N],y[N];ll ans[N],p1,p2;
struct query{int l,r,id;}q[N];
bool cmp1(query a,query b){return a.r<b.r;}
bool cmp2(query a,query b){return a.l>b.l;}
int tot,lc[N*60],rc[N*60];ll sum[N*60],tag[N*60];
#define mid ((l+r)>>1)
int new_node(){
++tot;
lc[tot]=rc[tot]=sum[tot]=tag[tot]=0;
return tot;
}
void push_up(int p){sum[p]=sum[lc[p]]+sum[rc[p]];}
void push_down(int p,int l,int r){
int v=tag[p];tag[p]=0;
if(!v||l==r)return;
sum[lc[p]]+=(mid-l+1)*v;tag[lc[p]]+=v;
sum[rc[p]]+=(r-mid)*v;tag[rc[p]]+=v;
}
void modify(int p,int l,int r,int L,int R,int v){
if(l>=L&&r<=R){sum[p]+=(r-l+1)*v;tag[p]+=v;return;}
if(!lc[p])lc[p]=new_node();if(!rc[p])rc[p]=new_node();
push_down(p,l,r);
if(L<=mid)modify(lc[p],l,mid,L,R,v);
if(R>mid)modify(rc[p],mid+1,r,L,R,v);
push_up(p);
}
ll query(int p,int l,int r,int L,int R){
if(!p)return 0;
if(l>=L&&r<=R)return sum[p];
push_down(p,l,r);
if(R<=mid)return query(lc[p],l,mid,L,R);
if(L>mid)return query(rc[p],mid+1,r,L,R);
return query(lc[p],l,mid,L,R)+query(rc[p],mid+1,r,L,R);
}
#undef mid
void add(int x,int v){for(;x<=n;x+=x&-x)modify(x,1,n,v,v,1);}
int ask(int x,int y){int res=0;for(;x;x-=x&-x)res+=query(x,1,n,y,n);return res;}
int main(){
scanf("%d%d%lld%lld",&n,&m,&p1,&p2);
for(int i=1;i<=n;i++)scanf("%d",a+i);
for(int i=1;i<=m;i++)scanf("%d%d",&q[i].l,&q[i].r),q[i].id=i;
sort(q+1,q+m+1,cmp1);new_node();
for(int i=1,j=1;i<=n;i++){
while(top&&a[st[top]]<a[i])--top;
modify(1,1,n+2,st[top]+1,i,1);
x[i]=st[top];st[++top]=i;
for(;j<=m&&q[j].r<=i;++j)
ans[q[j].id]+=query(1,1,n+2,q[j].l+1,q[j].r+1);
}tot=0;new_node();top=0;st[0]=n+1;
sort(q+1,q+m+1,cmp2);
for(int i=n,j=1;i>=1;i--){
while(top&&a[st[top]]<a[i])--top;
modify(1,1,n+2,i+2,st[top]+1,1);
y[i]=st[top];st[++top]=i;
for(;j<=m&&q[j].l>=i;++j)
ans[q[j].id]+=query(1,1,n+2,q[j].l+1,q[j].r+1);
}
tot=0;
for(int i=1;i<=n;i++)new_node();
for(int i=1;i<=n;i++)if(x[i]<y[i]&&x[i]!=0&&y[i]!=n+1)add(y[i],x[i]);
for(int i=1;i<=m;i++){
int id=q[i].id,l=q[i].l,r=q[i].r;
int cnt1=ask(r,l)+r-l,cnt2=ans[id]-2*cnt1;
ans[id]=p1*cnt1+p2*cnt2;
}
for(int i=1;i<=m;i++)printf("%lld\n",ans[i]);
return 0;
}

C 礼物

给定两个长为\(n\)的数列\(x,y\),所有数为\(1,2,...,m\)中的整数。可以将一个数列中所有数加上一个常数,也可以对一个数列做轮换。最小化\(\sum_{i=1}^{n}(x_i-y_i)^2\)。

\(n \le 5 \times 10^4,m \le 100\)


tag:FFT

假设在数列\(x\)上加上\(c\),则目标函数化为

\[\sum_{i=1}^{n}((x_i+c)-y_i)^2=\sum_{i=1}^{n}(x_i^2+y_i^2)+\sum_{i=1}^{n}x_iy_i+nc^2-2\sum_{i=1}^{n}(x_i-y_i)\times c
\]

关于\(c\)的部分是二次函数,且与\(x\)是否轮换无关,所以可以直接求出\(c\)。

剩下的部分是最小化\(\sum_{i=1}^{n}x_iy_i\),显然想到卷积形式,用FFT做即可。

但是!我还不会FFT,所以我使用\(O(n^2)\)的暴力。毕竟\(n\)也不大,卡卡常就过了。最慢的点跑了939ms(时限1s),喜提洛谷最劣解。

难度???,应该是板的,但是我不会板,但是我会暴力。暴力场切。

点击查看代码
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
using namespace std;
inline int read(){
int x=0;char ch=getchar();
while(ch<'0'||ch>'9')ch=getchar();
while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
return x;
}
typedef long long ll;
const int N=5e4+5;
int n,m,x[N<<1],y[N],sum,c,ans,res;
int main(){
n=read();m=read();
for(register int i=0;i<n;++i)x[n+i]=x[i]=read(),sum+=x[i],ans+=x[i]*x[i];
for(register int i=0;i<n;++i)y[i]=read(),sum-=y[i],ans+=y[i]*y[i];
c=floor(-1.0*sum/n+0.5);ans+=n*c*c+2*c*sum;
for(register int i=0;i<n;++i){
sum=0;
for(register int j=0;j<n;++j)sum+=x[i+j]*y[j];
res=max(res,sum);
}
printf("%d\n",ans-2*res);
return 0;
}

HNOI2018 Day1

2023-05-30


A 寻宝游戏

给定\(n\)个长为\(m\)的二进制串,可以在每两个二进制数之间和第一个数之前添加一个运算符(按位与/按位或),然后在这个算式最前面填上\(0\)。问使得这个算式的值刚好为给定的询问值的添加方法数。\(q\)组询问。答案对\(10^9+7\)取模。

\(n \le 1000, m \le 5000, q \le 1000\)。


tag:思维题

将按位与看做\(1\),按位或看做\(0\),会发现:将操作序列和每个数第\(j\)位构成的序列从后往前比较,若操作序列字典序小,则该位结果为\(1\),否则结果为\(0\)。证明是不难的。

那么只要将每一位构成的序列排序,如果询问串要求比一段前缀大,比一段后缀小就合法,输出分界点的两个数的差;否则无解。

用基数排序可以将时间复杂度做到\(O((n+q)m)\)。

难度Hard-。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=1005,M=5005,mod=1e9+7;
int n,m,q,d=1,cnt[2],val[M],A[M],B[M];char s[M];
int main(){
scanf("%d%d%d",&n,&m,&q);
for(int i=1;i<=m;i++)A[i]=i;
for(int i=1;i<=n;i++,d=d*2%mod){
scanf("%s",s+1);
cnt[0]=0;cnt[1]=m;
for(int j=1;j<=m;j++){
if(s[j]=='0')cnt[0]++;
else val[j]=(val[j]+d)%mod;
}
for(int j=m;j>=1;j--)
B[cnt[s[A[j]]-'0']--]=A[j];
for(int j=1;j<=m;j++)A[j]=B[j];
}
for(int i=1;i<=m;i++)B[A[i]]=i;
A[m+1]=m+1;val[m+1]=d;
for(int i=1;i<=q;i++){
int L=0,R=m+1;
scanf("%s",s+1);
for(int j=1;j<=m;j++){
if(s[j]=='0')L=max(L,B[j]);
else R=min(R,B[j]);
}
if(L>R)printf("0\n");
else printf("%d\n",(val[A[R]]-val[A[L]]+mod)%mod);
}
return 0;
}

B 转盘

一个转盘上有摆成一圈的\(n\)个物品,依次编号为\(1\sim n\),编号为的\(i\)物品会在\(T_i\)时刻出现(之后不消失)。在 \(0\) 时刻时,小G可以任选 \(n\) 个物品中的一个,每经过一个单位时间可以继续选择当前物品或选择下一个物品。在每一时刻,如果小 G 选择的物品已经出现了,那么小G将会标记它。问小G至少需要多少时间来标记所有物品。

然而还有\(m\)次修改,每次修改改变一个物品的出现时间。强制在线。

\(n,m\le 10^5\)。


建议BC两题换一下名字

tag:线段树,单调栈

首先转化问题,记\(a_i=t_i-i (1 \le i \le 2n),t_{i+n}=t_i\),注意到小G最多转一圈,随便推推容易发现:以\(i\)为起点的答案是\(\max_{i\le j \lt i+n}{a_j}+n+i-1\)。然后可以把上界换成\(2n\)。

这个时候转化视角,考虑对于某个\(j\)的最小值。显然只用考虑\(a_j\)是后缀最大值的情形。设\(a_i\)是大于\(a_j\)的数中最靠右的一个,则\(a_j\)的答案是\(a_j+i+n\)。

所以可以考虑用线段树维护一个单调栈,从后向前考虑即可。

难度Hard。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5,INF=1<<30;
int n,m,op,a[N<<1],ans;
struct node{int mx,res;}tr[N<<2];
int query(int p,int l,int r,int x){
if(l==r)return tr[p].mx>x?x+l:INF;
int mid=l+r>>1;
if(tr[p<<1|1].mx<=x)return query(p<<1,l,mid,x);
return min(tr[p].res,query(p<<1|1,mid+1,r,x));
}
void push_up(int p,int l,int r){
tr[p].mx=max(tr[p<<1].mx,tr[p<<1|1].mx);
tr[p].res=query(p<<1,l,l+r>>1,tr[p<<1|1].mx);
}
void build(int p,int l,int r){
if(l==r){tr[p].mx=a[l]-l;return;}
int mid=l+r>>1;
build(p<<1,l,mid);
build(p<<1|1,mid+1,r);
push_up(p,l,r);
}
void modify(int p,int l,int r,int x,int v){
if(l==r){tr[p].mx=v-l;return;}
int mid=l+r>>1;
if(x<=mid)modify(p<<1,l,mid,x,v);
else modify(p<<1|1,mid+1,r,x,v);
push_up(p,l,r);
}
int main(){
scanf("%d%d%d",&n,&m,&op);
for(int i=1;i<=n;i++)scanf("%d",a+i);
build(1,1,n);
printf("%d\n",ans=query(1,1,n,tr[1].mx-n)+n);
for(int i=1,x,y;i<=m;i++){
scanf("%d%d",&x,&y);
if(op)x^=ans,y^=ans;
modify(1,1,n,x,y);
printf("%d\n",ans=query(1,1,n,tr[1].mx-n)+n);
}
return 0;
}

C 毒瘤

\(n\)个点,\(m\)条边的连通简单无向图,求独立集个数。

\(n\le 10^5,m \le n+10\)。


tag:DP,虚树/动态DP

首先随便取一棵生成树,然后标记所有非树边的端点。

对于树的情况,很容易用树形DP解决。同时有一个显然的暴力:枚举上述所有端点的颜色,做树形DP。

考虑对所有端点建出虚树,在虚树上DP。这样时间复杂度就是\(O(s4^s)\),其中\(s=m-n+1\),能过。

发现在虚树上的一条边转移时,不论该边两个端点状态如何,因为这两个点在原树上之间的形态相同,所以转移系数是相同的。那么暴力求出每个点到它虚树上的父亲的转移系数。这里可以直接暴力,最多把每个点遍历一次。

剩下就是二进制枚举。

这道题也可以用动态DP,可以算是一个模板题。

难度Hard-。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+50,mod=998244353;
int n,m,ans,a[N];
int head[N],ver[N<<1],nxt[N<<1],tot=1;
int hc[N],vc[N<<1],nc[N<<1],tc;
void add(int u,int v){ver[++tot]=v;nxt[tot]=head[u];head[u]=tot;}
void addc(int u,int v){vc[++tc]=v;nc[tc]=hc[u];hc[u]=tc;}
int fa[N][20],dep[N],dfn[N],Dfn,tr[N<<1],vis[N],x[N],d[N],k,st[N],top;
void dfs(int u){
dfn[u]=++Dfn;
for(int i=head[u],v;i;i=nxt[i])
if(!dfn[v=ver[i]]){
tr[i]=tr[i^1]=1;
fa[v][0]=u;dep[v]=dep[u]+1;
dfs(v);
}
}
void LCA_pre(){
for(int j=1;(1<<j)<=n;j++)
for(int i=1;i<=n;i++)
fa[i][j]=fa[fa[i][j-1]][j-1];
}
int LCA(int u,int v){
if(dep[u]<dep[v])swap(u,v);
int d=dep[u]-dep[v];
for(int i=17;i>=0;i--)
if(d&(1<<i))u=fa[u][i];
if(u==v)return u;
for(int i=17;i>=0;i--)
if(fa[u][i]!=fa[v][i])
u=fa[u][i],v=fa[v][i];
return fa[u][0];
}
bool cmp(int i,int j){return dfn[i]<dfn[j];}
void vtree(){
sort(x+1,x+k+1,cmp);st[top=1]=1;
for(int i=1;i<=k;i++)if(x[i]!=1){
int g=LCA(x[i],st[top]);
if(g!=st[top]){
while(dfn[g]<dfn[st[top-1]])
addc(st[top-1],st[top]),--top;
if(dfn[g]!=dfn[st[top-1]])
addc(g,st[top]),st[top]=g;
else addc(g,st[top]),--top;
}
st[++top]=x[i];
}
for(int i=1;i<top;i++)addc(st[i],st[i+1]);
}
int dp[N][2],trans[N][2][2],f[N][2];
void DP_pre(int u){
dp[u][0]=dp[u][1]=1;
for(int i=head[u],v;i;i=nxt[i])
if(tr[i]&&(v=ver[i])!=fa[u][0]){
DP_pre(v);
if(!vis[v]){
dp[u][0]=1ll*dp[u][0]*(dp[v][0]+dp[v][1])%mod;
dp[u][1]=1ll*dp[u][1]*dp[v][0]%mod;
}
else vis[u]=1;
}
}
void DP(int u){
f[u][0]=dp[u][0];f[u][1]=dp[u][1];
if(a[u]!=-1)f[u][1-a[u]]=0;
for(int i=hc[u],v;i;i=nc[i]){
DP(v=vc[i]);
int f0=(1ll*trans[v][0][0]*f[v][0]+1ll*trans[v][0][1]*f[v][1])%mod,
f1=(1ll*trans[v][1][0]*f[v][0]+1ll*trans[v][1][1]*f[v][1])%mod;
f[u][0]=1ll*f[u][0]*(f0+f1)%mod;
f[u][1]=1ll*f[u][1]*f0%mod;
}
}
int main(){
memset(a,-1,sizeof(a));
scanf("%d%d",&n,&m);
for(int i=1,u,v;i<=m;i++){
scanf("%d%d",&u,&v);
add(u,v);add(v,u);
}
dfs(1);LCA_pre();
for(int i=2;i<=tot;i+=2)if(!tr[i]){
++k;vis[d[k]=x[k]=ver[i]]=1;
++k;vis[d[k]=x[k]=ver[i^1]]=1;
}
DP_pre(1);
sort(x+1,x+k+1);
k=unique(x+1,x+k+1)-x-1;
vtree();
for(int u=1;u<=n;u++)
for(int i=hc[u];i;i=nc[i]){
int v=vc[i];
trans[v][0][0]=trans[v][1][1]=1;
for(int x=v;fa[x][0]!=u;x=fa[x][0]){
int t00=trans[v][0][0],t01=trans[v][0][1],
t10=trans[v][1][0],t11=trans[v][1][1],y=fa[x][0];
trans[v][0][0]=1ll*dp[y][0]*(t00+t10)%mod;
trans[v][0][1]=1ll*dp[y][0]*(t01+t11)%mod;
trans[v][1][0]=1ll*dp[y][1]*t00%mod;
trans[v][1][1]=1ll*dp[y][1]*t01%mod;
}
}
for(int i=0;i<(1<<k);i++){
for(int j=1;j<=k;j++)a[x[j]]=(i>>j-1)&1;
bool flag=1;
for(int j=1;j<=m-n+1;j++)
if(a[d[2*j-1]]&&a[d[2*j]])flag=0;
if(!flag)continue;
DP(1);ans=(ans+(f[1][0]+f[1][1])%mod)%mod;
}
printf("%d\n",ans);
return 0;
}

动态DP:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e5+50;const ll mod=998244353;
int n,m;ll f[N][2],ans;
int head[N],nxt[N<<1],ver[N<<1],tot=1,tr[N<<1];
void add(int u,int v){ver[++tot]=v;nxt[tot]=head[u];head[u]=tot;}
ll power(ll a,ll b){
ll c=1;
for(;b;b>>=1){
if(b&1)c=c*a%mod;
a=a*a%mod;
}
return c;
}
struct Matrix{
ll a[2][2];
Matrix (){a[0][0]=a[0][1]=a[1][0]=a[1][1]=0;}
Matrix operator *(const Matrix&b)const{
Matrix c;
c.a[0][0]=(a[0][0]*b.a[0][0]+a[0][1]*b.a[1][0])%mod;
c.a[1][0]=(a[1][0]*b.a[0][0]+a[1][1]*b.a[1][0])%mod;
c.a[0][1]=(a[0][0]*b.a[0][1]+a[0][1]*b.a[1][1])%mod;
c.a[1][1]=(a[1][0]*b.a[0][1]+a[1][1]*b.a[1][1])%mod;
return c;
}
}g[N];
int fa[N],top[N],siz[N],L[N],dfn,R[N],son[N],id[N];
void dfs(int u){
f[u][0]=f[u][1]=1;siz[u]=1;
for(int i=head[u],v;i;i=nxt[i])
if(!siz[v=ver[i]]){
tr[i]=tr[i^1]=1;
fa[v]=u;dfs(v);siz[u]+=siz[v];
if(siz[v]>siz[son[u]])son[u]=v;
f[u][0]=f[u][0]*(f[v][0]+f[v][1])%mod;
f[u][1]=f[u][1]*f[v][0]%mod;
}
}
void rdfs(int u,int tp){
g[u].a[0][0]=g[u].a[1][0]=1;
L[u]=++dfn;id[dfn]=u;top[u]=tp;R[tp]=dfn;
if(son[u])rdfs(son[u],tp);
for(int i=head[u],v;i;i=nxt[i])
if(tr[i]&&(v=ver[i])!=fa[u]&&v!=son[u]){
rdfs(v,v);
g[u].a[0][0]=g[u].a[0][0]*(f[v][0]+f[v][1])%mod;
g[u].a[1][0]=g[u].a[1][0]*f[v][0]%mod;
}
g[u].a[0][1]=g[u].a[0][0];
}
struct SegmentTree{
Matrix a[N<<2];
#define mid (l+r>>1)
void build(int p,int l,int r){
if(l==r){a[p]=g[id[l]];return;}
build(p<<1,l,mid);
build(p<<1|1,mid+1,r);
a[p]=a[p<<1]*a[p<<1|1];
}
void modify(int p,int l,int r,int x){
if(l==r){a[p]=g[id[l]];return;}
if(x<=mid)modify(p<<1,l,mid,x);
else modify(p<<1|1,mid+1,r,x);
a[p]=a[p<<1]*a[p<<1|1];
}
Matrix query(int p,int l,int r,int L,int R){
if(l>=L&&r<=R)return a[p];
if(R<=mid)return query(p<<1,l,mid,L,R);
if(L>mid)return query(p<<1|1,mid+1,r,L,R);
return query(p<<1,l,mid,L,R)*query(p<<1|1,mid+1,r,L,R);
}
#undef mid
}seg;
int a[N],x[50],k,st[N],Top;Matrix g0[N];
vector<int> rej[N];
void update(int u,int op){
st[++Top]=u,g0[Top]=g[u];
if(op==0)g[u].a[1][0]=0;
else g[u].a[0][0]=g[u].a[0][1]=0;
while(u){
Matrix lst=seg.query(1,1,n,L[top[u]],R[top[u]]);
seg.modify(1,1,n,L[u]);
Matrix now=seg.query(1,1,n,L[top[u]],R[top[u]]);
u=fa[top[u]];st[++Top]=u,g0[Top]=g[u];
g[u].a[0][0]=g[u].a[0][0]*(now.a[0][0]+now.a[1][0])%mod
*power(lst.a[0][0]+lst.a[1][0],mod-2)%mod;
g[u].a[1][0]=g[u].a[1][0]*now.a[0][0]%mod
*power(lst.a[0][0],mod-2)%mod;
g[u].a[0][1]=g[u].a[0][0];
}
}
void clear(int tim){
while(Top>tim){
g[st[Top]]=g0[Top];
seg.modify(1,1,n,L[st[Top]]);
--Top;
}
}
void Enum(int p){
if(p==k+1){
Matrix res=seg.query(1,1,n,1,R[1]);
ans=(ans+res.a[0][0]+res.a[1][0])%mod;
return;
}
int tim=Top;
update(x[p],a[x[p]]=0);Enum(p+1);clear(tim);a[x[p]]=-1;
update(x[p],a[x[p]]=1);bool flag=1;
for(auto t:rej[x[p]])if(a[t]==1){flag=0;break;}
if(flag)Enum(p+1);clear(tim);a[x[p]]=-1;
}
int main(){
memset(a,-1,sizeof(a));
scanf("%d%d",&n,&m);
for(int i=1,u,v;i<=m;i++){
scanf("%d%d",&u,&v);
add(u,v);add(v,u);
}
dfs(1);rdfs(1,1);seg.build(1,1,n);
for(int i=2;i<=tot;i+=2)if(!tr[i]){
x[++k]=ver[i];x[++k]=ver[i^1];
rej[ver[i]].push_back(ver[i^1]);
rej[ver[i^1]].push_back(ver[i]);
}
sort(x+1,x+k+1);k=unique(x+1,x+k+1)-x-1;
Enum(1);
printf("%lld\n",ans);
return 0;
}

USACO23open选做

2023-05-22


A Custodial Cleanup G

\(n\) 个点 \(m\) 条边的无向图,点 \(i\) 有一个颜色 \(c_i\),一把颜色为 \(s_i\) 的钥匙,目标是在该点放置颜色为 \(f_i\) 的钥匙。FJ初始在点 \(1\),他可以捡起当前点的钥匙,放下一把或多把钥匙,以及移动向相邻的点。这里能够移动的条件是FJ手中至少有一把和目标点颜色相同的钥匙。问FJ能否完成目标。\(T\) 组数据。

\(0 \le m \le 10^5\), \(1 \le c_i,s_i,f_i \le n \le 10^5\)。

\(1 \le T \le 100\), \(1 \le \sum n \le 10^5\), \(1 \le \sum m \le 2 \times 10^5\)。


tag:BFS

(其实这个tag也无所谓,关键是分析)

首先考虑FJ至多能捡起多少钥匙,此时当然钦定FJ不放下任何钥匙。做一个BFS,不断扩展可以到达的连通块。扩展过程中,维护已经拿到的钥匙(用一个bool数组),并记录各个颜色相邻的点(用\(n\)个vector)。每次取出新点,检查是否获得该颜色的钥匙,如果没有则更新,并将该颜色的vector中所有点加入队列。然后扩展该点的邻点,如果颜色可以走到就加入队列,否则加入相应的vector。

在过程中,我们求出了FJ能到达的所有点。在这些点中,再来考虑FJ能放下哪些钥匙。假设FJ可以完成目标,让FJ倒着运动,即初始时点 \(i\) 有一把颜色为 \(f_i\) 的钥匙,此时FJ可以进入一个点,当且仅当他有该点颜色的钥匙,或者该点颜色的钥匙就放在该房间里。同样的做一遍BFS即可。

对于第二遍BFS中不能到达的点,检查是否有 \(s_i==f_i\) 成立。若有不成立者,则答案为NO,否则为YES。

难度Easy+,场切。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int T,n,m,c[N],s[N],t[N];
int head[N],nxt[N<<1],ver[N<<1],tot;
void add(int u,int v){ver[++tot]=v;nxt[tot]=head[u];head[u]=tot;}
int vis1[N],vis2[N],tag[N];
queue<int> Q;vector<int> col[N];
int main(){
scanf("%d",&T);
while(T--){
scanf("%d%d",&n,&m);tot=0;
for(int i=1;i<=n;i++){col[i].clear();head[i]=vis1[i]=tag[i]=0;}
for(int i=1;i<=n;i++)scanf("%d",c+i);
for(int i=1;i<=n;i++)scanf("%d",s+i);
for(int i=1;i<=n;i++)scanf("%d",t+i);
for(int i=1,u,v;i<=m;i++){scanf("%d%d",&u,&v);add(u,v);add(v,u);}
Q.push(1);
while(!Q.empty()){
int u=Q.front();Q.pop();vis1[u]=1;
if(!tag[s[u]]){
for(auto x:col[s[u]])Q.push(x);
col[s[u]].clear();tag[s[u]]=1;
}
for(int i=head[u],v;i;i=nxt[i])
if(!vis1[v=ver[i]]){
if(tag[c[v]])Q.push(v);
else col[c[v]].push_back(v);
}
}
for(int i=1;i<=n;i++){col[i].clear();vis2[i]=tag[i]=0;}
Q.push(1);
while(!Q.empty()){
int u=Q.front();Q.pop();vis2[u]=1;
if(!tag[t[u]]){
for(auto x:col[t[u]])Q.push(x);
col[t[u]].clear();tag[t[u]]=1;
}
for(int i=head[u],v;i;i=nxt[i])
if(vis1[v=ver[i]]&&!vis2[v]){
if(tag[c[v]]||c[v]==t[v])Q.push(v);
else col[c[v]].push_back(v);
}
}
bool flag=1;
for(int i=1;i<=n;i++)if(!vis2[i]&&s[i]!=t[i])flag=0;
printf(flag?"YES\n":"NO\n");
}
return 0;
}

B Tree Merging G

定义一棵有根树的一次合并操作为:选择两个具有相同父亲的结点,将其合并成一个节点,新节点的编号为原来的两个节点编号的较大值,新节点的子节点集合为原来的两个节点子节点集合的并集。

给定初始状态和最终状态,构造操作序列。保证有解。初始状态和最终状态分别是 \(n\),\(m\) 个点的树。\(T\) 组数据。

\(1\le t \le 100\), \(2 \le m \le n \le 1000\)。


tag:思维题

按照深度处理。

假设深度较小的所有合并已经正确维护,考虑某一个节点的所有儿子。那些不在最终树中的点将被合并。

Case 1:如果该点的子树中有点被保留,那么找到被保留的这个点的某个祖先与之深度相同,合并。

Case 2:否则,找到一个点,最终树中其子树往下每层的最大值都大于该点的最大值,合并。

所有步骤都可以暴力,我甚至还用了一个set。看起来复杂度不太对,但是应该不好卡。

难度Medium-,场切。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=1005;
int T,n,m,rt,f[N],g[N],r[N],mx[N][N],mx0[N][N];
set<int> a[N],b[N],s[N];
vector<pair<int,int> > ans;
void merge(int x,int y){
ans.push_back(make_pair(x,y));
for(auto p:a[x])a[y].insert(p);a[x].clear();
for(auto p:s[x])s[y].insert(p);s[x].clear();
for(int i=1;mx[y][i];i++)mx[y][i]=max(mx[y][i],mx[x][i]);
}
void dfsa(int u){
s[u].clear();s[u].insert(u);mx[u][0]=u;
for(auto v:a[u]){
dfsa(v);
for(auto x:s[v])s[u].insert(x);
for(int i=0;mx[v][i];i++)
mx[u][i+1]=max(mx[u][i+1],mx[v][i]);
}
}
void dfsb(int u){
mx0[u][0]=u;
for(auto v:b[u]){
dfsb(v);
for(int i=0;mx0[v][i];i++)
mx0[u][i+1]=max(mx0[u][i+1],mx0[v][i]);
}
}
void dfs(int u){
for(auto x:a[u])if(b[u].find(x)==b[u].end()){
int y=0;
for(auto z:s[x])if(r[z]){y=z;break;}
if(y){
for(;b[u].find(y)==b[u].end();y=g[y]);
merge(x,y);continue;
}
for(auto y:b[u]){
bool flag=1;
for(int i=0;mx[x][i];i++)if(mx0[y][i]<mx[x][i])flag=0;
if(flag){merge(x,y);break;}
}
}
for(auto v:b[u])dfs(v);
}
int main(){
scanf("%d",&T);
while(T--){
scanf("%d",&n);
ans.clear();
for(int i=1;i<=n;i++){
a[i].clear(),b[i].clear(),r[i]=f[i]=0;
for(int j=0;mx[i][j];j++)mx[i][j]=0;
for(int j=0;mx0[i][j];j++)mx0[i][j]=0;
}
for(int i=1,u,v;i<n;i++){
scanf("%d%d",&u,&v);
a[v].insert(u);f[u]=v;
}
scanf("%d",&m);
for(int i=1,u,v;i<m;i++){
scanf("%d%d",&u,&v);
b[v].insert(u);g[u]=v;r[u]=r[v]=1;
}
for(int i=1;i<=n;i++)if(!f[i])rt=i;
dfsa(rt);dfsb(rt);dfs(rt);
printf("%d\n",ans.size());
for(auto x:ans)printf("%d %d\n",x.first,x.second);
}
return 0;
}

C Pareidolia P

对于一个字符串,其权值为满足下面条件的 \(k\) 的最大值:字符串bessie重复 \(k\) 次后是该字符串的子串。

给定一个长为 \(n\) 的字符串 \(S\) 和 \(m\) 次修改字符,求所有修改前和每次修改后,字符串所有子串的权值之和。

\(1 \le n,m \le 2 \times 10^5\)。


tag:DP,线段树

首先用DP处理静态问题。记 \(T=\)bessie,下标从 \(0\) 开始。用 \(f_{i,j}\) 表示以 \(S_i\) 结尾,处理到 \(T_j\) 的子串数目。转移如下:

\(f_{i-1,j} \to f_{i,j+[s_i=t_j]} , 1 \to f_{i,[s_i=t_0]} , ([s_i=t_5])f_{i-1,5} \to ans\)

用线段树维护这个转移,对每个区间,需要记录:

  • 从 \(T_j\) 进入,离开的字符 \(nxt_j\)。
  • 离开的字符是 \(T_j\) 的后缀数 \(cnt_j\)。
  • 进入的字符是 \(T_j\) 的贡献位置数 \(res_j\)。

合并时,用左边的 \(cnt_j\) 与右边的 \(res_j\) 相乘更新答案。

难度Medium+。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=2e5+5;
int n,m;char s[N];
string t="bessie";
struct node{ll nxt[6],cnt[6],res[6],sum;}tr[N<<2];
node gen(char ch,int pos){
node x;x.sum=0;
for(int i=0;i<6;i++)x.nxt[i]=x.cnt[i]=x.res[i]=0;
if(pos){
for(int i=0;i<6;i++)x.nxt[i]=(ch==t[i]?(i+1)%6:i);
x.cnt[x.nxt[0]]=1;x.res[5]=(ch=='e'?n-pos+1:0);
}
return x;
}
void push_up(int p){
node a=tr[p<<1],b=tr[p<<1|1],c=gen('#',0);
c.sum=a.sum+b.sum;
for(int i=0;i<6;i++){
c.nxt[i]=b.nxt[a.nxt[i]];
c.cnt[i]+=b.cnt[i];c.cnt[b.nxt[i]]+=a.cnt[i];
c.res[i]=b.res[a.nxt[i]]+a.res[i];
c.sum+=a.cnt[i]*b.res[i];
}
tr[p]=c;
}
void build(int p,int l,int r){
if(l==r){tr[p]=gen(s[l],l);return;}
build(p<<1,l,l+r>>1);
build(p<<1|1,(l+r>>1)+1,r);
push_up(p);
}
void modify(int p,int l,int r,int x,char c){
if(l==r){tr[p]=gen(c,x);return;}
int mid=l+r>>1;
if(x<=mid)modify(p<<1,l,mid,x,c);
else modify(p<<1|1,mid+1,r,x,c);
push_up(p);
}
int main(){
scanf("%s",s+1);n=strlen(s+1);
build(1,1,n);
printf("%lld\n",tr[1].sum);
scanf("%d",&m);
for(int i=1;i<=m;i++){
int x;char c[2];
scanf("%d%s",&x,c);
modify(1,1,n,x,c[0]);
printf("%lld\n",tr[1].sum);
}
return 0;
}

D Triples of Cows P

给定一棵初始有 \(n\) 个点的树。在第 \(i\) 天,这棵树的第 \(i\) 个点会被删除,所有与点 \(i\) 直接相连的点之间都会两两连上一条边。在每次删点前,求出满足 \((a,b)\) 之间有边,\((b,c)\) 之间有边且 \(a\not=c\) 的有序三元组 \((a,b,c)\) 数。

\(n \le 2 \times 10^5\)。


tag:拆边,并查集

由于每次删点之后新增的边数太多,所以考虑拆边。用一个白点表示一条边,并将原来树中的点称为黑点,编号不变。对于第 \(i\) 条边 \((u_i,v_i)\),在 \((u_i,n+i),(v_i,n+i)\) 间连边,得到一棵树 \(T\)。

删除点 \(i\) 时,我们将点 \(i\) 相邻的所有白点合并为一个点,然后删除 \(i\)。容易归纳证明:每次删除后 \(T\) 仍然是树;真实的 \(u,v\) 间有边等价于树 \(T\) 中存在一个白点与 \(u,v\) 均相邻。

因为依次删除 \(1,2,...,n\),所以在树 \(T\) 中,可以把点 \(n\) 作为根。这样每次删除时,就可以把 \(i\) 的所有相邻白点合并到 \(i\) 的父亲白点上去。然后,用并查集维护每个白点被合并到了哪个点。

用 \(fa_u\) 表示在初始的 \(T\) 中 \(u\) 的父亲结点,\(p_u\) 表示某个时刻白点 \(u\) 被合并到的点。那么,在这一时刻,黑点 \(u\) 的父亲结点是 \(p_{fa_u}\),白点 \(u\) 的父亲节点是 \(fa_{p_u}\)。

下面来考虑如何统计答案。对白点 \(u\),记 \(s_u\) 为 \(u\) 的儿子个数。

对于一个符合要求的 \((a,b,c)\),设 \(a,b\) 通过白点 \(x\) 相连,\(b,c\) 通过白点 \(y\) 相连。

  1. 如果 \(x=y\):固定 \(x\),在 \(x\) 的邻点中任选 \(3\) 个,则对答案的贡献为
    \[\sum_{x} (s_x+1)s_x(s_x-1)
    \]

    求和的条件是 \(x\) 是白点。

  2. 如果 \(x\not=y\),且 \(x,y\) 都是 \(b\) 的子节点:固定 \(b\),先任取 \(b\) 的两个子结点 \(x,y\)(有序),此时贡献 \(s_xs_y\)。则总的贡献为
    \[\sum_{b} (\sum_{x} s_x) ^2 - \sum_{x} s_x^2
    \]

    第一个求和的条件是 \(b\) 是黑点,后两个求和的条件是 \(x\) 是 \(b\) 的儿子。

    注意到后一项拆出来就是对所有白点 \(x\),求 \(s_x^2\) 的和,那就拆出来吧。

  3. 如果 \(x\not=y\),且 \(x,y\) 一个是 \(b\) 的子结点,另一个是 \(b\) 的父亲结点:不妨 \(x\) 是 \(b\) 的父亲结点,固定 \(x\),\(b\) 是 \(x\) 的一个子结点,\(y\) 又是 \(b\) 的一个子结点,则对答案的贡献为
    \[\sum_{x} 2\times s_x \times \sum_{b} \sum_{y} s_y
    \]

    三个求和的条件分别为 \(x\) 是白点,\(b\) 是 \(x\) 的子结点,\(y\) 是 \(b\) 的结点。

列出式子后,我们发现需要维护以下数据:

  1. 白点 \(u\) 的儿子数目 \(s_u\)
  2. 黑点 \(u\) 的儿子的 \(s\) 值之和,也可以存到数组 \(s\) 里
  3. 白点 \(u\) 的儿子的 \(s\) 值之和 \(t_u\)

    答案是 \(\sum_{x} (f(s_x)+2s_xt_x)+\sum_{y} s_y^2\),其中 \(f(x)=x^3-x^2-x\),两个求和的条件分别是 \(x\) 是黑点,\(y\)是白点。

删除点 \(u\) 时,枚举它初始的的儿子(一定没有被合并过),在并查集中将其合并到 \(u\) 的父亲中。然后清零 \(u\) 和 \(u\) 的儿子的 \(s,t\) 值,更新 \(u\) 的三层祖先的值,并更新答案。

难度Hard。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=4e5+5;
int n,fa[N],p[N];ll s[N],t[N],ans;
int head[N],ver[N<<1],nxt[N<<1],tot;
void add(int u,int v){ver[++tot]=v;nxt[tot]=head[u];head[u]=tot;}
void dfs(int u){
for(int i=head[u],v;i;i=nxt[i])
if((v=ver[i])!=fa[u]){
fa[v]=u;dfs(v);
if(u<=n)s[u]+=s[v];
else ++s[u],t[u]+=s[v];
}
}
int find(int x){return (x==p[x]?x:(p[x]=find(p[x])));}
ll f(ll x){return x*x*x-x*x-x;}
int main(){
scanf("%d",&n);
for(int i=1,u,v;i<n;i++){
scanf("%d%d",&u,&v);
add(u,n+i);add(v,n+i);add(n+i,u);add(n+i,v);
}
dfs(n);
for(int i=1;i<2*n;i++)p[i]=i;
for(int i=1;i<=n;i++)ans+=s[i]*s[i];
for(int i=n+1;i<2*n;i++)ans+=f(s[i])+2*s[i]*t[i];
for(int u=1;u<=n;u++){
printf("%lld\n",ans);
int g=find(fa[u]),w=fa[g];ll del=-1;
ans-=f(s[g])+2*s[g]*t[g]+s[w]*s[w];s[w]-=s[g];--s[g];
t[g]-=s[u];ans-=s[u]*s[u];s[u]=0;
for(int i=head[u],v;i;i=nxt[i])
if((v=ver[i])!=fa[u]){
p[v]=g;s[g]+=s[v];t[g]+=t[v];del+=s[v];
ans-=f(s[v])+2*s[v]*t[v];s[v]=t[v]=0;
}
s[w]+=s[g];ans+=f(s[g])+2*s[g]*t[g]+s[w]*s[w];
t[w=find(fa[fa[g]])]+=del;ans+=2*s[w]*del;
}
return 0;
}

APIO2015

2023-04-20


A 巴厘岛的雕塑

\(n\) 个数分为若干组,组数不少于 \(a\) 且不多于 \(b\)。最小化各组和的 \(OR\) 值。

\(n \le 2000\),\(1=a \le b \le n\) 或 \(n \le 100\),\(1 \le a \le b\)。


tag:贪心,DP

按位处理,从高到低依次尝试每一位是否能够是 \(0\)。\(DP\) 求出在满足高位的条件下的最少分组数和最多分组数即可。

但似乎这个做法是伪的,uoj上没有过。

重新考虑 \(DP\) 状态,考虑到某一位能否分若干组即可。不想写了。

难度Easy,场切。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=2005;
typedef long long ll;
int n,a,b,dp[N][2];ll s[N],res;
int main(){
scanf("%d%d%d",&n,&a,&b);
for(int i=1,x;i<=n;i++){
scanf("%d",&x);
s[i]=s[i-1]+x;
}
res=(1ll<<45)-1;
for(int d=44;d>=0;d--){
ll chk=res-(1ll<<d);
dp[0][0]=dp[0][1]=0;
for(int i=1;i<=n;i++){
dp[i][0]=1e9;dp[i][1]=-1e9;
for(int j=0;j<i;j++)
if((s[i]-s[j]|chk)==chk){
dp[i][0]=min(dp[i][0],dp[j][0]+1);
dp[i][1]=max(dp[i][1],dp[j][1]+1);
}
}
if(dp[n][1]>=a&&dp[n][0]<=b)res=chk;
}
printf("%lld\n",res);
return 0;
}

B 雅加达的摩天楼

有 \(N\) 座楼排列成一条直线,依次编号为 \(0\) 到 \(N − 1\)。有 \(M\) 只叫做 “doge” 的神秘生物,编号依次是 \(0\) 到 \(M − 1\)。编号为 \(i\) 的 doge 最初在 \(B_i\) 的楼。doge 能够跳跃,编号为 \(i\) 的 doge 的跳跃能力为 \(P_i\)。在一次跳跃中,位于摩天楼 \(b\) 而跳跃能力为 \(p\) 的 doge 可以跳跃到编号为 \(b - p\) (如果 \(0 \leq b - p < N\))或 \(b + p\) (如果 \(0 \leq b + p < N\))的摩天楼。

编号为 \(0\) 的 doge 有一条紧急的消息要尽快传送给编号为 \(1\) 的 doge。任何一个收到消息的 doge 有以下两个选择:

  • 跳跃到其他摩天楼上;
  • 将消息传递给它当前所在的摩天楼上的其他 doge。

计算将消息从 \(0\) 号 doge 传递到 \(1\) 号 doge 所需要的最少总跳跃步数,或者告诉它们消息永远不可能传递到 \(1\) 号 doge。

\(1 \leq N \leq 30000\),\(1 \leq P_i \leq 30000\),\(2 \leq M \leq 30000\)


tag:最短路,建图技巧

以位置为顶点建图,每个点上如果有 doge 就向它能够跳到的点连边。跑 dijkstra 可以求解,但是会 T。

考虑重复的边。一个剩余类中可能有很多 doge,没有必要让它们都连边。事实上,同一个剩余类中只需要在相邻的 doge 间连边即可。此时容易证明边数级别为 \(O(n \sqrt n)\)。时间复杂度为 \(O(n \log n)\)。

难度Medium-,场切。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef pair<int,int> pii;
typedef vector<int> vi;
const int N=3e4+5,M=1e7+5;
int n,m,r,s,t,dis[N],vis[N];
int head[N],nxt[M],ver[M],val[M],tot;
void add(int u,int v,int w){
ver[++tot]=v;val[tot]=w;
nxt[tot]=head[u];head[u]=tot;
}
map<pii,vi> MP;vi d[N];
priority_queue<pii> Q;
int main(){
scanf("%d%d",&n,&m);
for(int i=1,b,p;i<=m;i++){
scanf("%d%d",&b,&p);
if(i==1)s=b;if(i==2)t=b;
d[b].push_back(p);
MP[pii(b%p,p)].push_back(b);
}
for(auto f:MP){
vi x=f.second;
int b=f.first.first,p=f.first.second;
sort(x.begin(),x.end());int len=x.size();
for(int i=0;i<len;i++){
int lst=b,nxt=(n-b)/p*p+b;
if(i!=0)lst=x[i-1];
if(i!=len-1)nxt=x[i+1];
for(int j=lst;j<=nxt;j+=p)
add(x[i],j,abs(x[i]-j)/p);
}
}
memset(dis,0x3f,sizeof(dis));
Q.push(make_pair(0,s));dis[s]=0;
while(!Q.empty()){
int u=Q.top().second,d=-Q.top().first;Q.pop();
if(vis[u])continue;vis[u]=1;
for(int i=head[u],v;i;i=nxt[i])
if(dis[v=ver[i]]>d+val[i]){
dis[v]=d+val[i];
Q.push(make_pair(-dis[v],v));
}
}
if(dis[t]>1e9)printf("-1\n");
else printf("%d\n",dis[t]);
return 0;
}

C 巴邻旁之桥

河岸两旁有 \(n\) 个人居住和工作,两岸分别为 A 和 B。每个人有一个居住地和工作地。现在要建 \(k\) 座桥使得所有人上班的总路程最小。桥长为 \(1\)。求最小值。

\(1 \le n \le 10^5\),$ k=1,2 $


tag:中位数

首先不考虑那些居住和工作在同侧的人。对于剩下的人,先加上桥上路程。

\(k=1\) 的情况中,设桥建在位置 \(x\),则目标函数形如 $\sum |x_i-x| $,这里 \(x_i\) 表示所有位置。要最小化目标函数,只用将 \(x\) 取为中位数即可。

\(k=2\) 的情况,对于一个人来说,他应当选择距离自己居住地和工作地中点更近的一座桥。那么按照这个中点排序,两座桥一定分别负责一段前缀和一段后缀。枚举这样的划分。对前缀求解时,进行一遍扫描,只需要动态维护中位数,用对顶堆即可。时间复杂度 \(O(n \log n)\)。

难度Medium,场切。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e5+5;
int n,k;ll ans;char c,d;
namespace task1{
int a[N*2],m=0;
void solve(){
for(int i=1,x,y;i<=n;i++){
cin>>c>>x>>d>>y;
if(c==d)ans+=abs(x-y);
else a[++m]=x,a[++m]=y;
}
sort(a+1,a+m+1);
for(int i=1;i<=m;i++)ans+=abs(a[i]-a[m/2]);
printf("%lld\n",ans+m/2);
}
}
namespace task2{
struct P{int l,r;}a[N];
int m=0;ll res=1e18,pre[N],suf[N];
bool cmp(P a,P b){return a.l+a.r<b.l+b.r;}
void solve(){
memset(pre,0x3f,sizeof(pre));
memset(suf,0x3f,sizeof(suf));
for(int i=1,x,y;i<=n;i++){
cin>>c>>x>>d>>y;
if(x>y)swap(x,y);
if(c==d)ans+=y-x;
else a[++m].l=x,a[m].r=y;
}
if(!m){printf("%lld\n",ans);return;}
sort(a+1,a+m+1,cmp);
pre[0]=suf[m+1]=0;
priority_queue<int> Q1,Q2;
while(!Q1.empty())Q1.pop();while(!Q2.empty())Q2.pop();
pre[1]=a[1].r-a[1].l;
Q1.push(a[1].l);Q2.push(-a[1].r);
ll s1=a[1].l,s2=a[1].r;
for(int i=2,x,tmp;i<=m;i++){
tmp=Q1.top();
x=a[i].l;if(x<tmp)Q1.push(x),s1+=x;else Q2.push(-x),s2+=x;
x=a[i].r;if(x<tmp)Q1.push(x),s1+=x;else Q2.push(-x),s2+=x;
if(Q1.size()>i){x=Q1.top();Q1.pop();s1-=x;s2+=x;Q2.push(-x);}
if(Q2.size()>i){x=-Q2.top();Q2.pop();s2-=x;s1+=x;Q1.push(x);}
pre[i]=s2-s1;
}
while(!Q1.empty())Q1.pop();while(!Q2.empty())Q2.pop();
suf[m]=a[m].r-a[m].l;
Q1.push(a[m].l);Q2.push(-a[m].r);
s1=a[m].l;s2=a[m].r;
for(int i=m-1,x,tmp;i>=1;i--){
tmp=Q1.top();
x=a[i].l;if(x<tmp)Q1.push(x),s1+=x;else Q2.push(-x),s2+=x;
x=a[i].r;if(x<tmp)Q1.push(x),s1+=x;else Q2.push(-x),s2+=x;
if(Q1.size()>m-i+1){x=Q1.top();Q1.pop();s1-=x;s2+=x;Q2.push(-x);}
if(Q2.size()>m-i+1){x=-Q2.top();Q2.pop();s2-=x;s1+=x;Q1.push(x);}
suf[i]=s2-s1;
}
for(int i=0;i<=m;i++)res=min(res,pre[i]+suf[i+1]);
printf("%lld\n",ans+m+res);
}
}
int main(){
ios::sync_with_stdio(false);
cin>>k>>n;
if(k==1)task1::solve();
else task2::solve();
return 0;
}

APIO2014

2023-04-04


A 回文串

给定字符串 \(S\)。对 \(S\) 的所有回文子串,求其长度与出现次数之积的最大值。

\(|S| \le 300000\)。


tag:Manacher、后缀数组 / 回文树

解法1:首先用Manacher算法求出所有的回文串。这并不难,只要在右端点每次扩展的时候记录回文串就可以了,剩下的回文串已经统计过了。

对每个回文串,我们来求它出现的次数。使用后缀数组,找到这个回文串对应的后缀在SA中对应的位置,然后向左向右两次二分,求出包含这一前缀的后缀数目。后缀数组的性质决定了这可以实现。

时间复杂度 \(O(n \log n)\)。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const int N=3e5+5;
int n,m,a[N<<1],lg2[N],cnt,str[N<<2][2];
int S,x[N],y[N],c[N],sa[N],rk[N],h[N],mn[N][25];
ll ans;char s[N],t[N<<1];
void SA(){
S=300;
for(int i=1;i<=n;i++){x[i]=s[i];++c[x[i]];}
for(int i=2;i<=S;i++)c[i]+=c[i-1];
for(int i=n;i>=1;i--)sa[c[x[i]]--]=i;
for(int k=1;k<=n;k<<=1){
int num=0;
for(int i=n-k+1;i<=n;i++)y[++num]=i;
for(int i=1;i<=n;i++)if(sa[i]>k)y[++num]=sa[i]-k;
for(int i=1;i<=S;i++)c[i]=0;
for(int i=1;i<=n;i++)c[x[i]]++;
for(int i=2;i<=S;i++)c[i]+=c[i-1];
for(int i=n;i>=1;i--){sa[c[x[y[i]]]--]=y[i];y[i]=0;}
for(int i=1;i<=n;i++)swap(x[i],y[i]);
num=1;x[sa[1]]=1;
for(int i=2;i<=n;i++){
if(y[sa[i]]==y[sa[i-1]]&&y[sa[i]+k]==y[sa[i-1]+k])
x[sa[i]]=num;
else x[sa[i]]=++num;
}
if(num==n)break;S=num;
}
}
void LCP(){
int k=0;
for(int i=1;i<=n;i++)rk[sa[i]]=i;
for(int i=1;i<=n;i++){
if(rk[i]==1)continue;
if(k)k--;
int j=sa[rk[i]-1];
while(i+k<=n&&j+k<=n&&s[i+k]==s[j+k])k++;
h[rk[i]]=k;
}
}
void build_ST(){
for(int i=1;i<=n;i++)mn[i][0]=h[i];
for(int j=1;(1<<j)<=n;j++)
for(int i=1;i<=n-(1<<j)+1;i++)
mn[i][j]=min(mn[i][j-1],mn[i+(1<<j-1)][j-1]);
}
int query(int l,int r){
if(l>r)return 0;
int i=lg2[r-l+1];int j=(1<<i);
return min(mn[l][i],mn[r-j+1][i]);
}
void Manacher(){
int l=1,r=0;
for(int i=1;i<=m;++i){
if(r>=i&&i+a[l+r-i]<=r)a[i]=a[l+r-i];
else{
int k=max(0,r-i);
while(i+k<=m&&i-k>=1&&t[i+k]==t[i-k]){
++k;++cnt;
str[cnt][0]=(i-k+2)/2;str[cnt][1]=(i+k-1)/2;
if(str[cnt][0]>str[cnt][1])--cnt;
}
a[i]=k;l=i-k+1;r=i+k-1;
}
}
}
ll solve(int l,int r){
int L=0,R=0,p1=0,p2=0;
L=2;R=rk[l];p1=rk[l]+1;
while(L<=R){
int mid=L+R>>1;
if(query(mid,rk[l])>=r-l+1)p1=mid,R=mid-1;
else L=mid+1;
}
L=rk[l]+1;R=n;p2=rk[l];
while(L<=R){
int mid=L+R>>1;
if(query(rk[l]+1,mid)>=r-l+1)p2=mid,L=mid+1;
else R=mid-1;
}
return 1ll*(p2-p1+2)*(r-l+1);
}
int main(){
scanf("%s",s+1);
n=strlen(s+1);m=2*n+1;t[1]='#';
for(int i=0;(1<<i)<=n;i++)lg2[1<<i]=i;
for(int i=1,lst=0;i<=n;i++){
if(lg2[i])lst=lg2[i];
else lg2[i]=lst;
}
for(int i=n;i>=1;--i)t[2*i]=s[i],t[2*i+1]='#';
Manacher();SA();LCP();build_ST();
for(int i=1;i<=cnt;i++)ans=max(ans,solve(str[i][0],str[i][1]));
printf("%lld\n",ans);
return 0;
}

解法2:回文树模板题。似乎不必多讲。

难度Medium,场切,但是因为数据太水。做法不是以上两种。


B 序列分割

将长为 \(n\) 的数组分 \(k\) 次,每次在某一段内部将其分成两段,得分是分出两段的元素和的乘积。\(k\) 次操作的总得分是每次得分之和。求最大得分并输出方案。

\(n \le 100000\),\(k \le 200\),\(k \lt n\)。


tag:斜率优化DP

首先发现分段的顺序无关紧要,最后的得分是分出各段元素和两两积的和,也就是所有元素总和的平方,减去各段元素和的平方,再折半。所以只用求各段元素和的平方的最小值。

用 \(dp_{i,j}\) 表示前 \(i\) 个分为 \(j\) 段的最小值。\(dp_{i,1}=s_i^2\) 作为初始值,其中 \(s\) 是前缀和。

转移方程是

\[dp_{i,j}=\max_{1\le t \lt i} \{ dp_{t,j-1}+(s_i-s_t)^2) \}
\]

然后按照斜率优化的套路写就可以了。

难度Easy+。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e5+5,K=205;
int n,k,lst[K][N];
ll dp[K][N],s[N],b[N],sum;
int q[N],he,ta;
int main(){
scanf("%d%d",&n,&k);
for(int i=1,x;i<=n;i++){
scanf("%d",&x);
s[i]=s[i-1]+x;sum+=x;
}
memset(dp,0x3f,sizeof(dp));
for(int j=1;j<=n;j++)dp[1][j]=s[j]*s[j];
for(int i=2;i<=k+1;i++){
he=ta=1;q[1]=i-1;
b[i-1]=s[i-1]*s[i-1]+dp[i-1][i-1];
for(int j=i;j<=n;j++){
while(he<ta&&(b[q[he+1]]-b[q[he]])<=2*s[j]*(s[q[he+1]]-s[q[he]]))++he;
dp[i][j]=s[j]*s[j]-2*s[q[he]]*s[j]+b[q[he]];lst[i][j]=q[he];
b[j]=s[j]*s[j]+dp[i-1][j];
while(ta>he&&(b[j]-b[q[ta-1]])*(s[j]-s[q[ta]])>=
(b[j]-b[q[ta]])*(s[j]-s[q[ta-1]]))--ta;
q[++ta]=j;
}
}
printf("%lld\n",(sum*sum-dp[k+1][n])/2);
for(int i=k,j=lst[k+1][n];i>=1;i--){
printf("%d ",j);
j=lst[i][j];
}
return 0;
}

C 连珠线

初始有一个点,用如下两种操作生成一棵树:

Append(w, v):一个新的点 \(w\) 和一个已经添加的点 \(v\) 用红边连接起来。

Insert(w, u, v):一个新的点 \(w\) 插入到用红边连起来的两个点 \(u, v\) 之间。具体过程是删去 \(u, v\) 之间红边,分别用蓝边连接 \(u, w\) 和 \(w, v\)。

给定 \(n\) 个点的最终状态。求蓝边权值之和的最大可能值。

\(n \le 200000\)。


tag:换根DP

首先分析这样生成的树的蓝边分布有什么限制。显然蓝边可以按照加入时间两两配对,且配对的两条边相邻。称它们的公共点为这两条蓝边的中心。

可以把操作看成删边。考虑最后剩下某个点 \(r\) 的情形,将原树看做以 \(r\) 为根的有根树。用 \(dp_{u,0}\) 表示将以 \(u\) 为根的子树删到只剩 \(u\),且 \(u\) 不作为某组蓝边的中心时的最大值。\(dp_{u,1}\) 类似,但表示 \(u\) 作为某个中心的最大值。注意此时 \(u\) 所在的两条蓝边必定包含它向上连的边,否则删去这两条边后图出现多个连通分支,不合题意。这里 \(dp_{u,1}\) 的值不包括向上的这条边。

在此基础上,不难写出转移方程:

\[dp_{u,0}=\sum_{v是u的儿子}(\max \{ dp_{v,0},dp_{v,1}+val_{i} \})
\]
\[dp_{u,1}=dp_{u,0}-\max_{v是u的儿子} \{dp_{v,0}+val_{i}-\max \{ dp_{v,0},dp_{v,1}+val_{i} \}\}
\]

其中 \(val_i\) 表示连接 \(u,v\) 的边的边权。

然后换根即可。只用预处理 \(dp_{v,0}+val_{i}-\max \{ dp_{v,0},dp_{v,1}+val_{i} \}\) 的最大值和次大值。

难度Medium,换根不熟悉。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+5,INF=1<<29;
int n,dp[N][2],g[N][2],ans;
int head[N],nxt[N<<1],ver[N<<1],val[N<<1],tot;
int max(int a,int b){return a>b?a:b;}
void add(int u,int v,int w){
ver[++tot]=v;
val[tot]=w;
nxt[tot]=head[u];
head[u]=tot;
}
void dfs(int u,int fa){
dp[u][0]=0;g[u][0]=g[u][1]=-INF;
for(int i=head[u];i;i=nxt[i]){
int v=ver[i];
if(v==fa)continue;
dfs(v,u);
int tmp=max(dp[v][0],dp[v][1]+val[i]);
dp[u][0]+=tmp;
tmp=dp[v][0]+val[i]-tmp;
if(tmp>g[u][0])g[u][1]=g[u][0],g[u][0]=tmp;
else if(tmp>g[u][1])g[u][1]=tmp;
}
dp[u][1]=g[u][0]+dp[u][0];
}
void redfs(int u,int fa,int in){
for(int i=head[u];i;i=nxt[i]){
int v=ver[i];
if(v==fa)continue;
int t0=dp[u][0],t1=dp[u][1],t2=dp[v][0],t3=dp[v][1];
int tmp=max(dp[v][0],dp[v][1]+val[i]);
dp[u][0]=dp[u][0]-tmp;
if(g[u][0]==val[i]+dp[v][0]-tmp)dp[u][1]=dp[u][0]+max(in,g[u][1]);
else dp[u][1]=dp[u][0]+max(in,g[u][0]);
tmp=max(dp[u][0],dp[u][1]+val[i]);
dp[v][0]=dp[v][0]+tmp;
dp[v][1]=dp[v][0]+max(g[v][0],val[i]+dp[u][0]-tmp);
ans=max(ans,dp[v][0]);
redfs(v,u,val[i]+dp[u][0]-tmp);
dp[u][0]=t0;dp[u][1]=t1;dp[v][0]=t2;dp[v][1]=t3;
}
}
int main(){
scanf("%d",&n);
for(int i=1,u,v,w;i<n;i++){
scanf("%d%d%d",&u,&v,&w);
add(u,v,w);add(v,u,w);
}
dfs(1,-1);ans=dp[1][0];
redfs(1,-1,-INF);
printf("%d\n",ans);
return 0;
}

APIO2013

2023-04-10


A 机器人

给定一个 \(w \times h\) 的带障碍网格图以及 \(n\) 个顺次编号机器人,每次将一个机器人推向某个方向知道碰到墙或障碍才会停止,如果碰到了转向器(不算障碍),会转向继续前进。在停下后若有编号相邻的,则将两个机器人合并,新编号为最小的编号与最大的编号。同一时刻只能有一个机器人移动,求最小推动次数使所有机器人合并。

$ n \le 9 $, $ w,h \le 500 $


tag:分层图DP

用 \(f_{i,j,l,r}\) 表示将编号 \(l\) 到 \(r\) 的机器人合并到点 \((i,j)\) 的最小次数。

两种转移:一种是在某处将两个机器人合并,另一种是同一个机器人的移动。

前者直接枚举合并即可,后者跑最短路。

难度Hard-。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef pair<int,int> pii;
const int N=502,INF=0x3f3f3f3f;
int T,m,n,L,R,f[N][N][10][10];
char a[N][N];bool used[N][N];pii to[N][N][4];
int dx[]={0,1,0,-1},dy[]={1,0,-1,0};
pii push(int x,int y,int d){
if(to[x][y][d]!=pii(0,0))return to[x][y][d];
to[x][y][d]=pii(-1,-1);
int dd=d;
if(a[x][y]=='C')dd=(d+1)%4;
if(a[x][y]=='A')dd=(d+3)%4;
int xx=x+dx[dd],yy=y+dy[dd];
if(xx>n||xx<1||yy>m||yy<1||a[xx][yy]=='x')
return (to[x][y][d]=pii(x,y));
return (to[x][y][d]=push(xx,yy,dd));
}
struct Pair_Queue{
int he,ta,t[N*N];
pii q[N*N],c[N*N];
inline void init(){memset(t,0,sizeof(t));}
inline void reset(){he=1;ta=0;}
inline bool empty(){return ta<he;}
inline void push(int x,int y){q[++ta]=pii(x,y);}
inline pii front(){return q[he];}
inline void pop(){++he;}
#define dis(k) f[(k).first][(k).second][L][R]
inline void sort(){
int mn=INF,mx=0;
for(int i=he,val;val=dis(q[i]),i<=ta;i++)
c[i]=q[i],mn=min(mn,val),mx=max(mx,val),t[val]++;
for(int i=mn+1;i<=mx;i++)t[i]+=t[i-1];
for(int i=he;i<=ta;i++)q[t[dis(c[i])]--]=c[i];
for(int i=mn;i<=mx;i++)t[i]=0;
}
#undef dis
}Q1,Q2;
void bfs(int l,int r){
Q1.reset();Q2.reset();L=l;R=r;
for(int i=1;i<=n;++i)
for(int j=1;j<=m;++j)
if(f[i][j][l][r]<INF)Q1.push(i,j);
if(Q1.empty())return;
memset(used,0,sizeof(used));Q1.sort();pii u;
#define dis(k) f[(k).first][(k).second][L][R]
#define use(k) used[(k).first][(k).second]
for(int x,y;!Q1.empty()||!Q2.empty();){
if(Q1.empty())u=Q2.front(),Q2.pop();
else if(Q2.empty())u=Q1.front(),Q1.pop();
else if(dis(Q1.front())<dis(Q2.front()))u=Q1.front(),Q1.pop();
else u=Q2.front(),Q2.pop();
x=u.first;y=u.second;used[x][y]=1;
for(int d=0;d<4;d++){
int xx=to[x][y][d].first,yy=to[x][y][d].second;
if(xx<1||xx>n||yy<1||yy>m||a[xx][yy]=='x')continue;
if(f[x][y][l][r]+1<f[xx][yy][l][r]){
f[xx][yy][l][r]=f[x][y][l][r]+1;
used[xx][yy]=1;Q2.push(xx,yy);
}
}
while(!Q1.empty()&&use(Q1.front()))Q1.pop();
}
}
int main(){
memset(f,0x3f,sizeof(f));Q1.init();Q2.init();
scanf("%d%d%d",&T,&m,&n);
for(int i=1;i<=n;++i)scanf("%s",a[i]+1);
for(int i=1;i<=n;++i)
for(int j=1;j<=m;++j)
if(isdigit(a[i][j])){
int v=a[i][j]-'0';
f[i][j][v][v]=0;
}
for(int i=1;i<=n;++i)
for(int j=1;j<=m;++j)
for(int d=0;d<4;++d)
if(a[i][j]!='x')to[i][j][d]=push(i,j,d);
for(int len=1;len<=T;++len)
for(int l=1,r;r=l+len-1,r<=T;++l){
for(int mid=l;mid<r;++mid)
for(int i=1;i<=n;++i)for(int j=1;j<=m;++j)
f[i][j][l][r]=min(f[i][j][l][r],
f[i][j][l][mid]+f[i][j][mid+1][r]);
bfs(l,r);
}
int ans=INF;
for(int i=1;i<=n;++i)
for(int j=1;j<=m;++j)ans=min(ans,f[i][j][1][T]);
printf("%d\n",ans<INF?ans:-1);
return 0;
}

B 道路费用

给定 \(n\) 个点,\(m\) 条边的图,点,边都有权值,边权互不相同。现新加入 \(k\) 条边,权值自定,要求最大化图中某一棵最小生成树的代价。这里代价定义为:每个点到 \(1\) 号点的路径上新加入边的权值和乘以点权,再对所有点求和。求这个最大代价。

\(n \le 100000\),\(m \le 300000\),\(k \le 20\)。


tag:最小生成树,缩点

注意 \(k\) 很小。

先求原图的最小生成树。然后钦定 \(k\) 条边的权值为0,再求最小生成树。这样最小生成树其余至少 \(n-k\) 条边是必选的,\(k\) 条边是可选的。对这些必选边建图缩点,图中至多剩下 \(k+1\) 个点。问题就可以转化为这样的小情况。

二进制枚举 \(k\) 条边中选用的边,对每种选择跑最小生成树。然后用上述剩下 \(k\) 条边中没有选入的边,找到树上包含它的环,更新环上边的最大权值即可。

难度Medium+。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n,m,k,num,p[N],q[25][2],P[N],no[N];
int head[N],nxt[N<<1],ver[N<<1],tot=1;
int b[25],c[25],d[25],f[25],g[25];long long ans,a[25],sz[25];
int find(int u){return (u==P[u]?u:(P[u]=find(P[u])));}
struct edge{int u,v,w;}e[N*3];
void add(int u,int v){
ver[++tot]=v;
nxt[tot]=head[u];
head[u]=tot;
}
bool operator <(const edge &a,const edge &b){return a.w<b.w;}
void dfs(int u,int fa,int t){
no[u]=t;a[t]+=p[u];
for(int i=head[u];i;i=nxt[i])
if(ver[i]!=fa)dfs(ver[i],u,t);
}
void rdfs(int u){
sz[u]=a[u];
for(int i=head[u],v;i;i=nxt[i]){
v=ver[i];
if(v==f[u])continue;
f[v]=u;d[v]=d[u]+1;
rdfs(v);sz[u]+=sz[v];
}
}
int main(){
scanf("%d%d%d",&n,&m,&k);
for(int i=1,u,v,w;i<=m;i++)scanf("%d%d%d",&e[i].u,&e[i].v,&e[i].w);
for(int i=1;i<=k;i++)scanf("%d%d",&q[i][0],&q[i][1]);
for(int i=1;i<=n;i++)scanf("%d",p+i);
for(int i=1;i<=n;i++)P[i]=i;
sort(e+1,e+m+1);
for(int i=1,cnt=0;i<=m;i++){
int u=e[i].u,v=e[i].v;
int fu=find(u),fv=find(v);
if(fu==fv)continue;
P[fu]=fv;e[++cnt]=e[i];
}
for(int i=1;i<=n;i++)P[i]=i;
for(int i=1;i<=k;i++){
int fu=find(q[i][0]),fv=find(q[i][1]);
if(fu==fv)continue;P[fu]=fv;
}
for(int i=1;i<n;i++){
int fu=find(e[i].u),fv=find(e[i].v);
if(fu==fv){e[++num]=e[i];continue;}
add(e[i].u,e[i].v);add(e[i].v,e[i].u);P[fu]=fv;
}
for(int i=1,cnt=0;i<=n;i++)if(!no[i])dfs(i,-1,++cnt);
for(int i=1;i<=num;i++)e[i].u=no[e[i].u],e[i].v=no[e[i].v];
for(int i=1;i<=k;i++)q[i][0]=no[q[i][0]],q[i][1]=no[q[i][1]];
for(int st=1;st<(1<<k);st++){
long long res=0;
tot=1;for(int i=1;i<=num+1;i++)head[i]=0;
for(int i=1;i<=k;i++)b[i]=(st>>i-1)&1;
for(int i=1;i<=num+1;i++){P[i]=i;g[i]=1e9;}
for(int i=1;i<=k;i++)if(b[i]){
int fu=find(q[i][0]),fv=find(q[i][1]);
if(fu==fv){res=-1;break;}
add(q[i][0],q[i][1]);add(q[i][1],q[i][0]);P[fu]=fv;
}
if(res==-1)continue;
for(int i=1;i<=num;i++){
int fu=find(e[i].u),fv=find(e[i].v);
if(fu==fv){c[i]=0;continue;}
P[fu]=fv;c[i]=1;
add(e[i].u,e[i].v);add(e[i].v,e[i].u);
}
rdfs(1);
for(int i=1;i<=num;i++)if(!c[i]){
int u=e[i].u,v=e[i].v,w=e[i].w;
if(d[u]<d[v])swap(u,v);
while(d[u]>d[v]){g[u]=min(g[u],w);u=f[u];}
while(u!=v){g[u]=min(g[u],w);g[v]=min(g[v],w);u=f[u];v=f[v];}
}
for(int i=1;i<=k;i++)if(b[i]){
int u=q[i][0],v=q[i][1];
if(d[u]>d[v])swap(u,v);
res+=1ll*sz[v]*g[v];
}
ans=max(ans,res);
}
printf("%lld\n",ans);
return 0;
}

C 出题人

提交答案题。

目标是造数据,输入有数字个数限制。循环超过 \(10^6\) 次算作超时。

Q1:Floyd,Heap-Dijkstra,BellmanFord 三种算法,任取两个(有序),卡掉一个而放另一个过。

Q2:求图的色数,目标算法是从小到大枚举并判断,要求放它过和卡掉它。点数 \(\gt 70\),边数 \(\gt 1500\)。


tag:思维题

卡 Floyd 非常简单。

卡 BellmanFord 只要一堆自环,重边即可。

卡 Heap-Dijkstra 需要构造连续的三角形,利用负权边把它卡到指数级别。

Q2要卡掉随便构造都可以,要放过只要构造二分图。

难度Medium。


APIO2012

2023-03-29


A 派遣

\(N\) 个人的从属关系构成一棵树,第 \(i\) 个人的直接上司是 \(B_i\)(\(B_i \lt i\),\(B_i=0\) 表示i为根),每个人有两个参数:薪水 \(C_i\) 和领导力 \(L_i\)。现在要确定一个管理者,选择其若干下属(直接或间接,可以不含管理者),使得选出的人得薪水总和不超过给定的预算 \(M\)。在此条件下,最大化管理者的领导力与选出的人数之积。

\(1 \le N \le 10^5\),\(1 \le M \le 10^9\),\(0 \le B_i \lt i\),\(1 \le C_i \le M\),\(1 \le L_i \le 10^9\)。


tag:线段树合并 / 堆、启发式合并

先考虑固定管理者的情形。此时要最大化选出的人数,也就是要在这棵子树中从小到大依次取 \(C_i\),使得总和不超过 \(m\)。

这有两种方法维护:一是用线段树合并,二分查找,时间复杂度 \(O(n \log ^2n)\)。二是用优先队列启发式合并,只维护和不超过 \(m\) 的部分,多的删掉。此时每个元素至多删除一次,所以时间复杂度 \(O(n \log n)\)。

实现了第一种。

难度Easy,场切。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<ll,int> pli;
pli operator +(const pli &a,const pli &b){
return make_pair(a.first+b.first,a.second+b.second);
};
const int N=1e5+5;
int n,a[N],b[N],num[N];ll m,c[N],ans;
int head[N],nxt[N],ver[N],tot;
void add(int u,int v){
ver[++tot]=v;
nxt[tot]=head[u];
head[u]=tot;
}
int rt[N],segtot;
ll sum[N*20];int cnt[N*20],lc[N*20],rc[N*20];
void modify(int p,int l,int r,int x){
if(l==r){sum[p]+=b[x];++cnt[p];return;}
int mid=l+r>>1;
if(x<=mid){
if(!lc[p])lc[p]=++segtot;
modify(lc[p],l,mid,x);
}
else{
if(!rc[p])rc[p]=++segtot;
modify(rc[p],mid+1,r,x);
}
sum[p]=sum[lc[p]]+sum[rc[p]];
cnt[p]=cnt[lc[p]]+cnt[rc[p]];
}
pli query(int p,int l,int r,int L,int R){
if(!p)return make_pair(0,0);
if(l>=L&&r<=R)return make_pair(sum[p],cnt[p]);
int mid=l+r>>1;pli res=make_pair(0,0);
if(L<=mid)res=res+query(lc[p],l,mid,L,R);
if(R>mid)res=res+query(rc[p],mid+1,r,L,R);
return res;
}
int merge(int p,int q){
if(!p)return q;
if(!q)return p;
sum[p]+=sum[q];cnt[p]+=cnt[q];
lc[p]=merge(lc[p],lc[q]);
rc[p]=merge(rc[p],rc[q]);
return p;
}
void dfs(int u){
rt[u]=++segtot;
modify(rt[u],1,n,c[u]);
for(int i=head[u],v;i;i=nxt[i]){
v=ver[i];dfs(v);
rt[u]=merge(rt[u],rt[v]);
}
int l=1,r=n,res=0;
while(l<=r){
int mid=l+r>>1;
pli p=query(rt[u],1,n,1,mid);
if(p.first<=m)l=mid+1,res=p.second;
else r=mid-1;
}
ans=max(ans,1ll*res*a[u]);
}
int main(){
scanf("%d%lld",&n,&m);
for(int i=1,fa;i<=n;i++){
scanf("%d%lld%d",&fa,c+i,a+i);b[i]=c[i];
if(fa)add(fa,i);
}
sort(b+1,b+n+1);
for(int i=1;i<=n;i++){
int l=1,r=n,pos=0;
while(l<=r){
int mid=l+r>>1;
if(b[mid]<c[i])l=mid+1;
else r=mid-1;
if(b[mid]==c[i])pos=mid;
}
c[i]=pos+num[pos];
num[pos]++;
}
dfs(1);
printf("%lld\n",ans);
return 0;
}

B 守卫

长为 \(n\) 的 \(01\) 数列中有 \(k\) 个 \(1\),满足 \(m\) 个条件。每个条件形如 \(a_i\),\(b_i\),\(c_i\),含义如下:

  • \(c_i=0\) 时,区间 \([a_i,b_i]\) 中没有 \(1\)。
  • \(c_i=1\) 时,区间 \([a_i,b_i]\) 中有 \(1\)。

保证初始有解。求所有必定为 \(1\) 的位置。

\(1 \le n \le 10^5\),\(0 \le m \lt 10^5\),\(1 \le k \le n\),\(1 \le a_i \le b_i \le n\)。


tag:贪心

首先解决简单情况:

  1. 丢掉所有条件给出为 \(0\) 的位置。
  2. 如果剩下的可能位置数与总数相同,剩下的所有位置都满足条件。

    3.如果有某个 \(c_i=1\) 的区间长为 \(1\),这个位置满足条件。

    4.如果有某两个 \(c_i=1\) 的区间有包含关系,只用考虑小的那个。

下面来考虑剩下的情形。对所有区间按左端点递增排序,则右端点也递增。

首先可以贪心求出一组解(不考虑 \(k\)),这只要从左到右扫描所有区间,如果某个区间还没有 \(1\),在其右端点放 \(1\)。同时可以求出满足前 \(i\) 个区间所需要的 \(1\) 的数目的最小值 \(f_i\)。类似的,可以求出满足后 \(i\) 个区间所需要的 \(1\) 的数目的最小值 \(g_i\)。

“某个位置必须是 \(1\)”等价于“某个位置是 \(0\) 时无解”。只用考虑那些满足 \(f_i = f_{i-1} + 1\) 的区间 \([a_i,b_i]\) 的右端点,记为 \(x\)。则满足前 \(i\) 个区间的条件需要 \(f_i\) 个 \(1\)。

再考虑所有完全在 \(x-1\) 右侧的区间,设为后 \(p\) 个。假设 \(x\) 不是 \(1\),所以满足前 \(i\) 个区间时不会满足后 \(p\) 个区间中的任何一个。那么无解的充分必要条件就是 \(f_i + g_p \gt k\)。

\(p\) 可以二分求。时间复杂度 \(O(n \log n)\)。

难度Medium+。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n,k,m,d[N],t,h[N],lst[N],nxt[N],cnt,st[N],top,f[N],g[N],flag;
struct range{int l,r;}p[N];
bool operator <(const range &a,const range &b){
return a.l!=b.l?a.l<b.l:a.r<b.r;
}
int main(){
scanf("%d%d%d",&n,&k,&m);
for(int i=1,a,b,c;i<=m;i++){
scanf("%d%d%d",&a,&b,&c);
if(c==0)++d[a],--d[b+1];
else{++t;p[t].l=a;p[t].r=b;}
}
for(int i=1;i<=n;i++){
d[i+1]+=d[i];
if(!d[i]){lst[i]=nxt[i]=++cnt;h[cnt]=i;}
}
if(k==cnt){
for(int i=1;i<=cnt;i++)printf("%d\n",h[i]);
printf("\n");
return 0;
}
for(int i=1;i<=n;i++)if(!lst[i])lst[i]=lst[i-1];
for(int i=n;i>=1;i--)if(!nxt[i])nxt[i]=nxt[i+1];
for(int i=1;i<=t;i++)p[i].l=nxt[p[i].l],p[i].r=lst[p[i].r];
sort(p+1,p+t+1);top=0;
for(int i=1;i<=t;i++){
if(p[i].l>p[i].r)continue;
while(top&&p[st[top]].r>=p[i].r)--top;
st[++top]=i;
}t=top;
for(int i=1;i<=t;i++)p[i]=p[st[i]];
int mx=0,mn=1e9;
for(int i=1;i<=t;i++){
if(p[i].l>mx)f[i]=f[i-1]+1,mx=p[i].r;
else f[i]=f[i-1];
}
for(int i=t;i>=1;i--){
if(p[i].r<mn)g[i]=g[i+1]+1,mn=p[i].l;
else g[i]=g[i+1];
}
for(int i=1;i<=t;i++){
if(p[i].l==p[i].r){flag=1;printf("%d\n",h[p[i].l]);continue;}
if(f[i]!=f[i-1]+1)continue;
int pos=t+1,l=i+1,r=t;
while(l<=r){
int mid=l+r>>1;
if(p[mid].l>p[i].r-1)pos=mid,r=mid-1;
else l=mid+1;
}
if(f[i]+g[pos]>k){flag=1;printf("%d\n",h[p[i].r]);}
}
if(!flag)printf("-1\n");
return 0;
}

C 苦无

\(W \times H\) 的表格中,有 \(n\) 个质点从某方格中心同时开始运动。没有两个质点初始时在同一方格中。所有质点以同样的速度做直线运动,方向为上、下、左、右之一。如果两个或多个质点在同一时刻位于同一位置,则这些质点相撞并消失。求被质点经过的格子数。

这里每个质点用三个参数 \(x\),\(y\),\(d\) 来描述,表示其开始运动的位置是从左往右的 \(x\) 列、从上往下的第 \(y\) 行的方格的中心。运动的方向由 \(d\) 表示,分别为:

  • \(d = 0\),表示向右;
  • \(d = 1\),表示向上;
  • \(d = 2\),表示向左;
  • \(d = 3\),表示向下。

\(1 \le n \le 10^5\),\(1 \le w \le 10^9\),\(1 \le h \le 10^9\);\(1 \le x \le W\),\(1 \le y \le H\),\(0 \le d \le 3\)。


tag:模拟、堆、扫描线

只用求出每个质点的运动路程,然后看作宽为 \(1\) 的矩形做扫描线即可。

求运动路程,就只需要求相撞的情况。相撞质点的方向有 \(6\) 种可能,如同一行,左边的向右,右边的向左;再如同一条左上—右下对角线,左边的向下,右边的向左;等等。

在每一种可能中,最先相撞的一定是两个相邻的点。一旦遇到两个相撞,就要删去这两个点,考虑新产生的相邻对,于是使用链表。

按照 \(6\) 种情形,\(4\) 个方向排序,排序后构建链表。然后将所有相撞按时间放入一个堆中,每次取出某个时刻所有的相撞,判断是否存在(可能已经撞过)。如果是存在的相撞,就更新链表和相撞。

细节:

  1. 排序的时候要先按照方向分开。
  2. 可以假设质点每秒运动半个方格边长,这样所有的时间都是整数。

难度Medium+。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=2e5+5,INF=1<<30;
int n,W,H,X[N<<1];ll ans;
struct P{int no,x,y,d,nxt[6],lst[6],t,ex;}a[N];
int fir[]={0,3,0,3,0,1},sec[]={2,1,1,2,3,2},dx[]={1,0,-1,0},dy[]={0,-1,0,1};
bool cmp(P a,P b){return a.no<b.no;}
bool cmp1(P a,P b){
if((a.d==0||a.d==2)&&(b.d==1||b.d==3))return true;
if((b.d==0||b.d==2)&&(a.d==1||a.d==3))return false;
return a.y!=b.y?a.y<b.y:a.x<b.x;
}
bool cmp2(P a,P b){
if((a.d==1||a.d==3)&&(b.d==0||b.d==2))return true;
if((b.d==1||b.d==3)&&(a.d==0||a.d==2))return false;
return a.x!=b.x?a.x<b.x:a.y<b.y;
}
bool cmp3(P a,P b){
if((a.d==0||a.d==1)&&(b.d==2||b.d==3))return true;
if((b.d==0||b.d==1)&&(a.d==2||a.d==3))return false;
return a.x-a.y!=b.x-b.y?a.x-a.y<b.x-b.y:a.x<b.x;
}
bool cmp4(P a,P b){
if((a.d==0||a.d==3)&&(b.d==1||b.d==2))return true;
if((b.d==0||b.d==3)&&(a.d==1||a.d==2))return false;
return a.x+a.y!=b.x+b.y?a.x+a.y<b.x+b.y:a.x<b.x;
}
struct hit{int t,p,q;};
bool operator <(const hit &a,const hit &b){return a.t>b.t;}
priority_queue<hit> Q;
struct ScanLine{
ll l,r,h;int op;
bool operator <(const ScanLine &b){
return h<b.h;
}
}line[N<<1];
struct SegmentTree{
int ls[N<<3],rs[N<<3],sum[N<<3];ll len[N<<3];
#define lc p<<1
#define rc p<<1|1
void push_up(int p){
if(sum[p])len[p]=X[rs[p]+1]-X[ls[p]];
else len[p]=len[lc]+len[rc];
}
void build(int p,int l,int r){
ls[p]=l;rs[p]=r;len[p]=sum[p]=0;
if(l==r)return;
build(lc,l,l+r>>1);
build(rc,(l+r>>1)+1,r);
}
void modify(int p,int L,int R,int c){
if(X[rs[p]+1]<=L||R<=X[ls[p]])return;
if(L<=X[ls[p]]&&X[rs[p]+1]<=R){sum[p]+=c;push_up(p);return;}
modify(lc,L,R,c);modify(rc,L,R,c);
push_up(p);
}
}seg;
int main(){
//freopen("kunai.in","r",stdin);
//freopen("kunai.out","w",stdout);
scanf("%d%d%d",&W,&H,&n);
for(int i=1;i<=n;i++){
scanf("%d%d%d",&a[i].x,&a[i].y,&a[i].d);
a[i].ex=1;a[i].no=i;
for(int k=0;k<6;k++)a[i].nxt[k]=a[i].lst[k]=-1;
switch(a[i].d){
case 0:a[i].t=2*(W-a[i].x);break;
case 1:a[i].t=2*(a[i].y-1);break;
case 2:a[i].t=2*(a[i].x-1);break;
case 3:a[i].t=2*(H-a[i].y);break;
}
}
a[n+1].d=2;a[0].d=0;
sort(a+1,a+n+1,cmp1);
for(int i=1;i<n;i++){
if(a[i].d==1||a[i].d==3)break;
if(a[i].y==a[i+1].y)a[i].nxt[0]=a[i+1].no,a[i+1].lst[0]=a[i].no;
}
sort(a+1,a+n+1,cmp2);
for(int i=1;i<n;i++){
if(a[i].d==0||a[i].d==2)break;
if(a[i].x==a[i+1].x)a[i].nxt[1]=a[i+1].no,a[i+1].lst[1]=a[i].no;
}
sort(a+1,a+n+1,cmp3);
for(int i=0,op=2;i<n;i++){
if((a[i].d==0||a[i].d==1)&&(a[i+1].d==2||a[i+1].d==3)){op=3;continue;}
if(i!=0&&a[i].x-a[i].y==a[i+1].x-a[i+1].y)
a[i].nxt[op]=a[i+1].no,a[i+1].lst[op]=a[i].no;
}
sort(a+1,a+n+1,cmp4);
for(int i=0,op=4;i<n;i++){
if((a[i].d==0||a[i].d==3)&&(a[i+1].d==1||a[i+1].d==2)){op=5;continue;}
if(i!=0&&a[i].x+a[i].y==a[i+1].x+a[i+1].y)
a[i].nxt[op]=a[i+1].no,a[i+1].lst[op]=a[i].no;
}
sort(a+1,a+n+1,cmp);
for(int k=0;k<6;k++)
for(int i=1;i<=n;i++)
if(a[i].nxt[k]!=-1&&a[i].d==fir[k]&&a[a[i].nxt[k]].d==sec[k])
Q.push({abs(a[i].x-a[a[i].nxt[k]].x)+abs(a[i].y-a[a[i].nxt[k]].y),i,a[i].nxt[k]});
while(!Q.empty()){
int t0=Q.top().t;
while(!Q.empty()&&Q.top().t==t0){
hit tmp=Q.top();int i=tmp.p,j=tmp.q;Q.pop();
if(!a[i].ex&&a[i].t!=t0||!a[j].ex&&a[j].t!=t0)continue;
if(a[i].ex){
for(int k=0;k<6;k++){
int LS=a[i].lst[k],NX=a[i].nxt[k];
if(LS!=-1)a[LS].nxt[k]=NX;
if(NX!=-1)a[NX].lst[k]=LS;
if(LS!=-1&&NX!=-1&&a[LS].d==fir[k]&&a[NX].d==sec[k])
Q.push({abs(a[LS].x-a[NX].x)+abs(a[LS].y-a[NX].y),LS,NX});
}
}
if(a[j].ex){
for(int k=0;k<6;k++){
int LS=a[j].lst[k],NX=a[j].nxt[k];
if(LS!=-1)a[LS].nxt[k]=NX;
if(NX!=-1)a[NX].lst[k]=LS;
if(LS!=-1&&NX!=-1&&a[LS].d==fir[k]&&a[NX].d==sec[k])
Q.push({abs(a[LS].x-a[NX].x)+abs(a[LS].y-a[NX].y),LS,NX});
}
}
a[i].t=a[j].t=t0;a[i].ex=a[j].ex=0;
}
}
for(int i=1;i<=n;i++){
int dist=a[i].t,X1,Y1,X2,Y2;
if(dist%2==0)dist/=2;
switch(a[i].d){
case 0:{X1=a[i].x;X2=a[i].x+dist+1;Y1=a[i].y;Y2=a[i].y+1;break;}
case 1:{X1=a[i].x;X2=a[i].x+1;Y1=a[i].y-dist;Y2=a[i].y+1;break;}
case 2:{X1=a[i].x-dist;X2=a[i].x+1;Y1=a[i].y;Y2=a[i].y+1;break;}
case 3:{X1=a[i].x;X2=a[i].x+1;Y1=a[i].y;Y2=a[i].y+dist+1;break;}
}
X[2*i-1]=X1;X[2*i]=X2;
line[2*i-1]={X1,X2,Y1,1};line[2*i]={X1,X2,Y2,-1};
}
sort(line+1,line+2*n+1);
sort(X+1,X+2*n+1);
int tot=unique(X+1,X+2*n+1)-X-1;
seg.build(1,1,tot-1);
for(int i=1;i<2*n;i++){
seg.modify(1,line[i].l,line[i].r,line[i].op);
ans+=seg.len[1]*(line[i+1].h-line[i].h);
}
printf("%lld\n",ans);
return 0;
}

Competition Set - 模拟赛 I的更多相关文章

  1. NOIP模拟赛20161022

    NOIP模拟赛2016-10-22 题目名 东风谷早苗 西行寺幽幽子 琪露诺 上白泽慧音 源文件 robot.cpp/c/pas spring.cpp/c/pas iceroad.cpp/c/pas ...

  2. NOI模拟赛 Day1

    [考完试不想说话系列] 他们都会做呢QAQ 我毛线也不会呢QAQ 悲伤ING 考试问题: 1.感觉不是很清醒,有点困╯﹏╰ 2.为啥总不按照计划来!!! 3.脑洞在哪里 4.把模拟赛当作真正的比赛,紧 ...

  3. NOIP第7场模拟赛题解

    NOIP模拟赛第7场题解: 题解见:http://www.cqoi.net:2012/JudgeOnline/problemset.php?page=13 题号为2221-2224. 1.car 边界 ...

  4. contesthunter暑假NOIP模拟赛第一场题解

    contesthunter暑假NOIP模拟赛#1题解: 第一题:杯具大派送 水题.枚举A,B的公约数即可. #include <algorithm> #include <cmath& ...

  5. NOIP模拟赛 by hzwer

    2015年10月04日NOIP模拟赛 by hzwer    (这是小奇=> 小奇挖矿2(mining) [题目背景] 小奇飞船的钻头开启了无限耐久+精准采集模式!这次它要将原矿运到泛光之源的矿 ...

  6. 小奇模拟赛9.13 by hzwer

    2015年9月13日NOIP模拟赛 by hzwer    (这是小奇=> 小奇挖矿(explo) [题目背景] 小奇要开采一些矿物,它驾驶着一台带有钻头(初始能力值w)的飞船,按既定路线依次飞 ...

  7. PKUSC 模拟赛 day1 下午总结

    下午到了机房之后又困又饿,还要被强行摁着看英文题,简直差评 第一题是NOIP模拟赛的原题,随便模拟就好啦 本人模拟功力太渣不小心打错了个变量,居然调了40多分钟QAQ #include<cstd ...

  8. [GRYZ]寒假模拟赛

    写在前面 这是首次广饶一中的OIERS自编自导,自出自做(zuo)的模拟赛. 鉴于水平气压比较低,机(wei)智(suo)的WMY/XYD/HYXZC就上网FQ下海找了不少水(fei)题,经过他们优( ...

  9. BZOJ2741: 【FOTILE模拟赛】L

    2741: [FOTILE模拟赛]L Time Limit: 15 Sec  Memory Limit: 162 MBSubmit: 1170  Solved: 303[Submit][Status] ...

  10. 大家AK杯 灰天飞雁NOIP模拟赛题解/数据/标程

    数据 http://files.cnblogs.com/htfy/data.zip 简要题解 桌球碰撞 纯模拟,注意一开始就在袋口和v=0的情况.v和坐标可以是小数.为保险起见最好用extended/ ...

随机推荐

  1. Java AES CBC模式 加密和解密

    import org.apache.tomcat.util.codec.binary.Base64; import javax.crypto.Cipher; import javax.crypto.s ...

  2. 基于proteus的4026的二分频计数

    基于proteus的4026的二分频计数 1.芯片原理 4026还是一个CMOS芯片,是直接输出段码的计数器.显然,这个芯片的作用就是和七段数码管配合,直接将计数结果显示在数码管上.这里只是用于分频, ...

  3. urllib+BeautifulSoup爬取并解析2345天气王历史天气数据

    urllib+BeautifulSoup爬取并解析2345天气王历史天气数据 网址:东城历史天气查询_历史天气预报查询_2345天气预报 1.代码 import json import logging ...

  4. #根号分治,分块#洛谷 5309 [Ynoi2011] 初始化

    题目传送门 分析 如果 \(x\) 比较大那么可以暴力修改,\(x\) 比较小的话可以用数组打标记 查询的时候对于暴力修改的部分可以分块,暴力修改的同时需要给块打标记 如果 \(x\) 比较小的情况, ...

  5. #dp,vector#AT2567 [ARC074C] RGB Sequence

    题目 分析 一种很正常的想法就是设\(dp[i][j][k]\), 表示前\(i\)个格子,其它两种颜色出现的位置分别为\(j,k,j>k或j=k=0(可取两种颜色)\)的方案数 那么颜色种类限 ...

  6. #特征方程,dp,快速幂#洛谷 4451 [国家集训队]整数的lqp拆分

    题目 分析 设\(dp[n]\)表示答案,因为\(dp[n]=\sum\prod_{i=1}^mF_{a_i}\) \(dp[n]=\sum_{i=1}^{n-1}dp[i]*F_{n-i-1}\) ...

  7. 李俊刚:我是如何在OpenHarmony完成ap6275s WiFi驱动的HDF适配工作的?

    编者按:在 OpenHarmony 生态发展过程中,涌现了大批优秀的代码贡献者,本专题旨在表彰贡献.分享经验,文中内容来自嘉宾访谈,不代表 OpenHarmony 工作委员会观点. 李俊刚 深圳开鸿数 ...

  8. HR必备|可视化大屏助HR实现人才资源价值最大化

    人力资源管理质量的优劣关系到企业可持续发展目标的实现,在信息化时代背景下,应用信息技术加强人力资源管理过程的优化,利用技术提升人力资源管理质量和效率已是大势所趋. 利用信息技术构建信息化人力资源管理平 ...

  9. 面试连环炮系列(二十️五):RocketMQ怎么保证消息不丢失

    RocketMQ怎么保证消息不丢失? A. 从Producer的视角来看:如果消息未能正确的存储在MQ中,或者消费者未能正确的消费到这条消息,都是消息丢失. B. 从Broker的视角来看:如果消息已 ...

  10. WGAN

    wgan之前, 原始GAN出现了什么问题? https://www.cnblogs.com/Allen-rg/p/10305125.html 判别器越好,生成器梯度消失越严重 一句话概括:最小化第二种 ...