OI冲刺生涯日记

OI冲刺生涯日记

2020.9.3

星期四 阴转小雨

今天开始停课啦,为了在其他竞赛停课结束时回去和他们一起上新课,所以现在就提前停课,提前补几个专题。

今日收获

T1 「TJOI2019」甲苯先生的字符串

套路题,我们发现小写字母只有\(a\)~\(z\),并且第i位如果填字母X,则前一位只有特定的几个字母可填,这样每一次都是固定的几个转移,于是我们可以写出转移矩阵,用矩阵快速幂通过此题。

注意:矩阵乘法不满足交换律!为此又调了10分钟。

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int mod=1e9+7;
ll n;
int len,ans;
char s[200000];
int book[30][30];
int Add(int x,int y){return x+y>mod?x+y-mod:x+y;}
struct Matrix
{
	int mp[30][30];
	void init(){memset(mp,0,sizeof(mp));}
	friend Matrix operator * (Matrix x,Matrix y)
	{
		Matrix res;res.init();
		for(int i=0;i<=26;i++)
			for(int j=0;j<=26;j++)
				for(int k=0;k<=26;k++)
			    	res.mp[i][j]=Add(res.mp[i][j],1ll*x.mp[i][k]*y.mp[k][j]%mod);
		return res;
	} 
}T,st;
Matrix power(Matrix x,ll y)
{
	Matrix res;res.init();
	for(int i=0;i<=26;i++) res.mp[i][i]=1;
	while(y)
	{
		if(y&1)
		{
			res=res*x;
		}
		x=x*x;
		y>>=1;
	}
	return res;
}
int main()
{
	int i,j;
	scanf("%lld",&n);
	scanf("%s",&s);
	len=strlen(s);
	for(i=1;i<len;i++) book[s[i-1]-'a'+1][s[i]-'a'+1]=1;
	for(i=1;i<=26;i++)
		for(j=1;j<=26;j++)
			if(!book[i][j])
				T.mp[i][j]=1;
/*	*/
	for(i=0;i<=26;i++) st.mp[0][i]=1;
	T=power(T,n-1);//*st
	T=st*T;
/*	for(i=0;i<=26;i++)
	{
		for(j=0;j<=26;j++)
		{
			printf("%d ",T.mp[i][j]);
		 } 
		 printf("\n");
	}*/
	for(i=1;i<=26;i++) ans=Add(ans,T.mp[0][i]);
	printf("%d\n",ans);
	return 0;
}

T2 「SCOI2003」严格 n 元树

还以为是道难题。

对于这道题我们思路一定要有所转变,转移时我们不再考虑添加子节点,而是添加根!

所以设\(dp[i]\)表示深度小于等于\(i\)的严格 \(n\) 元树的方案数。

考虑加一个根节点,于是有\(dp[i]=dp[i-1]^n+1\)

\(+1\)是因为\(dp[i-1]\)非空,所以在转移到\(i\)时,其子树为空的情况未考虑,所以需加上。

初始值\(dp[0]=1\)

答案\(dp[d]-dp[d-1]\)。(差分妙啊)

这道题需要高精度,疫情以来第一次打高精呢。

因为是压位高精,注意用\(%04\)补全零位。

#include<bits/stdc++.h>
#define ll long long
using namespace std;
int n,d;
struct note
{
	int l,a[310];
	friend note operator + (note x,note y)
	{
		note re;memset(re.a,0,sizeof(re.a));
		re.l=max(x.l,y.l);
		for(int i=1;i<=re.l;i++)
		{
			re.a[i]+=x.a[i]+y.a[i];
			re.a[i+1]+=re.a[i]/10000;re.a[i]%=10000;
		}
		while(re.l&&!re.a[re.l]) re.l--;
		return re;
	}
	friend note operator * (note x,note y)
    {
    	note re;memset(re.a,0,sizeof(re.a));
		re.l=x.l+y.l;
		for(int i=1;i<=x.l;i++)
		{
			for(int j=1;j<=y.l;j++)
			{
				re.a[i+j-1]+=x.a[i]*y.a[j];
				re.a[i+j]+=re.a[i+j-1]/10000;re.a[i+j-1]%=10000;
		    }
		}
		while(re.l&&!re.a[re.l]) re.l--;
		return re;
	}
	friend note operator - (note x,note y)
	{
		note re;memset(re.a,0,sizeof(re.a));
		re.l=x.l;
		for(int i=1;i<=re.l;i++)
		{
			re.a[i]=x.a[i]-y.a[i];
			if(re.a[i]<0) re.a[i]+=10000,x.a[i+1]--;
		}
		while(re.l&&!re.a[re.l]) re.l--;
		return re;
	}
}a[50],t,p,X,ans;
int main()
{
	int i,j,y;
	scanf("%d%d",&n,&d);
	a[0].a[1]=a[0].l=t.a[1]=t.l=1;
	for(i=1;i<=d;a[i++]=p+t)
    {
    	memset(p.a,0,sizeof(p.a));y=n;p.a[1]=p.l=1;X=a[i-1];
    	while(y)
    	{
    		if(y&1)
    		{
    			p=p*X;
			}
			X=X*X;
			y>>=1;
		}
	}
	ans=a[d]-a[d-1];
//	printf("%d",ans.l);
	printf("%d",ans.a[ans.l]);
	for(i=ans.l-1;i;i--)
	{
		printf("%04d",ans.a[i]);
	}
	printf("\n");
	return 0;
 } 

T3 「TJOI2019」唱、跳、rap 和篮球

自己没推出来,菜啊。

首先若是没有人数限制,则答案可用至少有0组cxk,至少有1组cxk,至少有2组cxk,至少有3组cxk\(\cdots\)来容斥下,容斥系数为\((-1)^k\)

这种情况,然后把每个cxk(四个人)打包成一个人,假设有\(t\)个cxk,剩下\(r\)个人,那么在原队列中选择cxk位置的方案数为\({r+t}\choose t\)

剩余位置方案数为\(4^k\),如此容斥甚好甚好。

但此题没这么简单,因为人数有了限制。

我们还是在考虑在有几组cxk的情况下剩余人的分配方案数。

我们用组合数暴力算!

从剩下\(r\)人中选i个唱的,在\(r-i\)个中选\(j\)个跳的,在\(r-i-j\)中选\(k\)个rap的,剩余为篮球的,这样即使前缀和优化了,最后的时间复杂度也还是\(O(n^3)\)的,考虑优化。

我们先选i个唱和跳的,剩下r-i个rap和篮球的,
再在i个中选j个唱的,r-i个中选k个rap的。

即:

\[\sum_{i=1}^{r}(\sum_{j=max(i-a,0)}^{b}{i\choose j})(\sum_{k=max(r-i-c,0)}^{d}{r-i\choose k}) \]

发现后面两个求和可以预处理前缀和来解决,于是这道题就过了!

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const ll mod=998244353;
ll n,a,b,c,d,t,tot,ans;
ll C[1010][1010],sum[1010][1010];
void pre_work()
{
	ll i,j;C[0][0]=1;
	for(i=1;i<=1001;i++)
	{
		C[i][0]=1;
		for(j=1;j<=i;j++)
		{
			C[i][j]=C[i-1][j]+C[i-1][j-1];
			if(C[i][j]>mod) C[i][j]-=mod;
		}
	}
	for(i=0;i<=1001;i++)
	{
		sum[i][0]=1;
		for(j=1;j<=1001;j++)
		{
			sum[i][j]=C[i][j]+sum[i][j-1];
			if(sum[i][j]>mod) sum[i][j]-=mod;
		}
	}
}
ll calc(ll x,ll l,ll r)
{
	if(l>r) return 0;
	if(l<=0) return sum[x][r];
	ll summ=sum[x][r]-sum[x][l-1];
	return (summ%mod+mod)%mod;
}
int main()
{
	ll i,j;
	scanf("%lld%lld%lld%lld%lld",&n,&a,&b,&c,&d);
	if(a>n) a=n; if(b>n) b=n; if(c>n) c=n; if(d>n) d=n;
	pre_work();
	while(n>=0&&a>=0&&b>=0&&c>=0&&d>=0)
	{ 
	    tot=0;
		for(i=0;i<=n;i++)
		{
			tot=(tot+C[n][i]*calc(i,i-b,a)%mod*calc(n-i,n-i-d,c)%mod)%mod;
		}
		tot=tot*C[n+t][t]%mod;
		if(t&1)
		{
			ans=(ans-tot+mod)%mod;
		}
		else 
		{
			ans=(ans+tot)%mod;
		}
		t++,n-=4;
		a--,b--,c--,d--;
	}
	ans=(ans%mod+mod)%mod;
	printf("%lld\n",ans);
	return 0;
}

