日常机房模拟
今天不是停课…所以所有人都来了
有一道T1,本来想到了正解,结果忘了可以启发式合并,觉得时间复杂度过不去就否定掉了
T2想了2个半小时,最后交了一个自己认为错的程序结果A了(下午发现是正解2333333)
T3没时间了,辣鸡骗分苟过了10pts233333
T1
题意简述:
维护一个数据结构,支持两种操作
1、将一张图中任意两点之间连一条边
2、查询某一次操作后某一个点所在的联通块的大小
我们可以很自然的想到可持久化并查集
虽然正解可以不用可持久化并查集,但是可持久化并查集肯定是能A的(把修改siz和fat的部分写两个函数,用主席树搞一搞应该能出来)
然而
开考的时候半个小时教练都没有断网,然后我们班一个妹子在这个时间里打开了一篇博客把可持久化并查集学习了,随后A掉了这道题
正解是并查集加二分…(思想有点类似于时间分治)
由于我们每一次的查询都仅仅针对任何一个点
那么我们考虑将每一个点被修改时候的情况存储起来。
我们发现每一个点的查询只跟它最后一次被修改的时间有关系,那么我们对于任意一个点,在其被修改以后记录下这次被修改后的并查集的根节点。
然后我们再对每一个节点开一个vector,vector里存储二元组,分别表示该节点是哪次被修改,以及那一次被修改过后的siz。
稍微想一想就可以发现单次合并的时候并不需要把两个并查集里面的所有点的信息都加入vector(考试的时候就是栽在这里,认为需要全部push进去导致否认了这个本来正确的算法),我们可以只将根节点的信息push进去,然后合并并查集的时候不压缩路径这个是重中之重,因为我们可以利用启发式合并的性质来保证复杂度。
我们可以发现,如果不压缩路径的话,我们就可以完成以下的操作:
当要查询一个节点的时候,一直跳该节点的父亲节点,而我们已经事先记录下来该节点最后一次修改实在什么时候,那么我们假设跳到某一个节点u的时候,该节点的修改时间已经比我们要查询的时间更在后面了的话,那么就说明我们需要查询的信息就在这个根节点上面。
而我们每一次合并的时候都会把信息储存在根节点里面,所以我们跳到的点上面肯定会拥有那一次修改之后的信息。
所以对于每一个并查集都要多记录一个并查集的深度,每次将深度浅的合并到深的那里,否则的话是无法保证复杂度的,这叫做启发式合并(这样的操作比起路径压缩来说其实是非常慢的,虽然可以证明均摊后的复杂度是log,但是如果说出题人真的很想卡你,它可以把这个“均摊”变成“一次"23333)
其实代码比本蒟蒻讲解的更好…
所以献上丑陋无比的代码
#include
#include
#include
#include
#define OP "build"
const int MAXN=1e5+5;
int read()
{
int X=0,w=0;
char ch=0;
while(!isdigit(ch)) {w|=ch=='-';ch=getchar();}
while(isdigit(ch)) X=(X<<3)+(X<<1)+(ch^48),ch=getchar();
return w?-X:X;
}
class Node
{
public:
int tim;
int siz;
bool operator<(const Node &z)const
{
return tim<z.tim;
}
};
int lastans;
int siz[MAXN];
int f[MAXN];
int dep[MAXN];
int t[MAXN];
std::vector<Node>V[MAXN];
int find(int x)
{
return x==f[x]?x:find(f[x]);
}
void unionn(int u,int v,int i)
{
if(find(u)==find(v))
{
return ;
}
int fu=find(u);
int fv=find(v);
if(dep[fu]<dep[fv])
{
std::swap(fu,fv);
}
f[fv]=fu;
siz[fu]+=siz[fv];
if(dep[fu]==dep[fv])
{
dep[fu]++;
}
V[fu].push_back((Node){i,siz[fu]});
t[fv]=i;
}
int solve(int u,int times)
{
while(u!=f[u]&&t[u]<=times)
{
u=f[u];
}
std::vector<Node>::iterator it=std::upper_bound(V[u].begin(),V[u].end(),(Node){times,0});
it--;
return it->siz;
}
int main()
{
std::freopen(OP".in","r",stdin);
std::freopen(OP".out","w",stdout);
int n,m;
n=read();
m=read();
for(int i=1;i<=n;i++)
{
f[i]=i;
dep[i]=siz[i]=1;
V[i].push_back((Node){0,1});
}
for(int i=1;i<=m;i++)
{
int opt,x,y;
opt=read();
x=read();
y=read();
int u=x+lastans;
int v=y+lastans;
if(opt==1)
{
unionn(u,v,i);
}
else
{
std::printf("%d\n",lastans=solve(v,u));
}
}
return 0;
}
/*
5 5
1 1 2
2 0 1
1 1 2
1 0 4
2 2 1
*/
好的A掉了2333333(考场上没有想到这一点,最后苟了特殊数据50pts)
T2
题意简述:
有一个全部为正整数的序列,你和另外一个人轮流按序列顺序取数,当轮到你取的时候,对于任意的一个数,你可以通过把它送给你的对手并且借之以获得下一次继续取的权利,或者取走这一个数之后,下一次取数的权利交给对手,每一次只能取走或送掉这一个序列的第一个数。问你双方都采取最优策略的情况,你最多能够获取多少利润。
很有博弈性质的一道dp的题…
开始的时候以为dirty只会固定选择pure选了之后的时候的那一个物品,然后好奇这道题为什么这么水…
后来发现两个人都采取了最优策略,所以我们的dp其实不是很好想
这道题拥有一个天坑,那就是如果你把dp方程定义为dp[i]表示两人互相分配到第i个城市的时候pure能够获得的最大收益,你这辈子都推不出来
因为一个问题能够使用dp来解决的前提条件是它要有无后效性,然而你正着推并没有(非常显而易见…)
所以我们应该定义dp[i]表示从第i个开始取取到最后一个的最大收益,那么状态的转移只有两种情况
我们首先定义一个sum数组记录从i到n的后缀和
1、我们要选择第i个物品,那么dp[i]=sum[i+1]-dp[i+1]+v[i]
2、不选择这个物品,那么dp[i]=dp[i+1]
所以dp[i]=std::max(dp[i+1],sum[i+1]-dp[i+1]+v[i]).
代码实在是太SB了…(考场上问题想复杂了,定义了二维)
#include
#include
#include
#include
#define OP "distribute"
#define LL long long
const int MAXN=1e5+5;
int a[MAXN];
LL dp[MAXN][2];
LL sum[MAXN];
int main()
{
std::freopen(OP".in","r",stdin);
std::freopen(OP".out","w",stdout);
int n;
std::scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d",a+i);
}
dp[n][0]=dp[n][1]=sum[n]=a[n];
for(int i=n-1;i>0;i--)
{
sum[i]=sum[i+1]+a[i];
dp[i][0]=std::max(dp[i+1][0],sum[i+1]-dp[i+1][1]+a[i]);
dp[i][1]=std::max(dp[i+1][1],sum[i+1]-dp[i+1][0]+a[i]);
}
return std::printf("%lld",dp[1][0]),0;
}
``
于是T2成为今天唯一一道轻松愉快 切掉的题目(其实花了2h,主要是一开始题意理解错误,然后还给那个错误的程序写了一份对拍代码,实在是很伤)
T3
题意简述:
给你一个无向图,问你其中是否存在简单环,若存在则输出所有简单环上面的所有边,否则就输出0。
本来没有人真正想到正解,
但是又俩人,最后交卷之前,抱着骗分的心理(写不来暴力但是又不想T3爆炸),写了一个割点求边双,竟然奇迹般的过了样例(实际上是他们俩发现了边双可以过样例才这么写…),然后交上去之后
A了…
A了…
A了…
woc…真的是俩小天才(我真是想破脑子也没有想出来边双…)
所以我的考场代码是这个样子的
#include
#define OP "find"
int main()
{
std::freopen(OP".in","r",stdin);
std::freopen(OP".out","w",stdout);
std::puts("0\n");
return 0;
}
具体的证明过程比较复杂,但是我们的题解上面写的很简单…所以我就直接把solution贴上来吧
在做这道题之前我一直没有意识到自己的图的连通性学得巨tm差…
然后我终于会求割点和桥了2333333333(去年就讲了解法然后我现在才会)
#include
#include
#define OP "find"
const int MAXN=1e5+5;
class Edge
{
public:
int nxt;
int to;
}edge[MAXN<<1];
int head[MAXN];
int num=1;
void add(int from,int to)
{
edge[++num].nxt=head[from];
edge[num].to=to;
head[from]=num;
}
int low[MAXN];
int dfn[MAXN];
bool vis[MAXN<<1];
bool ok[MAXN<<1];
int stk[MAXN<<1];
int indx=0;
int top=0;
int color[MAXN];
int cnt=0;
int vc[MAXN<<1];
void tarjan(int x,int fe)
{
indx++;
dfn[x]=low[x]=indx;
for(int i=head[x];i;i=edge[i].nxt)
{
int v=edge[i].to;
if((i^1)==fe)
{
continue;
}
if(!vis[i])
{
vis[i^1]=vis[i]=1;
stk[++top]=i;
}
if(!dfn[v])
{
tarjan(v,i);
low[x]=std::min(low[x],low[v]);
if(low[v]>=dfn[x])
{
int nump=0;
int nume=0;
cnt++;
while(1)
{
int e=stk[top--];
if(color[edge[e].to]!=cnt)
{
color[edge[e].to]=cnt;
nump++;
}
if(color[edge[e^1].to]!=cnt)
{
color[edge[e^1].to]=cnt;
nump++;
}
vc[++nume]=e;
if(e==i)
{
break;
}
}
if(nump==nume)
{
for(int i=1;i<=nume;i++)
{
ok[vc[i]]=ok[vc[i]^1]=1;
}
}
}
}
else
if(dfn[v]<low[x]){
low[x]=std::min(low[x],dfn[v]) ;
}
}
}
int main()
{
std::freopen(OP".in","r",stdin);
std::freopen(OP".out","w",stdout);
int n,m;
std::scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
int u,v;
std::scanf("%d%d",&u,&v);
add(u,v);
add(v,u);
}
for(int i=1;i<=n;i++)
{
if(!dfn[i])
{
tarjan(i,0);
}
}
int ans=0;
for(int e=2;e<=num;e+=2)
{
if(ok[e])
{
ans++;
}
}
std::printf("%d\n",ans);
for(int e=2;e<=num;e+=2)
{
if(ok[e])
{
std::printf("%d ",e/2);
}
}
std::puts("\n");
return 0;
}
最后得分还被卡掉了20pts,只有140…