HNOI/AHOI2018题解
[HNOI/AHOI2018]道路
这应该是最水的题。
首先考虑正着dp,发现状态不太好表示,考虑倒着dp。
设\(dp[u][i][j]\)表示\(u\)号点,它到根节点的路径上有\(i\)条未翻修的公路和\(j\)条未返修的铁路。
转移就很简单:
- \(dp[u][i][j]=c_u (a_u+i)(b_u+j)\) ,\(u\)为叶子节点。
- \(dp[u][i][j]=min(dp[s[u]][i+1][j]+dp[t[u]][i][j],dp[s[u]][i][j]+dp[t[u]][i][j+1])\),\(u\)不为叶子节点。
答案就是\(dp[1][0][0]\)
Code:
#include<bits/stdc++.h>
using namespace std;
const int maxn=40010;
int n,s[maxn],t[maxn];
int a[maxn],b[maxn],c[maxn];
map<int,long long >f[maxn][41];
void dfs(int x,int A,int B)
{
if(!s[x])
{
for(int i=0;i<=A;i++)
for(int j=0;j<=B;j++)
f[x][i][j]=1ll*c[x]*(a[x]+i)*(b[x]+j);
return ;
}
dfs(s[x],A+1,B);dfs(t[x],A,B+1);
for(int i=0;i<=A;i++)
for(int j=0;j<=B;j++)
f[x][i][j]=min(f[s[x]][i+1][j]+f[t[x]][i][j],f[s[x]][i][j]+f[t[x]][i][j+1]);
}
int main()
{
scanf("%d",&n);
for(int i=1;i<n;i++)
{
scanf("%d%d",&s[i],&t[i]);
if(s[i]<0)
s[i]=n-1-s[i];
if(t[i]<0)
t[i]=n-1-t[i];
}
for(int i=1;i<=n;i++)
scanf("%d%d%d",&a[n+i-1],&b[n+i-1],&c[n+i-1]);
dfs(1,0,0);
printf("%lld\n",f[1][0][0]);
return 0;
}
[HNOI/AHOI2018]排列
简单贪心题。
首先判无解,如果有环就一定无解,否则就有解,这一步可以用并查集来判。
然后考虑如何求出答案,我们将\(i\)连向\(a_i\),连出以\(0\)为根一颗树。
假设当前的最小值为\(x\),如果\(x\)点没有父亲,我们肯定直接选了,如果它有父亲,那么也会在父亲选了之后直接选。
于是我们就可以每一次取出权值最小的点(这一步可以用堆或者set实现),然后将这个点和父亲合并,统计新产生的答案。
考虑一个点的权值表示什么。
设有两个序列\(A\)和\(B\),他们的权值和分别为\(w_a,w_b\),他们的大小分别为\(siz_a,siz_b\)。
- 如果\(AB\)连接,那么新产生的权值为\(siz_a \times w_b\)。
- 如果\(BA\)连接,那么新产生的权值为\(siz_b \times w_a\)。
假设\(AB\)连接更优,那么\(siz_a \times w_b \ge siz_b \times w_a\)
也就是\(\frac{w_a}{siz_a} \le \frac{w_b}{siz_b}\)。
那么,我们每一次取出平均权值最小的点即可。
Code:
#include<bits/stdc++.h>
using namespace std;
const int maxn=710000;
int n,a[maxn];
long long w[maxn];
int fa[maxn],siz[maxn];
struct ljq
{
int x,siz;
long long w;
const bool operator < (const ljq &x)const{return w*x.siz>siz*x.w;}
};
char buf[1<<23],*p1=buf,*p2=buf;
#define getchar() (p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<21,stdin),p1==p2)?EOF:*p1++)
inline int rd() {
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
while(isdigit(ch)) x=x*10+(ch^48),ch=getchar();
return x*f;
}
priority_queue<ljq> S;
int find(int x){return x==fa[x]?x:fa[x]=find(fa[x]);}
void Union(int x,int y)
{
int fx=find(x),fy=find(y);
if(fx==fy)
{
puts("-1");
exit(0);
}
fa[fx]=fy;
}
signed main()
{
n=rd();
for(int i=0;i<=n;i++)
fa[i]=i;
for(int i=1;i<=n;i++)
a[i]=rd(),Union(i,a[i]);
for(int i=1;i<=n;i++)
w[i]=rd(),S.push({i,1,w[i]});
for(int i=0;i<=n;i++)
fa[i]=i,siz[i]=1;
long long ans=0;
while(!S.empty())
{
ljq p=S.top();S.pop();int u;
if(siz[u=find(p.x)]!=p.siz)
continue;
int t=find(a[u]);
fa[u]=t;
ans+=1ll*siz[t]*w[u];
siz[t]+=siz[u],w[t]+=w[u];
if(t)
S.push({t,siz[t],w[t]});
}
printf("%lld\n",ans);
return 0;
}
[HNOI/AHOI2018]游戏
首先我们发现询问没用,我们只要求出每一个点出发能到达的区间的左右端点即可。
一个很显然的结论是,如果一个点可以到达另外一个点,那么这个点也一定可以到达另外一个点能到达的点。
于是我们就可以用记忆化搜索来解决这个问题。
当一个点答案还没有求出来时,我们从一个点开始向左右扩展,然后不断加入新到达的点所到达区间即可。
复杂度\(O(n)\),很好证明。
Code:
#include<bits/stdc++.h>
using namespace std;
const int maxn=1500000;
int n,m,p,x,y;
int pos[maxn],t[maxn],b[maxn];
int l[maxn],r[maxn];
void dfs(int x)
{
if(l[x]&&r[x])
return ;
l[x]=x,r[x]=t[x];
while(true)
{
int ok=0;
if(pos[l[x]-1]>=l[x]&&pos[l[x]-1]<=r[x])
{
dfs(b[l[x]-1]);
r[x]=max(r[x],r[b[l[x]-1]]);
l[x]=min(l[x],l[b[l[x]-1]]);
ok++;
}
if(pos[r[x]]>=l[x]&&pos[r[x]]<=r[x])
{
dfs(b[r[x]+1]);
l[x]=min(l[x],l[b[r[x]+1]]);
r[x]=max(r[x],r[b[r[x]+1]]);
ok++;
}
if(!ok)
break;
}
}
int main()
{
scanf("%d%d%d",&n,&m,&p);
for(int i=1;i<=m;i++)
scanf("%d%d",&x,&y),pos[x]=y;
for(int i=1;i<=n;i++)
if(i==1||pos[i-1])
t[i]=b[i]=i;
else
b[i]=b[i-1],t[b[i]]=i;
for(int i=1;i<=n;i++)
if(b[i]==i)
dfs(i);
for(int i=1;i<=p;i++)
{
scanf("%d%d",&x,&y);
if(y>=l[b[x]]&&y<=r[b[x]])
puts("YES");
else
puts("NO");
}
return 0;
}
[HNOI/AHOI2018]毒瘤
码农虚树题。
我们先来考虑一下树的情况怎么做。
老套路,设\(f_{x,0}\)表示\(x\)不选时的方案数,\(f_{x,1}\)表示\(x\)选时的方案数。
设\(t\)为\(x\)的儿子,则有:
- \(f_{x,0}=\prod_t f_{t,0}+f_{t,1}\)
- \(f_{x,1}=\prod_t f_{t,0}\)
考虑图的情况,图的独立集的方案数是一个NPC问题,但我们发现这个图上的非树边最多只有11条,因此我们可以枚举每一条非树边的一个端点选还是不选,由此决定另外一个端点可不可以选,然后重新进行dp。
时间复杂度:\(O(n 2^{m-n+1})\)。
考虑优化,我们发现每一次dp转移发生了改变的只有少部分点,大部分点的转移不会发生改变。
因此我们可以把所有非树边连接的点作为关键点,然后对于这些关键点建立虚树。
我们每一次枚举后都只在虚树上dp,复杂度就会降低许多,但是这样转移的方程式就会改变。
- \(f_{x,0}=\prod_t k_1 f_{t,0}+k_2 f_{t,1}\)
- \(f_{x,1}=\prod_t k_3 f_{t,0}+k_4 f_{t,1}\)
其中\(k1,k2,k3,k4\)都是常数。
我们考虑如何来求出这些常数。
我们在每一个非虚树上的点的联通快进行dp即可,具体做法就是进行dfs,遇到非虚树上的点就进行转移,遇到虚树上的点就不进行转移,就可以了。
Code:
#include<bits/stdc++.h>
using namespace std;
const int mod=998244353;
const int maxn=300000;
int n,m,u,v;
int tot=0,pre[maxn],now[maxn],son[maxn];
int k0[maxn][2],k1[maxn][2];
int st[maxn],top=0;
int id[maxn],idn=0,f[maxn][20],dep[maxn],dp[maxn][2],DP[maxn][2];
int stk[maxn],tp=0,wh[maxn],vis[maxn];
vector<int> G[maxn];
set<int> D[maxn];
int h[maxn],k=0,in[maxn],ans=0;
int cmp(int x,int y){return id[x]<id[y];}
void put(int x,int y)
{
pre[++tot]=now[x];
now[x]=tot;
son[tot]=y;
}
int power(int x,int y)
{
int ans=1,last=x;
while(y)
{
if(y&1)
ans=1ll*ans*last%mod;
last=1ll*last*last%mod;
y>>=1;
}
return ans;
}
int LCA(int x,int y)
{
if(dep[x]<dep[y])
swap(x,y);
for(int i=19;i>=0;i--)
if(dep[f[x][i]]>=dep[y])
x=f[x][i];
if(x==y)
return x;
for(int i=19;i>=0;i--)
if(f[x][i]!=f[y][i])
x=f[x][i],y=f[y][i];
return f[x][0];
}
int Wson(int x,int y)
{
for(int i=19;i>=0;i--)
if(dep[f[y][i]]>dep[x])
y=f[y][i];
return y;
}
void calcDP(int x)
{
DP[x][0]=dp[x][0],DP[x][1]=dp[x][1];
for(auto t:G[x])
{
calcDP(t);
int P=Wson(x,t);
DP[x][0]=1ll*DP[x][0]*power(dp[P][0]+dp[P][1],mod-2)%mod;
DP[x][1]=1ll*DP[x][1]*power(dp[P][0],mod-2)%mod;
}
}
void get(int x,int y)
{
int u=y;k0[y][0]=k0[y][1]=k1[y][0]=1;
while(f[u][0]!=x)
{
int t=f[u][0],dp0=1ll*dp[t][0]*power(dp[u][0]+dp[u][1],mod-2)%mod,dp1=1ll*dp[t][1]*power(dp[u][0],mod-2)%mod;
int t0=k0[y][0],t1=k0[y][1];
k0[y][0]=(1ll*dp0*t0+1ll*dp1*k1[y][0])%mod;
k0[y][1]=(1ll*dp0*t1+1ll*dp1*k1[y][1])%mod;
k1[y][1]=1ll*dp0*t1%mod,k1[y][0]=1ll*dp0*t0%mod;
u=f[u][0];
}
}
void calck(int x,int fa)
{
if(fa)
get(fa,x);
for(auto t:G[x])
calck(t,x);
}
void Dp(int x)
{
dp[x][0]=DP[x][0],dp[x][1]=DP[x][1];
int ok=wh[x];
for(auto t:D[x])
if(wh[t]==1)
ok=-1;
if(ok==1)
dp[x][0]=0;
if(ok==-1)
dp[x][1]=0;
for(auto t:G[x])
{
Dp(t);
dp[x][1]=1ll*dp[x][1]*(1ll*k1[t][0]*dp[t][0]%mod+1ll*k1[t][1]*dp[t][1]%mod)%mod;
dp[x][0]=1ll*dp[x][0]*(1ll*k0[t][0]*dp[t][0]%mod+1ll*k0[t][1]*dp[t][1]%mod)%mod;
}
}
void dfs(int x,int fa,int Dp)
{
id[x]=++idn;f[x][0]=fa;dep[x]=Dp;
dp[x][0]=dp[x][1]=1;
for(int i=1;i<=19;i++)
f[x][i]=f[f[x][i-1]][i-1];
for(int p=now[x];p;p=pre[p])
if(son[p]!=fa)
{
if(!id[son[p]])
dfs(son[p],x,Dp+1),dp[x][0]=1ll*dp[x][0]*(dp[son[p]][0]+dp[son[p]][1])%mod,dp[x][1]=1ll*dp[x][1]*dp[son[p]][0]%mod;
else
{
if(vis[x]+vis[son[p]]==0)
vis[x]=1,stk[++tp]=x;
h[++k]=x,h[++k]=son[p];
}
}
}
void build()
{
sort(h+1,h+k+1,cmp);
st[top=1]=1;
for(int i=1;i<=k;i++)
if(h[i]!=1)
{
int Lca=LCA(h[i],st[top]);in[Lca]=1;
if(Lca!=st[top])
{
while(id[st[top-1]]>id[Lca])
G[st[top-1]].push_back(st[top]),top--;
if(st[top-1]!=Lca)
G[Lca].push_back(st[top]),st[top]=Lca;
else
G[Lca].push_back(st[top]),top--;
}
st[++top]=h[i];
}
for(int i=1;i<top;i++)
G[st[i]].push_back(st[i+1]);
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
scanf("%d%d",&u,&v),put(u,v),put(v,u);
dfs(1,0,1);
sort(h+1,h+k+1);k=unique(h+1,h+k+1)-h-1;
sort(stk+1,stk+tp+1);tp=unique(stk+1,stk+tp+1)-stk-1;
for(int i=1;i<=k;i++)
in[h[i]]=1;
for(int i=1;i<=k;i++)
for(int p=now[h[i]];p;p=pre[p])
if(in[son[p]])
D[h[i]].insert(son[p]);
build();calcDP(1);calck(1,0);
int S=(1<<tp)-1;
if(m>=n)
for(int i=0;i<=S;i++)
{
for(int j=1;j<=k;j++)
wh[h[j]]=0;
for(int j=1;j<=tp;j++)
wh[stk[j]]=((i&(1<<j-1))!=0)?1:-1;
int ok=0;
for(int j=1;j<=k;j++)
for(auto t:D[h[j]])
if(wh[h[j]]+wh[t]==2)
ok=1;
if(ok==1)
continue;
Dp(1);
ans=(0ll+ans+dp[1][0]+dp[1][1])%mod;
}
else
ans=(dp[1][0]+dp[1][1])%mod;
printf("%d\n",ans);
return 0;
}
[HNOI/AHOI2018]寻宝游戏
找规律神题。
看到二进制,首先按位考虑,我们发现,对于每一位,我们去给它\(\vee 1\)或者\(\wedge 0\)都对当前位上的值没有任何影响,而\(\vee 0\)相当于赋值为0,\(\wedge 1\)相当于赋值为1。
如果要求一位上的最终结果为\(1\),那么必须要有\(\wedge 1\)操作,并且最后一次\(\wedge 1\)操作一定要在最后一次\(\vee 0\)操作之后。
同理,如果要求一位上的最终结果为\(0\),那么要么没有\(\wedge 1\)操作和\(\vee 0\),或者最后一次\(\wedge 1\)操作在最后一次\(\vee 0\)操作之前。
我们将所有的操作表示为一个二进制串,\(\vee\)为\(0\),\(\wedge\)为\(1\),最后一次操作为最高位,并设这个串为\(S\),设所有数的当前位组成的字符串为\(X\),那么有:
- 如果当前位的最终结果为\(1\),那么一定有\(X \gt S\)。
- 如果当前位的最终结果为\(0\),那么一定有\(X \le S\)。
经过步步转换,问题变成了一个比大小问题,我们枚举每一位,求出操作二进制串的取值范围就可以求出答案了。
Code:
#include<bits/stdc++.h>
using namespace std;
const int maxn=5020;
const int mod=1000000007;
int n,m,q,b[maxn],rk[maxn];
int v[maxn];
char s[maxn][maxn],r[maxn][maxn];
bool cmp(int x,int y)
{
for(int i=n;i>=1;i--)
if(s[i][x]!=s[i][y])
return s[i][x]>s[i][y];
return 0;
}
int main()
{
scanf("%d%d%d",&n,&m,&q);
for(int i=1;i<=m;i++)
b[i]=i;
for(int i=1;i<=n;i++)
scanf("%s",s[i]+1);
for(int i=1;i<=m;i++)
for(int j=n;j>=1;j--)
v[i]=(2ll*v[i]+s[j][i]-'0')%mod;
v[m+1]=1;
for(int i=1;i<=n;i++)
v[m+1]=2ll*v[m+1]%mod;
sort(b+1,b+m+1,cmp);
for(int i=1;i<=m;i++)
rk[b[i]]=i;
rk[m+1]=0,b[m+1]=0;
rk[0]=m+1,b[0]=m+1;
for(int i=1;i<=q;i++)
scanf("%s",r[i]+1);
for(int i=1;i<=q;i++)
{
int mn=0,mx=m+1;
for(int j=m;j>=1;j--)
if(r[i][j]=='1')
mn=max(mn,rk[j]);
else
mx=min(mx,rk[j]);
mn=b[mn],mx=b[mx];
if(rk[mn]>=rk[mx])
puts("0");
else
printf("%d\n",(v[mn]-v[mx]+mod)%mod);
}
return 0;
}
[HNOI/AHOI2018]转盘
首先,停留操作全部停在起点肯定不会影响答案,然后段环成链,答案就可以表示为
\(\min_{i=n}^{2n-1} (i+\max_{j=i-n+1}^{i}T_j-j)\),其中\(i\)为终点。
设\(s_i=T_i-i\)
替换一下得到
把\(i+n-1\)替换为\(2n\)不会产生影响。
现在转化成了后缀最大值,但是复杂度还是没有降低。
接着,我们发现所有可能的后缀最大值构成一个单调栈,我们可以维护这个单调栈。
设栈中元素个数为\(sum\),第\(i\)个元素为\(p_i\),\(p_0=inf\)。
那么\(Ans=n+\min_{i=1}^{sum} s_{p_i}+p_{i-1}\)。
对于修改,用线段树维护单调栈即可。
Code:
#include<bits/stdc++.h>
using namespace std;
const int maxn=500000;
int n,m,p,ans[maxn],T[maxn],x,y,mx[maxn],last=0;
int query(int p,int l,int r,int v)
{
if(l==r)
return mx[p]>v?v+l:1e9;
int mid=(l+r)/2;
if(mx[p+p+1]>v)
return min(ans[p+p],query(p+p+1,mid+1,r,v));
return query(p+p,l,mid,v);
}
void updata(int p,int l,int r)
{
mx[p]=max(mx[p+p],mx[p+p+1]);
ans[p+p]=query(p+p,l,(l+r)/2,mx[p+p+1]);
}
void build(int p,int l,int r)
{
if(l==r)
return mx[p]=T[l]-l,void();
int mid=(l+r)/2;
build(p+p,l,mid);build(p+p+1,mid+1,r);
updata(p,l,r);
}
void change(int p,int l,int r,int x,int v)
{
if(l==r)
return mx[p]=v-l,void();
int mid=(l+r)/2;
if(mid>=x)
change(p+p,l,mid,x,v);
else
change(p+p+1,mid+1,r,x,v);
updata(p,l,r);
}
int main()
{
scanf("%d%d%d",&n,&m,&p);
for(int i=1;i<=n;i++)
scanf("%d",&T[i]);
build(1,1,n);
last=query(1,1,n,mx[1]-n)+n;
printf("%d\n",last);
while(m--)
{
scanf("%d%d",&x,&y);
if(p)
x^=last,y^=last;
change(1,1,n,x,y);
printf("%d\n",last=query(1,1,n,mx[1]-n)+n);
}
return 0;
}