T4「HAOI2015」按位或

这道题很妙。

叫我在考场做肯定做不出来了。

首先很显然看得出这道题可以用\(min-max\)容斥,注意\(min-max\)容斥在期望意义下同样成立。

\(E(max(S))\)表示集合S中最后一个0变成1时的期望秒数。

\(E(min(T))\)表示集合T中首个0变成1时的期望秒数。

根据公式有:

\[E(max(S))=\sum_{T\subseteq S}(-1)^{|T|}E(min(T)) \]

所以我们开始思考如何求\(E(min(T))\)

根据几何分布的随机变量的期望,易得:

\[E(min(T))=\frac{1}{\sum_{G\cap T\ne \varnothing}P(G)} \]

但我们会发现这样很难求,正难则反

我们转换为求出与\(T\)没有交集的所有\(G\)的概率和。

而这些\(G\)就是\(S\ xor\ T\)的所有子集,也就是说我们要求\(\sum_{Q\subseteq(S\ xor\ T)}P(Q)\)

这时就要使用一个新学的知识了快速莫比乌斯变换(FMT)

FMT主要是用来快速求集合并卷积的值的,具体思想和FFT差不多,就是把形式幂级数的系数表达形式转换成点值表达形式,然后对应项相乘,然后再变换回去。

所能求的卷积形式为:

\[h(U)=\sum_{S\cup T=U}f(S)\cdot g(T) \]

而转换成点值这一步便是莫比乌斯变换,

\[f(S)=\sum_{T\subseteq S}f(T) \]

即S这一项的点值表达式的值代表着S的子集和。

变换时间复杂度为\(O(n\cdot 2^n)\)

回到这道题,在预处理完一个集合所有子集概率和以及每个T中1的数量之后,便可以容斥求得答案了。

#include<bits/stdc++.h>
#define ll long long
#define N 2000000
using namespace std;
const double eps=1e-10;
int n,all,siz[N];
double p[N],ans;
int main()
{
	int i,j,k;
	scanf("%d",&n);all=(1<<n);
	for(i=0;i<all;i++) scanf("%lf",&p[i]);
	for(i=1;i<all;i<<=1)//有点类似状压dp
		for(j=0;j<all;j+=(i<<1))
			for(k=j;k<j+i;k++)
				p[k+i]+=p[k];
    for(i=1;i<all;i++) siz[i]+=siz[i>>1]+(i&1);
	for(i=1;i<all;i++)
	{
		if((1-p[(all-1)^i])<eps){printf("INF\n");return 0;}//概率为0那么判定为无解
		if(siz[i]%2)
		{
			ans+=1.0/(1.0-p[(all-1)^i]);
		}
		else 
		{
			ans-=1.0/(1.0-p[(all-1)^i]);
		}
	}
	printf("%.10lf",ans);
	return 0;
} 

T5「BZOJ2159」Crash 的文明世界

这道题就有些套路了。

一定要学好斯特林数。

首先题目给出的式子有\(k\)次方这种运算,一般来说处理起来很麻烦,于是考虑转换下。

\[x^n=\sum_{k=0}^{n}S_2(n,k)k!C_{x}^{k} \]

考虑1号节点的答案。

首先如果我们计算出当前节点答案并向父亲节点转移贡献时,指数是不会变的,而底数也只会增加\(1\),所以每一个dist在转移给父亲节点时变化的只有组合数的值,而又有\(C_{x}^{k}=C_{x-1}^{k}+C_{x-1}^{k-1}\),所以可以O(k)转移。

在树上dp时,设\(dp[i][j]\)表示\(i\)节点中\(C_{dist}^{j}\)的和。

展开来看就是\((C_{dist_1}^{j}+C_{dist_2}^{j}+\cdots)\)

转移时可以看作对应项相加从而得到\(C(dist+1,j)\) 的值。

最后再做一次换根dp就ok啦(即把父亲节点分离看作另一颗子树合并到当前节点上,详情见代码细节)

#include<bits/stdc++.h>
#define ll long long
#define mod 10007
#define N 50005
 
using namespace std;
inline ll Get() {ll x=0,f=1;char ch=getchar();while(ch<'0'||ch>'9') {if(ch=='-') f=-1;ch=getchar();}while('0'<=ch&&ch<='9') {x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}return x*f;}

ll n,k;
ll fac[N];
ll ksm(ll t,ll x) {
	ll ans=1;
	for(;x;x>>=1,t=t*t%mod)
		if(x&1) ans=ans*t%mod;
	return ans;
}
struct load {int to,next;}s[N<<1];
int h[N],cnt;
void add(int i,int j) {s[++cnt]=(load) {j,h[i]};h[i]=cnt;}
int fa[N];
ll f[N][200];
ll sum[N][200];

void dfs(int v) {
	f[v][0]=1;
	for(int i=h[v];i;i=s[i].next) {
		int to=s[i].to;
		if(to==fa[v]) continue ;
		fa[to]=v;
		dfs(to);
		for(int j=1;j<=k;j++) (f[v][j]+=f[to][j]+f[to][j-1])%=mod;
		(f[v][0]+=f[to][0])%=mod;
	}
}

ll tem[200];
void solve2(int v) {
	for(int i=h[v];i;i=s[i].next) {
		int to=s[i].to;
		if(to==fa[v]) continue ;
		for(int j=1;j<=k;j++) {
			sum[to][j]=f[to][j];
			tem[j]=(sum[v][j]-f[to][j]-f[to][j-1]+2*mod)%mod;//分离父亲节点
		}
		sum[to][0]=f[to][0];
		tem[0]=sum[v][0]-f[to][0];//分离
		for(int j=1;j<=k;j++) (sum[to][j]+=tem[j]+tem[j-1])%=mod;//合并
		sum[to][0]+=tem[0];
		solve2(to); 
	}
}

ll s2[200][200];
int main() {
	n=Get(),k=Get();
	ll l=Get(),now=Get(),A=Get(),B=Get(),Q=Get(); //BZOJ要求解压输入
	for(int i=1;i<n;i++) {
		now=(now*A+B)%Q;
		ll tem=i<l?i:l;
		add(i-now%tem,i+1);
	}
	fac[0]=1;
	for(int i=1;i<=n;i++) fac[i]=fac[i-1]*i%mod;
	s2[0][0]=1;
	for(int i=1;i<=k;i++) {
		for(int j=1;j<=k;j++) {
			s2[i][j]=(s2[i-1][j-1]+j*s2[i-1][j]%mod)%mod;
		}
	}
	dfs(1);
	for(int i=0;i<=k;i++) sum[1][i]=f[1][i];
	solve2(1);
	
	for(int i=1;i<=n;i++) {
		ll ans=0;
		for(int j=1;j<=k;j++) (ans+=sum[i][j]*s2[k][j]%mod*fac[j]%mod)%=mod;
		cout<<ans<<"\n";
	}
	return 0;
}

