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,那么状态转移方程就是
其中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是真的要练
执.

浙公网安备 33010602011771号