Title

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\)

\[Ans=\min_{i=n}^{2n-1} (i+\max_{j=i-n+1}^{i}s_j) \]

替换一下得到

\[Ans=n-1+\min_{i=1}^n (i+\max_{j=i}^{i+n-1} s_j) \]

\(i+n-1\)替换为\(2n\)不会产生影响。

\[Ans=n-1+\min_{i=1}^n (i+\max_{j=i}^{2n} s_j) \]

现在转化成了后缀最大值,但是复杂度还是没有降低。

接着,我们发现所有可能的后缀最大值构成一个单调栈,我们可以维护这个单调栈。

设栈中元素个数为\(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;
}
posted @ 2022-01-14 09:56  五百年前  阅读(26)  评论(0编辑  收藏  举报