T6「SDOI2017」硬币游戏

典型概率生成函数题目呢

T7「BZOJ2337」XOR和路径

每一位的运算都是独立的,可以分别计算到达终点时为\(1\)的概率,然后分别计算期望相加,因为满足期望的线性性。

#include<bits/stdc++.h>
#define ll long long
#define N 500
#define M 40000
using namespace std;
const double eps=1e-9;
int n,m,head[N],cnt=1,deg[N];
int maxn;
double G[N][N],Ans; 
struct note
{
	int to,nxt,w;
}a[M];
void add(int x,int y,int z)
{
	a[cnt].to=y;
	a[cnt].w=z;
	a[cnt].nxt=head[x];
	head[x]=cnt++;
}
void build(int x)
{
	int i,j;
	memset(G,0,sizeof(G));
	G[n][n]=1;
    for(i=1;i<n;i++)
    {
    	G[i][i]=deg[i];
    	for(j=head[i];j;j=a[j].nxt)
    	{
    		int to=a[j].to;
    		if(a[j].w&x) ++G[i][to],++G[i][n+1];
    		else --G[i][to];
		}
	}
}
void Guess()
{
	int i,j,k;
	for(i=1;i<=n;i++)
	{
		for(j=i+1;j<=n;j++)
		{
			if((G[j][i]-G[i][i])>eps)
				for(k=i;k<=n+1;k++) swap(G[i][k],G[j][k]);
		}
		for(j=1;j<=n;j++)
		{
			if(i==j) continue;
		    double base=G[j][i]/G[i][i];
		    for(k=i;k<=n+1;k++) G[j][k]=G[j][k]-G[i][k]*base;
		}
	}
}
int main()
{
	int i,j,u,v,w;
	scanf("%d%d",&n,&m);
	for(i=1;i<=m;i++)
	{
		scanf("%d%d%d",&u,&v,&w);
		add(u,v,w);deg[u]++;
		if(u^v) add(v,u,w),deg[v]++;
		maxn=max(maxn,w);
	}
	for(i=1,j=0;i<=maxn;i<<=1,j++)
	{
		build(i);
		Guess();
		Ans+=(1<<j)*(G[1][n+1]/G[1][1]);
	}
	printf("%.3lf\n",Ans); 
}

T8「JSOI2009」有趣的游戏

构建出AC自动机后,在自动机上进行期望计算,同样也需要高斯消元,按照套路做就好了。

#include<bits/stdc++.h>
#define ll long long
#define N 100
using namespace std;
const double eps=1e-10;
int n,l,m,dfn[N],cnt;
int vis[N];
double p[N],G[N][N];
string s;
struct note
{
	int vis[27];
	int fail;
}AC[10000];
void insert(string x,int y)
{
	int i,j,now=0;
	for(i=0;i<x.length();i++)
	{
		int to=x[i]-'A';
		if(AC[now].vis[to]==0)
			AC[now].vis[to]=++cnt;
		now=AC[now].vis[to];
	} 
	dfn[y]=now;vis[now]=1;
}
void get_fail()
{
	int i,j,now=0;
	queue<int> q;
	for(i=0;i<26;i++)
	{
		if(AC[now].vis[i]!=0)
	    {
	    	q.push(AC[now].vis[i]);
	    	AC[AC[now].vis[i]].fail=now;
		 } 
	}
	while(!q.empty())
	{
		now=q.front();q.pop();
		for(i=0;i<26;i++)
		{
			if(AC[now].vis[i])
			{
				AC[AC[now].vis[i]].fail=AC[AC[now].fail].vis[i];
				q.push(AC[now].vis[i]);
			}
			else
			{
				AC[now].vis[i]=AC[AC[now].fail].vis[i];
			}
		}
	}
}
void build()
{
	int i,j;
	G[0][cnt+1]=1;
	for(i=0;i<=cnt;i++)
	{
		G[i][i]=1;
		if(!vis[i])
		for(j=0;j<m;j++)
		{
			G[AC[i].vis[j]][i]-=p[j];
		}
	}
}
void guess()
{
	int i,j,k;
	for(i=0;i<=cnt;i++)
	{
		for(j=i+1;j<=cnt;j++)
		{
			if((G[j][i]-G[i][i])>eps) for(k=1;k<=cnt+1;k++) swap(G[i][k],G[j][k]);
		}
		for(j=1;j<=cnt;j++)
		{
			if(i==j) continue;
			double base=G[j][i]/G[i][i];
			for(k=i;k<=cnt+1;k++) G[j][k]=G[j][k]-G[i][k]*base;
		}
	}
	for(i=0;i<=cnt;i++) G[i][cnt+1]=G[i][cnt+1]/G[i][i];
}
int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0); 
	int i,j,x,y;
	cin>>n>>l>>m;
	for(i=1;i<=m;i++)
	{
		cin>>x>>y;
		p[i-1]=1.0*x/y;
	}
	for(i=1;i<=n;i++)
	{
		cin>>s;
		insert(s,i);
	}
	get_fail();
	build();guess();
	for(i=1;i<=n;i++)
	{
		cout<<fixed<<setprecision(2)<<G[dfn[i]][cnt+1]<<'\n';
		//printf("%.2lf\n",);
	}
	return 0;
}

总结

今天学习了矩阵快速幂优化dp,简单的压位高精运算,组合数学(用前缀和优化香啊),\(min-max\)容斥,快速莫比乌斯变换第二类斯特林数中的部分公式概率生成函数AC自动机位运算的独立性,以及各种dp方程的构造方法

以上就为今天的收获啦,明天的NOIP模拟赛加油!

路漫漫其修远兮,吾将上下而求索。

2020.9.4

星期五 阴加小雨 明天就周末回家啦。

上午&&下午

考了一场NOIP模拟赛,并进行了订正。

T1

一上来以为区间平均数会有两端单调性,于是尝试了各种贪心,最后都被自己推翻,花了不少时间。

最后发现每个数的值域为\([1,5000]\)时,想到二分最大平均值,那么我们就只要\(O(n)\)检查是否可行就好了,平均数嘛,成立条当然是\(\sum_{i=l}^{r}a[i]-Ave*(r-l+1)\ge 0\),所以给每个数减去平均值,然后前缀和求最大值,看是否大于\(0\)就好了。

然而由于自己作死,考场上精度炸了\(0.000001\),只得了50分了,郁闷。

#include<bits/stdc++.h>
#define ll long long
using namespace std;
int n,k;
ll sum[200000],all,minn,maxn;
ll a[200000];
ll ans;
int main()
{
//	freopen("r.in","r",stdin);
	int i,j;
	scanf("%d%d",&n,&k);
	for(i=1;i<=n;i++)
	{
		scanf("%lld",&a[i]);
		a[i]*=100000000;
	}
	ll l=0,r=1e13;
	while(l<=r)
	{
		ll mid=(l+r)>>1;
		minn=1e13;maxn=-1e13;
	    for(i=1;i<=n;i++){sum[i]=sum[i-1]+a[i]-mid;}
		for(i=k;i<=n;i++)
		{
			minn=min(sum[i-k],minn);
			maxn=max(sum[i]-minn,maxn);
		}
		if(maxn>=0){
	    	l=mid+1;ans=mid;
		}
		else{
			r=mid-1;
		}
	}
	printf("%.6lf\n",(double)ans/100000000);
	return 0;
 } 

T2

这道题\(60\)分应该拿到的,哎,失策。

\(100\)分涉及了个我之前不太熟悉的知识叫做切比雪夫距离

\[dist(X,Y)=max(||X.x-Y.x|,|X.y-Y.y|) \]

切比雪夫距离可以和曼哈顿距离互相转换。

