20200524 考试总结


果然自己菜的真实……

考试经过

先浏览题面,T1发现是概率期望有关的图论题,题面有些复杂,T2看了发现是高斯消元异或方程组,就
“这不是板子题吗”
上来就写,写+调40min,手模几个数据发现正确性可以,就交了,看T3发现不可做,回来干T1
仔细读题发现和聪聪可可有点像,没多想就写了XIN队dfs,觉得可以优化就写了一个小剪枝,过了样例就交了
T3没有思路,剩20分钟的时候想到转化成矩阵搞一搞,依然暴力XIN队,写完一看,为啥都是0啊???不管了交了,等等有个细节,m可以等于0!,这不就是1吗,一波特判提交带走
结果55+0+20=75
T1我连记忆化都没想到我就是个废*
T2:& --> ^,40-->0……我是智障
天哪T3我竟然骗了20分!我能高兴一年
不挂分是不可能的,应该还是正常发挥,古人云:傻人就要多做题。。。

景区路线规划

法一:记忆化搜索

在爆搜基础上加一个double数组记录对于走到每个景点x,剩t时间可以获得的期望收益,每次搜完记录一下,下一次搜索时,如果数组有值就直接返回
对于两个人搜两遍即可,最好分开写,一起写可能有玄学错误
剪枝体现在总状态只有nk个,所以把他枚举完了之后剩下的就可以直接更新,复杂度就是nk,顶多加一些常数

#include <bits/stdc++.h>
using namespace std;
double exs=1e-8;
bool pd(double x,double y)
{
	if(x>y)swap(x,y);
	if(y-x<=exs)return 1;
	return 0;
}
struct node{
	int from,to,next,w;
}a[20005];
int head[105],mm=1;
void add(int x,int y,int w)
{
	a[mm].from=x;a[mm].to=y;a[mm].w=w;
	a[mm].next=head[x];head[x]=mm++;
}
int n,m,k;
int c[105],h1[105],h2[105];
double dp1[105][500],dp2[105][500];
double dfs1(int x,int t)
{
	if(!pd(dp1[x][t],-1.0))return dp1[x][t];
	if(t<=0)
	{
		dp1[x][t]=0.0;
		return 0.0;
	}
	double ans=0;int d=0;
	for(int i=head[x];i;i=a[i].next)
	{
		if(a[i].w>t)continue;
		int y=a[i].to;d++;
		ans+=h1[y];
		ans+=dfs1(y,t-a[i].w);
	}
	if(!d)
	{
		dp1[x][t]=0.0;
		return 0.0;
	}
	dp1[x][t]=ans/(double)d;
	return dp1[x][t];
}
double dfs2(int x,int t)
{
	if(!pd(dp2[x][t],-1.0))return dp2[x][t];
	if(t<=0)
	{
		dp2[x][t]=0.0;
		return 0.0;
	}
	double ans=0;int d=0;
	for(int i=head[x];i;i=a[i].next)
	{
		if(a[i].w>t)continue;
		int y=a[i].to;d++;
		ans+=h2[y];
		ans+=dfs2(y,t-a[i].w);
	}
	if(!d)
	{
		dp2[x][t]=0.0;
		return 0.0;
	}
	dp2[x][t]=ans/(double)d;
	return dp2[x][t];
}
double play1(int t)
{
	double ans=0;
	for(int i=1;i<=n;i++)
	{
		ans+=h1[i];
		ans+=dfs1(i,t-c[i]);
	}
	ans/=(double)n;
	return ans;
}
double play2(int t)
{
	double ans=0;
	for(int i=1;i<=n;i++)
	{
		ans+=h2[i];
		ans+=dfs2(i,t-c[i]);
	}
	ans/=n;
	return ans;
}
int main()
{
	cin>>n>>m>>k;
	for(int i=1;i<=n;i++)
	 scanf("%d%d%d",&c[i],&h1[i],&h2[i]);
	for(int i=1;i<=m;i++)
	{
		int x,y,z;
		scanf("%d%d%d",&x,&y,&z);
		add(x,y,z+c[y]);add(y,x,z+c[x]);
//		du[x]++;du[y]++;
	}
	for(int i=0;i<=n;i++)
	 for(int j=0;j<=k;j++)
	 {
	 dp1[i][j]=-1.0;dp2[i][j]=-1.0;	
	 }
	printf("%.5lf %.5lf",play1(k),play2(k));
}

坑点:
1.看见注掉的东西了吗,不能一开始记录节点的度数,因为题中说只有下一个能到并且玩的时候才有对答案贡献,所以需要在枚举边的时候记录
2.浮点数比较最好别用==,防止卡精度手写比较函数比较好

法二:动态规划

设f[i][j]为从第i个点出发时(不算这个点的收益),还剩j时间,还能获得的期望收益
我们枚举i节点的每个合法的(可以达到并玩的)相连节点k,那么状态转移方程就是

