JZOJ模拟测爆0记

4 篇文章 0 订阅

JZOJ模拟测爆0记

By SemiWaker

如题

第一题 Guard

题意:

给一个二分图,每个点有点权A,要求选出一个点集{V}使得:

  1. A[x]T
  2. 原图存在某个匹配,使得选出的每一个点都被覆盖。

求方案数。

做题过程:

原题写的是:选出一个子图。
子图和点集差别太大了。。。
而且样例解释错掉了。。。

经过艰苦的读题,终于搞懂题目要干嘛了。
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。
有一个交换操作集合,一开始是空的。
操作:

  1. 交换P[x],P[y]。
  2. 加入交换操作(a,b),表示可以交换P[a]和P[b]。(并未实际操作)
  3. 询问:通过交换集合中的交换操作,是否能让P完成排序。
  4. 询问:设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满足 B1B2A1A2 就能给满足条件。

按照我理解的题意,A和B是个可以用Bitset表示的东西,然后水50。
然而看不懂题面的我还是爆0了

题解

首先,正确的题意就是良好就是能排序。

那么提供和需要的数的集合就是一个多重集了。
那么两个集合满足的条件变成 B1B2=A1A2
这就简洁多了。

多重集怎么表示呢?本来我想线段树合并想了好久,结果题解用的方法是Hash。

设每一个数出现次数为ci,那么

Hash=i=0nciHi

其中H是一个自定的常数。

这么设的好处是可以直接加减就可以修改合并多重集了。

而且,条件变为了 B1+B2=A1+A2
这就可以化简了 A1B1=(A2B2)
然后就可以用个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可不是一个好的现象。
水分水分再水分……

By SemiWaker

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值