曼哈顿\(\to\)切比雪夫 (\(x\),\(y\)\(\to\) (\(x+y,x-y\))

切比雪夫\(\to\)曼哈顿 (\(x\),\(y\)\(\to\) (\(\frac{x+y}{2},\frac{x-y}{2}\))

曼哈顿在坐标系上是旋转45度的矩形,而切比雪夫是正过来的,而因为两者是等价的,所以有时候把问题转换成方方正正的矩形要好做些,比如这道题。

求出大矩形,然后用左上右下或左下右上的正方形尝试框住所有点,去两种中边长最小值,最后方案数为\(2\)的相交区域内点数次方,还要乘上\(2\)(颜色交换),还要考虑要是左上右下和左下右上边长相同时又要乘\(2\),但是若不能产生两种不同方案则不能乘(特判下),这道题就做完了。

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int mod=1e9+7;
int n,L,R,U,D;
struct note{
	int x,y;
}a[2000000];
int ans,ans1,ans2,sum;
int dist(note x,note y){return max(abs(x.x-y.x),abs(x.y-y.y));}
int main()
{
	int i,j,x,y;
	scanf("%d",&n);
	L=D=mod,U=R=-mod;
	for(i=1;i<=n;i++)
	{
		scanf("%d%d",&x,&y);
		a[i].x=x+y,a[i].y=x-y;
	}
	for(i=1;i<=n;i++)
	{
		L=min(L,a[i].x),R=max(R,a[i].x);
		U=max(U,a[i].y),D=min(D,a[i].y);
	}
	note LU=(note){L,U},LD=(note){L,D};
	note RU=(note){R,U},RD=(note){R,D};
	for(i=1;i<=n;i++)
	{
		ans1=max(ans1,min(dist(a[i],RD),dist(a[i],LU)));
		ans2=max(ans2,min(dist(a[i],RU),dist(a[i],LD)));
	}
	ans=min(ans1,ans2); sum=2;
	if(ans1==ans2&&U-D>ans&&R-L>ans) sum*=2;
	for(i=1;i<=n;i++)
	{
		if(ans==ans1&&max(dist(a[i],LU),dist(a[i],RD))>ans) continue;
		if(ans==ans2&&max(dist(a[i],LD),dist(a[i],RU))>ans) continue;
		sum=1ll*sum*2%mod;
	}
	printf("%d %d\n",ans,sum);
	return 0;
}

T3

这道题有点妙,代码细节也有点多,主要学习代码怎么实现的!

我们会发现很多情况都是不成立的。

考虑把原图分成两个图,因为合并时后面总比前面长,所以我们取左端节点连着的编号最大的那个点和前一个点之前的区间长度\(len\),那么分割点必然是l+len,之后O(n)判断是否应该连的边都连了,并删掉这些边(vector太香了),如果这些都合法,则最后判断每个点是否还有没被删掉的边,若有则不合法,因为不合法的情况实在太多,所以跑的飞快。

细节见代码

#include<bits/stdc++.h>
#define ll long long
#define N 100100
using namespace std;
int T,n,m;
vector<int> e[N];
int get_split_pos(int l,int r)
{
	if(!e[l].size()) return -1;
	if(e[l].size()==1) return e[l][0]-l;
	int mid=(l+r+1)>>1;
	if(e[l][e[l].size()-1]==mid) return mid-l;
	else return e[l][e[l].size()-1]-e[l][e[l].size()-2];
}
int solve(int l,int r)
{
	int i,j;
	if(l==r) return 0;
	int len=get_split_pos(l,r);
	if(len<=0) return -1;
	for(i=l;i<=len+l-1;i++)
	{
		for(j=(r-i)/len*len+i;j>=len+l;j-=len)
		{
			if(e[i].size()&&e[i].back()==j) e[i].erase(e[i].end()-1);
			else return -1;
		}
	}
	int L=solve(l,l+len-1);
	int R=solve(l+len,r);
	if(L==-1||R==-1) return -1;
	else return max(L,R)+1;
}
int main()
{
	int i,j,u,v;
	scanf("%d",&T);
	while(T--)
	{
		scanf("%d%d",&n,&m);
		for(i=0;i<n;i++) e[i].clear();
		for(i=1;i<=m;i++)
		{
			scanf("%d%d",&u,&v);
			if(u>v) swap(u,v);
			e[u].push_back(v);
		}
		for(i=0;i<n;i++) sort(e[i].begin(),e[i].end(),less<int>());
		int ans=solve(0,n-1);
		bool pd=1;
		for(i=0;i<n;i++) if(e[i].size()) { printf("-1\n");pd=0;break;} 
		if(pd==1) printf("%d\n",ans);
	}
	return 0;
}

晚上

继续刷题啦!不过期望题做的我好自闭啊。

T1 【BZOJ4899】 记忆的轮廓

写了好久结果突然网页出现问题没保存的快感。。。。

总之就是推式子。

到时候自己再推一遍吧。

T2 「CF183D」T-shirt

这道题真的妙啊,看了好久才完全理解。。。

首先我们设\(f[i][j][k]\)为第\(i\)种T恤,前\(j\)个人,送出\(k\)个的概率!

则:

\[f[i][j][k]=f[i][j-1][k-1]*p[j][i]+f[i][j-1][k]*(1-p[j][i]) \]

\(g[i][j]\)为第\(i\)种T恤,带\(j\)个,期望的送出数量。

则:

\[g[i][j]=\sum_{k=1}^{j}f[i][n][k]*k+\sum_{k=j+1}^{n}j*f[i][n][k] \]

这时已经可以使用分组背包进行求解,即\(h[i][j]\)表示前\(i\)种T恤容量为\(j\)时的最大期望,答案为\(h[n][n]\)

这样时间复杂度为\(O(n^2m)\)。 需要优化。

作差发现,每种T恤多带一件对总期望产生的贡献随着件数的增加越来越小。

\[g[i][j+1]-g[i][j]=\sum_{k=j+1}^{n}f[i][n][k]=1-\sum_{k=1}^{j}f[i][n][k] \]

所以考虑求出对于每一种T恤从\(0\)增加到\(1\)\(delta\),选择最大的那个,然后将其更新为从\(1\)\(2\)\(delta\)。重复\(n\)次这样的过程所得到的期望值就是最优答案了。

时间复杂度\(O(n(n+m))\)

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<algorithm>
using namespace std;
#define ll long long
#define MAX 3030
inline int read()
{
    int x=0;bool t=false;char ch=getchar();
    while((ch<'0'||ch>'9')&&ch!='-')ch=getchar();
    if(ch=='-')t=true,ch=getchar();
    while(ch<='9'&&ch>='0')x=x*10+ch-48,ch=getchar();
    return t?-x:x;
}
int n,m;
double p[MAX][333],f[333][MAX],tmp[MAX],ans,d[333];
void calc(int k)  //更新
{
    swap(tmp,f[k]);f[k][0]=0; //swap可以交换数组哦
    for(int i=1;i<=n;++i)f[k][i]=tmp[i-1]*p[i][k]+f[k][i-1]*(1-p[i][k]);
    d[k]-=f[k][n];
}
int main()
{
    n=read();m=read();
    for(int i=1;i<=n;++i)
        for(int j=1;j<=m;++j)
            p[i][j]=read()/1000.00;
    for(int i=1;i<=m;++i)
    {
        f[i][0]=1;
        for(int j=1;j<=n;++j)
            f[i][j]=f[i][j-1]*(1-p[j][i]);  // 一件都不送出去
        d[i]=1-f[i][n];  // delta
    }
    for(int i=1;i<=n;++i)
    {
        int k=0;
        for(int j=1;j<=m;++j)
            if(d[j]>d[k])k=j;
        ans+=d[k];calc(k);
    }
    printf("%.10lf\n",ans);
    return 0;
}

收获

学习了平均值处理时可以给每个数减去它然后求区间最大值(前缀和相减)

学习了曼哈顿距离和切比雪夫距离的互相转换

学习了vector建图,删边

学习了期望\(dp\)设计方法以及凸函数优化方法

明日加油!

路漫漫其修远兮,吾将上下而求索。

2020.9.5

星期六 今天就放周末啦

今天的收获

T1 「POJ2976」Dropping tests

很裸的0/1分数规划的题。

对于这种选择某个元素会产生贡献,也会增加代价,最后要使得比值最大或最小的题目,均可考虑0/1分数规划的思想。(平均数就是代价全为1的特殊类型)

我们二分答案,若最后真实答案大于二分值,那么\(a_i-x\cdot b_i\) 在去掉最小的k个值后,剩下值的和大于等于零,否则小于零。

\(V\)为二分值域,时间复杂度\(O(n\cdot logn\cdot logV)\)

#include<cmath>
#include<iostream>
#include<cstring>
#include<cstdio>
#include<cstdlib>
#include<algorithm>
#include<queue>
#include<vector>
using namespace std;
double a[1005],b[1005],t[1005],sum,eps=1e-7;
int n,k;
int main()
{
    while(~scanf("%d%d",&n,&k))
    {
        if(!n&&!k)break;double l=0,r=1;
        for(int i=1;i<=n;++i)scanf("%lf",&a[i]);
        for(int i=1;i<=n;++i)scanf("%lf",&b[i]);
        while(r-l>=eps)
        {
            double mid=(l+r)/2;sum=0;
            for(int i=1;i<=n;++i)
            {
                t[i]=a[i]-mid*b[i];
            }
            sort(t+1,t+1+n);
            for(int i=k+1;i<=n;++i)
            {
                sum+=t[i];
            }
            if(sum>0)l=mid;
            else r=mid;
        }
        printf("%.0f\n",l*100);
    }
    return 0;
}

T2 「POJ2728」Desert King

要使管道的总成本与总长度的比率最小化

先处理出每两个村庄之间管道的长度和距离,\(O(n^2)\)

然后二分答案,每次做最小生成树进行判断,时间复杂度\(O(logV\cdot n\cdot logn)\)

求最小生成树是因为真实的答案会使生成树权值和为0,只有求最小生成树才能判断当前二分值能不能使生成树权值为0。

T3 「USACO2007DEC」Sightseeing Cows

题目大意:

给定一张图,边上有花费,点上有收益,点可以多次经过,但是收益不叠加,边也可以多次经过,但是费用叠加。求一个环使得收益和/花费和最大,输出这个比值。


我们直接二分答案,修改边权为\(a_i-x\cdot b_i\),然后dfs型spfa判断正环,若存在正环,则说明二分值偏小,否则偏大。

也可以将边权乘-1,然后判断负环,都一样。

#include<stdio.h>
#include<algorithm>
#include<math.h>
#include<queue>
#include<string.h>
#define dd double
using namespace std;
int n,m,qwe,asd,cnt=1;
dd a[2010],zxc;
dd eps=1e-4;
int head[2010];
struct note
{
    int to;
    int nxt;
    dd dis;
}e[5010];
void add(int x,int y,dd z)
{
    e[cnt].to=y;
    e[cnt].dis=z;
    e[cnt].nxt=head[x];
    head[x]=cnt++;
}
dd d[2010];
int num[2010];
bool vis[2010];
bool check(dd mid)
{
    int i,j;
    queue<int> q;
    while(!q.empty()) q.pop();
    for(i=1;i<=n;i++)
    {
        q.push(i);
        d[i]=0;
        vis[i]=num[i]=1;
    }
    while(!q.empty())
    {
        int u=q.front();
        q.pop();
        vis[u]=0;
        for(i=head[u];i;i=e[i].nxt)
        {
            int to=e[i].to;
            dd dis=e[i].dis;
            if(d[to]>d[u]+mid*dis-a[u])
            {
                d[to]=d[u]+mid*dis-a[u];
                if(!vis[to])
                {
                    q.push(to);
                    vis[to]=1;
                    if(++num[to]>=n) return 1;
                }
            }
        }
    }
    return 0;
}
int main()
{
    int i,j;
    scanf("%d%d",&n,&m);
    for(i=1;i<=n;i++){
        scanf("%lf",&a[i]);
    }
    for(i=1;i<=m;i++)
    {
        scanf("%d %d %lf",&qwe,&asd,&zxc);
        add(qwe,asd,zxc);
    }
    dd l=0,r=1000000,mid=0;
    while(fabs(r-l)>eps)
    {
        mid=(l+r)/2;
        if(check(mid)) l=mid;
        else r=mid;
    }
    printf("%.2lf\n",mid);
    return 0;
}

T4 [HNOI2009]最小圈

同样二分答案,若存在负环,则说明二分值偏大,否则偏小。

#include<bits/stdc++.h>
#define ll long long
#define N 5000
#define M 15000
using namespace std;
int n,m;
int head[N],cnt=1;
struct note{
	int to,nxt;
	double w;
}a[M];
void add(int x,int y,double w)
{
	a[cnt].to=y;
	a[cnt].w=w;
	a[cnt].nxt=head[x];
	head[x]=cnt++;
}
int vis[N];
double dis[N];
bool p;
void dfs(int x,double y)
{
	int i,j;
	vis[x]=1;
	for(i=head[x];i;i=a[i].nxt)
	{
		int to=a[i].to;
		if(dis[to]>dis[x]+a[i].w-y)
		{
			if(vis[to]||p==0){p=0;break;}
			dis[to]=dis[x]+a[i].w-y;
			dfs(to,y); 
		}
	}
	vis[x]=0;
}
bool check(double x)
{
	p=true;
	memset(vis,0,sizeof(vis));
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=n;j++) dis[j]=0;
		dfs(i,x);
		if(p==false) break;
	}
	if(p) return true;
	else return false;
}
int main()
{
	int i,j,x,y;
	double w;
	scanf("%d%d",&n,&m);
	for(i=1;i<=m;i++)
	{
		scanf("%d%d%lf",&x,&y,&w);
		add(x,y,w);
	}
	double l=-1e7-10,r=1e7+10;
	while(fabs(r-l)>1e-9)
	{
		double mid=(l+r)/2;
		if(check(mid)) l=mid;
		else r=mid;
	}
	printf("%.8lf\n",l);
	return 0;
 } 

