JZOJ模拟测爆0记
By SemiWaker
如题
第一题 Guard
题意:
给一个二分图,每个点有点权A,要求选出一个点集{V}使得:
- ∑A[x]≥T
- 原图存在某个匹配,使得选出的每一个点都被覆盖。
求方案数。
做题过程:
原题写的是:选出一个子图。
子图和点集差别太大了。。。
而且样例解释错掉了。。。
经过艰苦的读题,终于搞懂题目要干嘛了。
30%暴力选点,然后判是否存在匹配。
我写了个匈牙利。
可是可以匹配没有选的点,那我就贪心匈牙利,先匹配选了的点。
当然,这是错的!
另外30%完全图,不用判匹配。
那就做中途相遇,左右两边各搞个表出来二分。
然而,两边选点的最多个数为min(n,m),所以就爆0了
100%不会,弃。
题解
30%
肯定不是暴力匈牙利,是上下界网络流。
选了的点下界为1就好了。
这个暴力比正解还麻烦
100%
做中途相遇的问题是:无法判断是否有匹配。
是否有匹配是和两边都有关的,这样就和暴力无异了。
但是,用Hall定理去判断是否有匹配,可以做到两边无关!!!
也就是说,左边的点集用Hall判一下,可以就加入表。
右边的也这么做,然后二分左边。
这样左右两边就无关了……
Hall定理判定的方法:
对于一个点集,设它的任意子集{V}能关联的另外一边的点集为{V’}。如果对于任意子集{V},
|{V}|≤|{V′}|
,就说明有完美匹配。
暴力枚举子集的时间复杂度是 O(3n) ,这里有点超时。
优化方法:从小的子集开始枚举,只判全集满不满足Hall定理。
如果不满足,把包含当前子集的所有集合都设为不满足。
简单来说,筛。
然后就可以过了。
原来Hall定理可以这么用,直接用来暴力判断是否有匹配
贴代码
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <cmath>
#include <algorithm>
using namespace std;
int E[50];
int n,m;
int W[30],V[30],T;
int Cnt[(1<<20)+10];
char ST[30];
bool f[(1<<20)+10];
typedef long long LL;
LL Num[(1<<20)+10];
bool Check(int S,int n,int p)
{
int c1=0;
int k=0;
for (int i=0;i!=n;++i)
if ((S>>i)&1)
{
c1++;
k|=E[i+p];
}
return c1<=Cnt[k];
}
int main()
{
freopen("guard.in","r",stdin);
freopen("guard.out","w",stdout);
for (int S=0;S<(1<<20);++S)
for (int i=0;i!=20;++i)
if ((S>>i)&1) Cnt[S]++;
scanf("%d%d",&n,&m);
for (int i=1;i<=n;++i)
{
scanf("%s",ST);
for (int j=0;j!=m;++j)
if (ST[j]=='1')
{
E[i]|=(1<<j);
E[n+j+1]|=(1<<i-1);
}
}
for (int i=1;i<=n;++i) scanf("%d",&W[i]);
for (int i=1;i<=m;++i) scanf("%d",&V[i]);
scanf("%d",&T);
for (int S=0;S!=(1<<n);++S) f[S]=1;
for (int S=0;S!=(1<<n);++S)
if (f[S] && !Check(S,n,1))
{
for (int S1=S;S1!=(1<<n);++S1)
if ((S&S1)==S) f[S1]=0;
}
int k=0;
for (int S=0;S!=(1<<n);++S)
if (f[S])
{
Num[k]=0;
for (int i=0;i!=n;++i)
if ((S>>i)&1)
{
Num[k]+=W[i+1];
}
k++;
}
sort(Num+0,Num+k);
LL ans=0;
for (int S=0;S!=(1<<m);++S) f[S]=1;
for (int S=0;S!=(1<<m);++S)
if (f[S] && !Check(S,m,n+1))
{
for (int S1=S;S1!=(1<<m);++S1)
if ((S&S1)==S) f[S1]=0;
}
for (int S=0;S!=(1<<m);++S)
if (f[S])
{
LL Sum=0;
for (int i=0;i!=m;++i)
if ((S>>i)&1)
{
Sum+=V[i+1];
}
int p=lower_bound(Num+0,Num+k,LL(T)-Sum)-Num;
ans+=k-p;
}
printf("%I64d\n",ans);
return 0;
}
第二题 Trie
题意
给出一些字符串,每个字符串可以任意重新排列,问重新排列之后的字符串建出的最小Trie有多少个点。
做题过程
一看题,果断写了个排序+求Height以为自己秒了。
结果自己被秒了
题解
关键问题:到底怎么排列?
考虑全部字符串的最长公共前缀,这个把所有字母个数min一下就可以求出长度了。
显然把全部字符串的最长公共前缀排在前面是最优的。
然后Trie就分叉了。
具体怎么分叉呢?我们可以暴力枚举。
当然,可能是多叉的,但是没关系,就分成两个集合就好了。
然后求两个集合内部的Trie的最小点数,加起来再取最小。
问题来了:全部字符串的最长公共前缀已经消去了一部分了,怎么记下来?
方法:
不记。把两个子集合中,每个显然都会有最长公共前缀的这一段。两个加起来就多了一段,减去即可。
最后就是DP了。
贪心不证活该爆0
贴代码
#include <cstdio>
#include <cstdlib>
#include <cmath>
#include <cstring>
#include <algorithm>
using namespace std;
char ST[1000010];
int Num[30][30];
int Min[30];
int f[(1<<17)+10];
typedef long long LL;
int n;
int main()
{
freopen("trie.in","r",stdin);
freopen("trie.out","w",stdout);
scanf("%d",&n);
for (int i=0;i<n;++i)
{
scanf("%s",ST);
int len=strlen(ST);
for (int j=0;j!=len;++j) Num[i][ST[j]-'a']++;
}
for (int S=1;S<(1<<n);++S)
{
int cnt=0;
for (int i=0;i<26;++i) Min[i]=1000001;
for (int i=0;i!=n;++i)
if ((S>>i)&1)
{
cnt++;
for (int j=0;j<26;++j)
Min[j]=min(Min[j],Num[i][j]);
}
int sum=0;
for (int j=0;j<26;++j) sum+=Min[j];
if (cnt==1) {f[S]=sum;continue;}
f[S]=10000000;
for (int S1=(S-1)&S;S1>0;S1=S&(S1-1))
{
f[S]=min(f[S],f[S1]+f[S^S1]);
}
f[S]-=sum;
}
printf("%d\n",f[(1<<n)-1]+1);
return 0;
}
第三题 Sort
题意
给出一串数p,设排序之后的这串数为q。
有一个交换操作集合,一开始是空的。
操作:
- 交换P[x],P[y]。
- 加入交换操作(a,b),表示可以交换P[a]和P[b]。(并未实际操作)
- 询问:通过交换集合中的交换操作,是否能让P完成排序。
询问:设a能通过交换到的所有位置叫做a的群集。
如果a的群集能通过交换完成排序,那么a的群集是良好的。
求点对(a,b),使得:- a、b不在同一个群集
- a的群集和b的群集不良好
- 加入(a,b)后,a的群集良好
(a,b)和(b,a)是同一对
题面绕口+烧脑
做题过程
原题写的是:(a,b)和(b,a)不是同一对,而且良好的定义是:
对于每一个在a的集群中的x,可以通过交换使得P[x]=Q[x]
第一个直接错掉不说,第二个的意思到底是:只要每一个分别能交换到就好,还是要所有同时都能交换到呢?
这样爆0我一点办法都没有
显然是并查集,然后要满足群集之间的某些条件:
设一个群集内能提供的数的集合为A,一个群集内需要的数的集合为B,如果两个集合的A和B满足
B1∪B2⊂A1∪A2
就能给满足条件。
按照我理解的题意,A和B是个可以用Bitset表示的东西,然后水50。
然而看不懂题面的我还是爆0了
题解
首先,正确的题意就是良好就是能排序。
那么提供和需要的数的集合就是一个多重集了。
那么两个集合满足的条件变成
B1∪B2=A1∪A2
这就简洁多了。
多重集怎么表示呢?本来我想线段树合并想了好久,结果题解用的方法是Hash。
设每一个数出现次数为ci,那么
其中H是一个自定的常数。
这么设的好处是可以直接加减就可以修改合并多重集了。
而且,条件变为了
B1+B2=A1+A2
这就可以化简了
A1−B1=−(A2−B2)
然后就可以用个Map之类的水过。
Hash强无敌,线段树废物
贴代码
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <cmath>
#include <algorithm>
#include <map>
using namespace std;
typedef unsigned long long ULL;
typedef long long LL;
const ULL Tot=0xffffffffffffffffull;
const int H=1000003;
int A[1100000],B[1100000];
int Fa[1100000],Siz[1100000];
ULL HashP[1100000],HashQ[1100000];
ULL HX[1000001];
int Tmp[1100000];
int GetFa(int x);
LL ans;
int SortCnt;
int n,m;
map<ULL,int> M;
void Insert(ULL x);
void Remove(ULL x);
int main()
{
freopen("sort.in","r",stdin);
freopen("sort.out","w",stdout);
scanf("%d%d",&n,&m);
for (int i=1;i<=n;++i) scanf("%d",&A[i]),Tmp[i-1]=A[i];
sort(Tmp+0,Tmp+n);
int l=unique(Tmp+0,Tmp+n)-Tmp;
for (int i=1;i<=n;++i)
{
int p=lower_bound(Tmp+0,Tmp+l,A[i])-Tmp;
A[i]=B[i]=p;
}
//请无视这段没用的离散化
sort(B+1,B+n+1);
HX[0]=1;
for (int i=1;i<=l;++i) HX[i]=HX[i-1]*H;
for (int i=1;i<=n;++i) Fa[i]=i,Siz[i]=1,HashP[i]=HX[A[i]],HashQ[i]=HX[B[i]];
for (int i=1;i<=n;++i) Insert(i);
while (m--)
{
int op,x,y;
scanf("%d",&op);
if (op<=2) scanf("%d%d",&x,&y);
if (op==1)
{
int x1=GetFa(x);
int y1=GetFa(y);
if (x1==y1) {swap(A[x],A[y]);continue;}
Remove(x1);
Remove(y1);
HashP[x1]+=Tot-HX[A[x]]+1;
HashP[y1]+=Tot-HX[A[y]]+1;
swap(A[x],A[y]);
HashP[x1]+=HX[A[x]];
HashP[y1]+=HX[A[y]];
Insert(x1);
Insert(y1);
} else
if (op==2)
{
x=GetFa(x);
y=GetFa(y);
if (x==y) continue;
Remove(x);
Remove(y);
if (Siz[x]<Siz[y]) swap(x,y);
Fa[y]=x;
HashP[x]+=HashP[y];
HashQ[x]+=HashQ[y];
Siz[x]+=Siz[y];
Insert(x);
} else
if (op==3)
{
if (M[0]==n) printf("YES\n");
else printf("NO\n");
} else
{
printf("%I64d\n",ans);
}
}
return 0;
}
int GetFa(int x)
{
if (x==Fa[x]) return x;
return Fa[x]=GetFa(Fa[x]);
}
void Insert(ULL x)
{
if (HashP[x]!=HashQ[x])
ans+=LL(Siz[x])*M[HashQ[x]-HashP[x]];
M[HashP[x]-HashQ[x]]+=Siz[x];
}
void Remove(ULL x)
{
if (HashP[x]!=HashQ[x])
ans-=LL(Siz[x])*M[HashQ[x]-HashP[x]];
M[HashP[x]-HashQ[x]]-=Siz[x];
}
后记
遇到没见过的就爆0可不是一个好的现象。
水分水分再水分……