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. KingbaseESV8R6延迟提交参数

    前言 队列理论在我们生活中的应用随处可见,例如我们去食堂打饭需要排队,我们生活中随处可见排队的场景. 在计算机领域中,性能诊断等地方使用队列理论的案例也很多.服务器硬件分为动态设备和静态设备.CPU和 ...

  2. 22 axios和axios拦截器

    1. axios 由于jquery有严重的地狱回调逻辑. 再加上jquery的性能逐年跟不上市场节奏. 很多前端工程师采用axios来发送ajax. 相比jquery. axios更加灵活. 且容易使 ...

  3. java中DelayQueue的使用

    目录 简介 DelayQueue DelayQueue的应用 总结 java中DelayQueue的使用 简介 今天给大家介绍一下DelayQueue,DelayQueue是BlockingQueue ...

  4. centos部署Django二:项目上传及测试

    1. 上传项目 用 ftp 或者 sftp 上传项目到服务器. *:如果上传时,报各种错误,可以考虑下是不是服务器中文件夹权限的问题.如果是权限的问题,可以使用命令修改文件夹权限后在上传:chmod ...

  5. Quanto: PyTorch 量化工具包

    量化技术通过用低精度数据类型 (如 8 位整型 (int8)) 来表示深度学习模型的权重和激活,以减少传统深度学习模型使用 32 位浮点 (float32) 表示权重和激活所带来的计算和内存开销. 减 ...

  6. openGauss/MogDB-3.0.0 dcf测试(非om安装)

    openGauss/MogDB-3.0.0 dcf 测试(非 om 安装) 本文出处:https://www.modb.pro/db/402037 IP 地址 ... LERDER ... FOLLO ...

  7. 什么是coredump

    什么是 coredump 介绍 在 Linux 开发中,我们经常听到程序员说我的程序 core 掉了,通常出现这类的问题是低级 bug 中的内存访问越界.使用空指针.堆栈溢出等情况.使程序运行过程中异 ...

  8. openGauss3.1.0 版本的gs_stack功能解密

    openGauss3.1.0 版本的 gs_stack 功能解密 不管是测试还是研发,工作中总有遇到各种各样的问题.比如,你有没有遇到过在数据库中执行某个 SQL,却一直不返回结果,这时候的你是不是非 ...

  9. 双端队列的基本实现【数据结构与算法—TypeScript 实现】

    笔记整理自 coderwhy 『TypeScript 高阶数据结构与算法』课程 特性 本质:允许队列在两端进行 入队 和 出队 操作 设计 实现方式:基于 数组 实现 属性: data:存放队列元素 ...

  10. 【译】Visual Studio 中的 GitHub Copilot:2023年回顾

    在快速发展的软件开发世界中,保持领先是至关重要的.在 Visual Studio 中引入AI,特别是 GitHub Copilot,已经彻底改变了开发人员的编码方式.通过将 Copilot 集成到 V ...