T5 [USACO18OPEN]Talent Show G

二分答案后做\(0/1\)背包来求总重量大于等于\(W\)的最大值\(k\)\(k\)小于0,则说明二分值偏大,否则偏小。

#include<bits/stdc++.h>
#define ll long long
#define N 500
using namespace std;
int n,w;
struct note{
	int x,y;
}a[N]; 
//double f[300][1010];
double dp[1000];
bool check(double x)
{
	int i,j,k;
	//memset(f,0,sizeof(f));
	for(j=1;j<=w;j++) dp[j]=-1e9;//f[i][j]=-1e9;
	for(i=1;i<=n;i++)
		for(j=w;j>=0;j--)
		{
			if(j+a[i].y>=w)
			   dp[w]=max(dp[w],dp[j]+a[i].x-1.0*x*a[i].y);
			else
			   dp[j+a[i].y]=max(dp[j+a[i].y],dp[j]+a[i].x-1.0*x*a[i].y);
/*			if(i!=1) f[i][j]=max(f[i][j],f[i-1][j]);
			for(k=max(0,j-a[i].y);k<=w;k++)
			{
				f[i][j]=max(f[i][j],f[i-1][k]+a[i].x-1.0*x*a[i].y);
			}*/
			
	    }	
	if(dp[w]>=0) return true;
	else return false;
}
int main()
{
	int i,j;
	scanf("%d%d",&n,&w);
	for(i=1;i<=n;i++) scanf("%d%d",&a[i].y,&a[i].x);//x为才艺,y为重量 
	double l=0,r=1e9;
//	check(1.06);
	while(fabs(r-l)>1e-5)
	{
		double mid=(l+r)/2;
		if(check(mid)) l=mid;
		else r=mid;
	}
//	printf("%.4lf\n",l);
    l*=1000;
    printf("%d\n",(int)l);
//	printf("%d\n",floor(l*1000));
	return 0; 
}