\[f_{i,j}=\sum \frac{f_{k,j-w_{i,k}-t_k}}{d}+h_k \]

其中d是合法节点总数,w,t分别是走到,玩的花费,最后答案就是$\sum_{i=1}^n \frac{f_{i,j-t_i}+h_k }{n} $

#include <bits/stdc++.h>
using namespace std;
struct node{
	int from,to,next,w;
}a[20005];
int head[105],mm=1;
void add(int x,int y,int w)
{
	a[mm].from=x;a[mm].to=y;a[mm].w=w;
	a[mm].next=head[x];head[x]=mm++;
}
int n,m,k;
int c[105],h1[105],h2[105];
double f1[105][500],f2[105][500];
int main()
{
	cin>>n>>m>>k;
	for(int i=1;i<=n;i++)
	 scanf("%d%d%d",&c[i],&h1[i],&h2[i]);
	for(int i=1;i<=m;i++)
	{
		int x,y,z;
		scanf("%d%d%d",&x,&y,&z);
		add(x,y,z+c[y]);add(y,x,z+c[x]);
	}
	double ans1=0,ans2=0;
	for(int j=0;j<=k;j++)
	 for(int i=1;i<=n;i++)
	 {
	 	int d=0;
	 	for(int p=head[i];p;p=a[p].next)
	 	{
	 		if(j-a[p].w<0)continue;
	 		int y=a[p].to;d++;
			f1[i][j]+=f1[y][j-a[p].w]+h1[y];
		 }
		if(!d)continue;
		f1[i][j]/=(double)d;
	 }
	for(int j=0;j<=k;j++)
	 for(int i=1;i<=n;i++)
	 {
	 	int d=0;
	 	for(int p=head[i];p;p=a[p].next)
	 	{
	 		if(j-a[p].w<0)continue;
	 		int y=a[p].to;d++;
			f2[i][j]+=f2[y][j-a[p].w]+h2[y];
		 }
		if(!d)continue;
		f2[i][j]/=(double)d;
	 }
	for(int i=1;i<=n;i++)
	{
	  ans1+=f1[i][k-c[i]]+h1[i];
      ans2+=f2[i][k-c[i]]+h2[i];
	}
	ans1/=n;ans2/=n;
	printf("%.5lf %.5lf",ans1,ans2);
	return 0;  	 
}  

注意循环顺序,j作为状态应该在最外层

这名字真是烂大街了

法一:异或方程组

列出异或方程组,消元后对每个自由元分析,爆搜回代,代码见这里

法二:树形dp

用f[x][0-3]分别代表x节点所有子树都点亮的情况下,x按了亮,按了不亮,不按不亮,不按亮的情况下最少次数
用dfs树形dp,考虑x的子节点状态
初始化成正无穷,如果x是叶子,那么只有按了亮,不按不亮合法,分别赋值1和0
如果x按了,说明他的子节点以前都是不亮,所以0,1状态由1,2状态转移过来,现在问题是具体由哪个状态过来,注意到x是否要按与x的子节点按不按有关,若子节点按了奇数次就不用按就亮,否则要按一次
状态转移取最小值,统计最小值之和的同时,每次记录取的最小值按不按,所以用一个cnt记录按的次数,并且记录下两个决策之间差值的绝对值p,最后如果cnt是奇数就转移到0,1就用最小值之和加上p来更新,偶数就反一下
2,3状态由0,3状态转移过来,思路也一样,细节在代码里,要好好注意