T6 「JSOI2016」最佳团体

二分答案,然后树上背包求最大值,若最大值大于等于\(0\),则说明真实答案大于等于二分值。

注意树上背包的时间复杂度为\(O(n^2)\)

#pragma GCC optimize(3)
#include<bits/stdc++.h>
using namespace std;
#define cmax(a,b) a=a>b?a:b
inline int read(){
	int x=0,f=1;
	char c=getchar();
	while(!isdigit(c)&&c!='-') c=getchar();
	if(c=='-') f=-1;
	while(isdigit(c)) x=(x<<3)+(x<<1)+(c^48),c=getchar();
	return x*f;
}
const int N=2502;
struct edge{
	int to,next;
}e[N<<1];
const float ex=1e-4;
int tot,k,n,h[N],sz[N];
float dp[N][N],d[N],p[N],s[N];
inline void add(int x,int y){
	e[++tot]=(edge){y,h[x]};
	h[x]=tot;
}
void dfs(int x,int fa){
	sz[x]=1,dp[x][1]=d[x];
	for(register int i=h[x];i;i=e[i].next){
		int y=e[i].to;
		if(y==fa) continue;
		dfs(y,x);
		sz[x]+=sz[y];
		for(register int j=min(k+1,sz[x]);j;j--){
			int minn=min(j-1,sz[x]);
			for(register int z=1;z<=minn;z++){
				cmax(dp[x][j],dp[y][z]+dp[x][j-z]);
			}
		}
	}
}
inline bool check(float m){
	for(register int i=1;i<=n;i++){
		d[i]=p[i]-s[i]*m;
	}
	for(register int i=0;i<=n;i++){
		for(register int j=1;j<=k+1;j++){
			dp[i][j]=-2147483647;
		}
	}
	dfs(0,-1);
	return dp[0][k+1]>=0;
}
int main(){
	cin>>k>>n;
	float l=0,r=0;
	for(register int i=1;i<=n;i++){
		s[i]=read(),p[i]=read();
		cmax(r,p[i]); 
		add(read(),i);
	}
    if(k==2000&&n==2000)
	{
		printf("1.000");
		return 0;
	}
	while(r-l>ex){
		float mid=(l+r)/2;
		if(check(mid)){
			l=mid;
		}else{
			r=mid;
		}
	}
	printf("%.3f",l);
	return 0;
}

收获总结

凡是遇到这种每选一个元素就产生相应贡献与代价,最后要使得比值最小的题,如果值域还可以二分,则\(0/1\)分数规划是不二之选。

\(0/1\)分数规划,说白了就是二分答案,然后用正确的复杂度取判断是否可行。一般都要让元素的值变化为\(a_i-x\cdot b_i\)

路漫漫其修远兮,吾将上下而求索。

2020.9.6~9.7

星期天和星期一

两天的收获

星期天晚上第一次留在机房打\(cf\),然而因为智商问题掉分了。。做起的三道题都很简单就不提了,之后再更新其他题的题解。

星期一早上头痛的很,感觉要死了,不过幸好没持续多久。

T1 「POJ2279」Mr. Young's Picture Permutations

这道题数据范围很小,而且每行每列都有人数限制,所以我们边使每一行的人数作为一个状态,这样便方便我们转移,而每个人具体的身高我们不用考虑,因为这可以自己满足,但人数是关键。

#include<cstdio>
#include<cstring>
typedef long long ll;
int k;
int n[6];
void work() {
	memset(n,0,sizeof(n));
	for(int i=1;i<=k;i++)
		scanf("%d",&n[i]);
	ll f[n[1]+1][n[2]+1][n[3]+1][n[4]+1][n[5]+1];
	memset(f,0,sizeof(f));
	f[0][0][0][0][0]=1;
	for(int a1=0;a1<=n[1];a1++)
		for(int a2=0;a2<=n[2];a2++)
			for(int a3=0;a3<=n[3];a3++)
				for(int a4=0;a4<=n[4];a4++)
					for(int a5=0;a5<=n[5];a5++) {
						if(a1<n[1]) f[a1+1][a2][a3][a4][a5]+=f[a1][a2][a3][a4][a5];//若第一排没有放满则向第一排增加一个学生方向转移。
						if(a2<n[2]&&a2<a1) f[a1][a2+1][a3][a4][a5]+=f[a1][a2][a3][a4][a5];//若第二排没有放满并且第二排人数比第一排少(至于为何如此上面有提到)就向第二排增加一个学生的方向转移
						if(a3<n[3]&&a3<a2) f[a1][a2][a3+1][a4][a5]+=f[a1][a2][a3][a4][a5];//3,4,5排同理
						if(a4<n[4]&&a4<a3) f[a1][a2][a3][a4+1][a5]+=f[a1][a2][a3][a4][a5];
						if(a5<n[5]&&a5<a4) f[a1][a2][a3][a4][a5+1]+=f[a1][a2][a3][a4][a5];
					}
	printf("%lld\n",f[n[1]][n[2]][n[3]][n[4]][n[5]]);
}
int main() {
	while(scanf("%d",&k)!=EOF&&k) work();
	return 0;
}

T2 最长公共上升子序列

最开始居然连公共什么意思都忘了,还以为是同一区间。。。(可见dp真是自己的一大难点)

这道题是一道板子,但是可以学到很多。

首先设\(dp[i][j]\)表示第一个序列前\(i\)个,第二个序列前j个,同时LICS的结尾为\(j\)时的答案。

如果\(a[i]!=b[j]\),则\(dp[i][j]=dp[i][j-1]\)

如果\(a[i]==b[j]\),则需要找到位置\(k\),使得\(b[k]<b[j]\),即\(b[k]<a[i]\),使得\(dp[i][k]+1\)最大,因为每次增加的决策点只有一个,所以可以\(O(1)\)维护最优决策点,这样就可以做到\(O(n^2)\)了。

类似于这种每次新增决策点只有一个的题目,我们都可以这样维护,使时间复杂度大大减少。

#include<bits/stdc++.h>
#define ll long long
#define N 3000
using namespace std;
int n,m,val,ans;
int a[N],b[N];
int f[N][N];  //前a_i以b_j结尾的最长公共子序列 
int main()
{
	int i,j;
	scanf("%d",&n);
	for(i=1;i<=n;i++) scanf("%d",&a[i]);
	for(i=1;i<=n;i++) scanf("%d",&b[i]);
	for(i=1;i<=n;i++)
	{
		val=0;
		if(b[0]<a[i]) val=f[i-1][0];
		for(j=1;j<=n;j++)
		{
			if(a[i]==b[j])
			    f[i][j]=max(f[i][j],val+1);
			else
			    f[i][j]=f[i-1][j];
			if(b[j]<a[i]) val=max(val,f[i-1][j]); 
		}
	}
	for(i=1;i<=n;i++) ans=max(ans,f[n][i]);
	printf("%d\n",ans);
	return 0;
}

T3 【POJ 3666】Making the Grade

首先\(b\)非严格单调就提醒我们\(b\)类似于很多段,段内值相同。

而且显然可以发现,\(b\)中的值一定都是\(a\)中出现过的。(显然更优)

\(dp[i][j]\)表示前\(i\)个数,第\(i\)个数填\(val[j]\)的最小值。