#include <bits/stdc++.h>
using namespace std;
struct node{
	int from,to,next;
}a[210];
int head[110],mm=1;
int f[105][5];bool v[105];
//0:按了亮 1:按了不亮 2:不按亮 3:不按不亮 
struct pp{
	int x0,x1,x2,x3;
};
void add(int x,int y)
{
   a[mm].from=x;a[mm].to=y;
   a[mm].next=head[x];head[x]=mm++; 
} 
void clear()
{
	mm=1;
	memset(head,0,sizeof(head));
	memset(a,0,sizeof(a));
	memset(f,0x3f,sizeof(f));
	memset(v,0,sizeof(v));
}
pp dp(int x)
{
	v[x]=1;pp ans;
	bool ga=0;
	int cnt1=0,com1=9999999,cnt2=0,com2=9999999,an1=0,an2=0;
	for(int i=head[x];i;i=a[i].next)
	{
		int y=a[i].to;
		if(v[y])continue;
		pp yy=dp(y);
		if(yy.x1<yy.x3)cnt1++;an1+=min(yy.x1,yy.x3);
		com1=min(com1,abs(yy.x1-yy.x3));
		if(yy.x0<yy.x2)cnt2++;an2+=min(yy.x0,yy.x2);
		com2=min(com2,abs(yy.x0-yy.x2));
		ga=1;
	}
	if(!ga)
	{
		f[x][0]=1;f[x][3]=0;
		ans.x0=f[x][0];ans.x1=f[x][1];
		ans.x2=f[x][2];ans.x3=f[x][3];
		return ans;
	}
	f[x][0]=1;f[x][1]=1;f[x][2]=0;f[x][3]=0;
	f[x][1]++;
	if(cnt1%2)f[x][1]+=an1,f[x][0]+=an1+com1;
	else f[x][0]+=an1,f[x][1]+=an1+com1;
	if(cnt2%2)f[x][2]+=an2,f[x][3]+=an2+com2;
	else f[x][3]+=an2,f[x][2]+=an2+com2;
	ans.x0=f[x][0];ans.x1=f[x][1];ans.x2=f[x][2];ans.x3=f[x][3];
	return ans; 
}
int n;
int main()
{
	scanf("%d",&n);
	while(n)
	{
		clear();
		for(int i=1;i<=n-1;i++)
		{
			int x,y;
			scanf("%d%d",&x,&y);
			add(x,y);add(y,x); 
		}
		pp ans=dp(1);
		printf("%d\n",min(ans.x0,ans.x2));
		scanf("%d",&n);
	}
	return 0;
}  

奇怪的道路

看题是个计数问题,反正我没看出来是状压dp……
寻找最小的数据k,考虑将他进行状压,用二进制数表示每一位连边奇数还是偶数,注意这里要压9位,每次把他自己这个点也压进去
先想到f[i][j][s]表示已经处理i个点,j条边,i前面k个状态是s,这样一个方程,每次限制只能与前面的点连边,但仔细思考会发现,他会造成重复转移
假如我们把当前点放在最后,现在状态是00001,他可以和1点连边转移到10001,也可以和2连边转移到01001,然后考虑10001可以继续连2到11000,01001也可以继续连2转移到11000,这样我们发现一样的方案被算了两次,所以这个方法不行,原因是转移比较混乱
我们考虑加一维限制条件,新增一个l,用f[i][j][s][l]表示已经当前处理到i个点,连了j条边,i同他自己和前面k个状态为s,已经连好i-k到i-k+l-1,正在处理i-k+l时的方案数
我看网上题解大多是把l定义成了正在处理i-l时的方案,倒叙枚举,我的状态设计稍微有些不同,转移就是:
考虑i和i-k+l连边,分三种情况,刷表转移:
首先判断合法性,i-k+l<=0时不合法,因为肯定没有编号比0小的节点
1.当前边和i-k+l连,就有f[i][j+1][s(1<<k)(1<<l)][l]+=f[i][j][s][l];
2.如果不连,那么i-k和i-k+l之间都不能连,转移到下一个l,f[i][j][s][l+1]+=f[i][j][s][l];
3.如果l==k,证明这个i已经处理完了,考虑下一个i即可,f[i+1][j][s>>1][max(0,k-i)]+=f[i][j][s][l];
这里注意由于我们的状态定义,第四维不能取零,因为你不能保证0时一定合法(可能出现i-k<=0的情况),所以加一个max判断,保证转移到的状态一定合法,即把当前状态转到i+1时l最小的合法状态
初始化f[1][0][0][k]=1,答案是f[n][m][0][k],之所以取k不取0也是因为合法性的问题
是道好题,计数dp这方面不熟悉,尤其状压还得练

#include <bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int n,m,K;
int f[35][35][1<<9][11];
signed main()
{
	cin>>n>>m>>K;
	f[1][0][0][K]=1;
	for(int i=1;i<=n;i++)
	  for(int j=0;j<=m;j++)
	   for(int k=0;k<=(1<<(K+1))-1;k++)
	    for(int l=0;l<=K;l++)
		{
			if(i-K+l<=0)continue;
			if(l!=K)
			{
				f[i][j+1][k^(1<<l)^(1<<K)][l]=(f[i][j+1][k^(1<<l)^(1<<K)][l]+f[i][j][k][l])%mod;
				f[i][j][k][l+1]=(f[i][j][k][l+1]+f[i][j][k][l])%mod;
			}
		    else
		    {
		    	if(!(k&1))
				 f[i+1][j][k>>1][max(0,K-i)]=(f[i+1][j][k>>1][max(0,K-i)]+f[i][j][k][l])%mod;
			}
		 } 
	cout<<f[n][m][0][K];
	return 0;
}  

最后感谢cty提供朴素思路的反例和chy帮助整理思路>-<

考试反思

1.代码细节一定要注意,有时候整个板子背过了因为一个细节就崩了
2.搜索能写记忆化一定要写,不要干搜
3.dp是真的要练

执.

posted @ 2021-05-28 17:18  D'A'T  阅读(55)  评论(0)    收藏  举报