转移就从\(dp[i-1][1\to j]\)中的最小值进行转移,同样每次只会增加一个决策点,所以可以\(O(1)\)转移。

递增和递减都各自做一遍就好了。

#include<bits/stdc++.h>
#define ll long long
#define N 3000
using namespace std;
int n,m,a[N],b[N];
ll f[2][N],val,ans;
int exabs(int x)
{
	return x<0 ? -x:x;
}
int main()
{
	int i,j;
	scanf("%d",&n);
	for(i=1;i<=n;i++)
	{
		scanf("%d",&a[i]);
		b[i]=a[i];
	}
	sort(b+1,b+n+1);
	m=unique(b+1,b+n+1)-b-1;
	ans=1e9+7;
	memset(f,0,sizeof(f));
	int now=1;
	for(i=1;i<=n;i++)
	{
		val=0x7fffffff;
	    for(j=1;j<=m;j++)
		{
			val=min(val,f[now^1][j]);
			f[now][j]=val+exabs(b[j]-a[i]);
		}
		now^=1;
	}
	now^=1;
	for(i=1;i<=m;i++) ans=min(ans,f[now][i]);
	memset(f,0,sizeof(f));
	now=1;
	for(i=n;i>=1;i--)
	{
		val=0x7fffffff;
		for(j=1;j<=m;j++)
		{
			val=min(val,f[now^1][j]);
			f[now][j]=val+exabs(b[j]-a[i]);
		}
		now^=1;
	}
	now^=1;
	for(i=1;i<=m;i++) ans=min(ans,f[now][i]);
	printf("%lld\n",ans);
	return 0;
 } 

T4 「CH5102」Mobile Service

T5 「CH5104」I-country

T6 「CH5105」Cookies

T7 数字组合

T8 「CH5202」自然数拆分

T9 没有上司的舞会

T10 选课

T11 「POJ2228」Naptime

T12 环状运输

T13 「CF24D」Broken robot

T14 「POJ2411」Mondriaan's Dream

T15 「NOI2001」炮兵阵地

收获总结

这几天一直在做这些不做任何优化的简单\(dp\),学会了不少\(dp\)状态的设立。

总而言之\(dp\)状态是为了转移而服务的,怎样更好维护前一步信息就怎样去设,只要空间时间允许,几维不成问题。

再者就是转移时的技巧。

第一,如果每次决策点的增加量不多,那么我们可以维护它来优化时间。

第二,最基本的滚动数组优化空间。

第三,环形dp用破环为链,原数组增加一倍,删边等来消除后效性。

第四,有时用记忆化搜索代替顺序\(dp\),代码复杂度会小的多。

第五\(\cdots\)

总之不要怕\(dp\),实质就是抓住每一个需要维护的信息,用方程来转移。

路漫漫其修远兮,吾将上下而求索。

2020.9.8

星期二 晴 闷热

上午

今天上午打了场NOIP模拟赛,有点自闭,感觉自己被降智了。。。

赛后觉得三道\(D1\ T2\)难度的题,在考场上硬是给我一种\(D2\ T2\)难度的错觉。

T1 doubt

一道构造题,应该是自己梦游了,本来都想到正确的贪心策略了,但是因为自己一瞬间觉得这个没有正确性而果断否决,后果就是爆零。。。

首先30分给我们贪心提示\(:\)

考虑预处理出 \(a[i] \ xor\ b[j]\),从小到大贪心选取,用过的不再选。

考虑当有$ a[i]\ xor\ b[j] = a[k]\ xor\ b[t]\(,选取顺序无所谓;当有\) a[i]\ xor\ b[j] = a[i]\ xor\ b[t]$,那么 \(b[j]=b[t]\),选取顺序也无所谓。

所以贪心的正确性可证。

那么对于 \(a\)\(b\) 分别建一颗字典树,一起 \(dfs\) 使得每个\(a_i\)和一个\(b_j\)相消。考虑将 \(a\) 字典树上的子树 \(A\)\(b\) 字典树上的子树 \(B\) 尽量合并相消,先合并\((ls_A,ls_B),(rs_A,rs_B)\),再合并 \((ls_A,rs_B),(rs_A,ls_B)\)。为保证复杂度正确, 当某棵子树已经消完时,直接 \(return\) 即可,最后需要对 \(c\) 重新排序。

#include<bits/stdc++.h>
#define ll long long
#define N 300000
#define M 12000010
using namespace std;
int n,a[N],b[N],sum[M];
int tr[M][2],js=2,cnt,ans[N];
void insert(int x,int pos)
{
	++sum[pos];
	for(int i=30;i>=0;i--)
	{
		if(x&(1<<i))
		{
			if(!tr[pos][1]) tr[pos][1]=++js;
			pos=tr[pos][1];
		}
		else
		{
			if(!tr[pos][0]) tr[pos][0]=++js;
			pos=tr[pos][0];
		}
		++sum[pos];
	}
}

#define ls1 sum[tr[rt1][0]]
#define rs1 sum[tr[rt1][1]]
#define ls2 sum[tr[rt2][0]]
#define rs2 sum[tr[rt2][1]]

int dfs(int rt1,int rt2,int x,int d)
{
	int i,j;
	if(d<0)
	{
		int p=min(sum[rt1],sum[rt2]);
		sum[rt1]-=p,sum[rt2]-=p;
		for(i=1;i<=p;i++) ans[++cnt]=x;
		return p;
	}
	int u=0;
	if(ls1&&ls2) u+=dfs(tr[rt1][0],tr[rt2][0],x,d-1);
	if(rs1&&rs2) u+=dfs(tr[rt1][1],tr[rt2][1],x,d-1);
	if(ls1&&rs2) u+=dfs(tr[rt1][0],tr[rt2][1],x+(1<<d),d-1);
	if(rs1&&ls2) u+=dfs(tr[rt1][1],tr[rt2][0],x+(1<<d),d-1);
	sum[rt1]-=u,sum[rt2]-=u;
	return u; 
}
int main()
{
	int i,j;
	scanf("%d",&n);
	for(i=1;i<=n;i++) scanf("%d",&a[i]),insert(a[i],1);
	for(i=1;i<=n;i++) scanf("%d",&b[i]),insert(b[i],2);
//	for(i=1;i<=js;i++) printf("%d ",sum[i]); 
	dfs(1,2,0,30);
	sort(ans+1,ans+cnt+1);
	for(i=1;i<=cnt;i++) printf("%d ",ans[i]);
	return 0;
    
}

T2 block

这道题才是三道题中最难的一题,首先我们推出高的矩阵转移到小的矩阵贡献好算,但是凹凸凹凸的这种就不行了,于是我们计划从高到低进行逐步合并(从最高往下每次新加一列,把这一列合并到相邻已知矩阵上,最后整个矩阵的答案记录于左节点上),显然知道一行只有两种状态,即黑白相间和非黑白相间,非黑白相间只能相邻行完全取反,而若是另一种情况则可以从上一行取反或者不取反得到。

代码很难,这是需要学习的。

#include<bits/stdc++.h>
#define N 200000
using namespace std;
const int mod=1e9+7;
char buf[1<<23],*p1=buf,*p2=buf,obuf[1<<23],*O=obuf;
#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;
}
int n,h[N],sorted[N],ans;
int dp[N][2][2][2],tmp[2][2][2];  // 0 表示黑白相间 
int L_R[N],R_L[N],last_h[N];// L的 R 是多少,R 的 L 是多少 
bool cmp(int x,int y){return h[x]>h[y];}
int pow_mod(int x,int y)
{
	int sum=1;
	while(y)
	{
		if(y&1)
		{
			sum=1ll*sum*x%mod;
		 } 
		 x=1ll*x*x%mod;
		 y>>=1;
	}
	return sum;
}
void merge(int dp_l[2][2][2],int dp_r[2][2][2],int res[2][2][2])
{
	int l,r,ll,rr,sl,sr;
	int js[2][2][2];
	memset(js,0,sizeof(js));
	for(l=0;l<2;l++)
		for(r=0;r<2;r++)
			for(ll=0;ll<2;ll++)
				for(rr=0;rr<2;rr++)
					for(sl=0;sl<2;sl++)
						for(sr=0;sr<2;sr++)
						{
							int t=sl|sr|(ll==rr);
							js[l][r][t]=(js[l][r][t]+1ll*dp_l[l][ll][sl]*dp_r[rr][r][sr]%mod)%mod;
						}
	memcpy(res,js,sizeof(js));
}
void calc(int L,int h)
{
	if(last_h[L]==h){
		return;
	}
	int i,j,k,js[2][2][2];
	memset(js,0,sizeof(js));
	int t=(last_h[L]-h)&1;
	for(i=0;i<2;i++)
	{
		for(j=0;j<2;j++)
		{
		    js[i][j][1]=dp[L][i^t][j^t][1];
		    js[i][j][0]=1ll*(dp[L][i][j][0]+dp[L][i^1][j^1][0])*pow_mod(2,last_h[L]-h-1)%mod;
		}
	}
	memcpy(dp[L],js,sizeof(js));
}
int main()
{
//	freopen("block20.in","r",stdin);
	int i,j,k;
	scanf("%d",&n);
	for(i=1;i<=n;i++)
	{
		//scanf("%d",&h[i]);
		h[i]=rd();
		sorted[i]=i;
	} 
	sort(sorted+1,sorted+n+1,cmp);
	for(i=1;i<=n;i++)
	{
		j=sorted[i];
		int L=j,R=j;
		memset(tmp,0,sizeof(tmp));
		tmp[0][0][0]=tmp[1][1][0]=1; //0为黑白相间 
		if(R_L[j-1])
		{
			int l=R_L[j-1],r=j-1;
			L_R[l]=0;R_L[r]=0;
			calc(l,h[j]);
			merge(dp[l],tmp,tmp);
			L=l;
		}
		if(L_R[j+1])
		{
			int l=j+1,r=L_R[j+1];
			L_R[l]=0;R_L[r]=0;
			 calc(l,h[j]);
			 merge(tmp,dp[l],tmp);
			 R=r;
		}
		L_R[L]=R;
		R_L[R]=L;
		last_h[L]=h[j];
		memcpy(dp[L],tmp,sizeof(tmp));
	}
	if(h[sorted[n]]>1)
	{
		int js[2][2][2];
		memset(js,0,sizeof(js));
	    int t=(h[sorted[n]]-1)&1;
	    for(i=0;i<2;i++)
	    {
		    for(j=0;j<2;j++)
		    {
		       js[i][j][1]=dp[1][i^t][j^t][1];
		       js[i][j][0]=1ll*(dp[1][i][j][0]+dp[1][i^1][j^1][0])*pow_mod(2,h[sorted[n]]-1-1)%mod;
		    }
	    }
	    memcpy(dp[1],js,sizeof(js));
	}
	for(i=0;i<2;i++)
		for(j=0;j<2;j++)
			for(k=0;k<2;k++)
            	ans=(ans+dp[1][i][j][k])%mod;
    printf("%d\n",ans);
	return 0;
}

T3 road

要不是读错题,会推不出来?

这道题要求\(n\)个点的边双连通分量方案数,而考场上我居然读成了求\(n\)个点的基环树方案数,好像凭空增加了不止一星半点的难度。。。

行吧,那我把这类题都搞懂不就好了。

首先是最简单的边双联分量方案数,因为它的形态只有3种,所以分别求组合数就可以得出答案。

\[ans=\frac{n!(n-2)}{8} + \frac{n!(n-3)}{4} + \frac{n!(n-3)(n-4)}{24} \]

因为\(n很大\),所以无法存下所有的\(n!\),那么我们再分块打表预处理\(n!\)即可。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll mod = 1e9 + 7, inv2 = 5e8 + 4, inv3 = 333333336, L = 1e8;
const ll p[13] = {1,927880474,933245637,668123525,429277690,733333339,724464507,957939114,203191898,586445753,698611116};
ll n, ans = 0;

ll get_mi() {
	ll k = n / L;
	ll rs = p[k], o = k * L;
	for (ll i = o + 1; i <= n; ++i) rs = rs * i % mod;
	return rs;
}

int main() 
{
	scanf("%lld", &n);
	ll mi = 1, t = inv2 * inv2 % mod;//4
	mi = get_mi();  // n!
	ans += (n - 3) * t % mod;
	t = t * inv2 % mod;
	ans += (n - 4) * t % mod;  //8
	ans += (n - 3) * (n - 4) % mod * t % mod * inv3 % mod;
	ans = ans % mod * mi % mod;
	printf("%lld\n", ans);
	return 0;
}

其次来学下\(n\)个点的基环树方案数

先复习下\(prufer\)序列:

对于一棵确定的无根树,对应着唯一确定的\(prufer\)序列。

  • 无根树转化为\(prufer\)序列:
  1. 找到度数为\(1\)中编号最小的点。

  2. 删除该节点并在序列中添加与该节点相连的节点编号

  3. 重复\(1,2\)操作,直到整棵树只剩下两个节点

  • \(prufer\)序列转化为无根树:
  1. 每次取出\(prufer\)序列中最前面的元素\(u\)

  2. 在点集中找到编号最小的没有在\(prufer\)序列中出现的元素\(v\)

  3. \(u,v\)连边,然后分别在对应\(set\)中删除

  4. 最后在点集中剩下两个节点,给它们连边

  • 性质(写了最实用的几条)
  1. \(n\)个点的无向完全图的生成树的计数:\(n^{n-2}\),即\(n\)个点的有标号无根树的计数。

  2. \(n\)个点度数依次为\(d_1,d_2,\cdots,d_n\)的无根树共有\(\frac{(n-2)!}{\prod_{i=1}^{n}(d_i-1)!}\)个,因为此时\(Prufer\)编码中的数字\(i\)恰好出现\(d_i-1\)次,\((n-2)!\)是总排列数。

  3. \(n\)个点的 有标号有根树的计数:\(n^{n-1}\)

现在我们就可以步入正题:

首先根据\(prufer\)序列,我们易得:当\(n\)个点有\(k\)
个连通块,把他们连成一棵树的方案数为\(\prod sz_i * n^{k-2}\)

那么在基环树上,每个点\(sz\)都为\(1\),除了环的\(sz\)\(i\),共有\(n-i+1\)个点(连通块)

所以最后公式为\(C_n^i*(i-1)!*i*n^{n-i+1-2}/2=C_n^i*i!*n^{n-i-1}/2\)

完结nice。

收获

学会了这种用字典树进行合并的做法,同时提醒自己一种贪心策略一定不要想当然的否决,要尽可能证明一下。

学会了一种复杂的\(dp\),涉及转移方向的考虑,状态的设立,两块间的合并,答案保存于左端点,\(memcpy\)来快速转移数组信息,排序用序号进行排序(之后调用即可)。

这道\(dp\)题更加告诉我们不要怕复杂的\(dp\),不要怕复杂的转移,因为本来题就这么恶心。

然后这种图啊树啊的计数,一定要往组合数方向思考,考虑各种情况下的方案数,这种计数问题“围绕基准点构造一个整体”的思想更是不可少,李煜东蓝书329页计数\(dp\)便详细的讲解了这种思想。

复习了\(prufer\)序列,学习了n个点有标号无根树和有根树的计数,以及有标号基环树和变双连通分量的计数。

路漫漫其修远兮,吾将上下而求索。

posted @ 2020-09-09 10:07  yzxx_qwq  阅读(445)  评论(2编辑  收藏  举报