• 10月清北学堂培训 Day 3


    今天是钟皓曦老师的讲授~

    zhx:题很简单,就是恶心一些qwq~

    T1

    别人只删去一个字符都能AC,我双哈希+并查集只有40?我太菜了啊qwq

    考虑到越短的字符串越难压缩,越长的字符串越好压缩,所以我们可以先压缩短的字符串,再压缩长的字符串,这就是全损压缩法:

    全损压缩法:

    1.先把所有的字符串读进来,然后按照长度从小到大排序;

    2.为了压缩不成问题,我们应该从最短的字符串开始压缩;

    3.依次从 a , b , c …… , x , y , z , aa , ab ……这样的顺序进行压缩,如果两个字符串相同就把它们压缩成相同的,可以发现这样的压缩方法一定是最优的;

    #include<cstdio>
    #include<cstdlib>
    #include<cstring>
    #include<map>
    #include<string>
    #include<iostream>
    #include<algorithm>
    
    using namespace std;
    
    const int maxn=1010;
    
    map<string,string> ma;
    
    string z[maxn];
    
    int n,l=1,res[233],y[maxn];
    
    bool cmp(int a,int b)
    {
        return z[a]<z[b];
    }
    
    int main()
    {
        cin >> n;
        for (int a=1;a<=n;a++)
        {
            cin>>z[a];
            y[a]=a;
        }
        res[1]=1;
        sort(y+1,y+n+1,cmp);
        for (int a=1;a<=n;a++)
        {
            string now = z[y[a]];
            if (ma.count(now)!=0) ;
            else
            {
                string cur = "";
                for (int a=l;a>=1;a--)
                    cur = cur + (char)(res[a]+'a'-1);
                ma[now] = cur;
                res[1]++;
                for (int a=1;a<=l;a++)
                    if (res[a]>26)
                    {
                        res[a]=1;
                        res[a+1]++;
                    }
                if (res[l+1]) l++;
            }
        }
        for (int a=1;a<=n;a++)
            cout << ma[z[a]] << endl;
    
        return 0;
    }

    T2

    题意:

    给了一棵有向树(外向树:所有边都是从根向外指的树就是外向树),并且在树上随便插了一条边,我们需要把这条边找出来,并保证这条边的编号尽可能的大 。

    分两组情况:

    1. 知道根节点是谁:

    也就是能找到一个入度为 0 的点;

    也就说明违和的边是从根下连到根下的,分为两种边:返祖边,横叉边;

    但这两种边还是能归结到一种情况:有一个点的入度一定为 2;

    我们就删掉连着这个入度为 2 的编号最大的边就好了;

    2.不知道根节点是谁:

    如果我们找不到一个入度为 0 的点的话,也就是说新加的那一条边的终点就是根节点;

    那么会构成一个环,我们删掉环上编号最大的边删掉就好了;

    #include<cstdio>
    #include<cstdlib>
    #include<cstring>
    #include<algorithm>
    
    using namespace std;
    
    const int maxn=100010;
    
    int n,en,head,tail,in[maxn],ex[maxn][2],q[maxn],f[maxn],pre[maxn][2];
    
    bool use[maxn];
    
    struct edge
    {
        int s,e,p;
        edge *next;
    }*v[maxn],ed[maxn];
    
    void add_edge(int s,int e,int p)
    {
        en++;
        ed[en].next=v[s];v[s]=ed+en;v[s]->e=e;v[s]->p=p;
        in[e]++;
        ex[en][0]=s;ex[en][1]=e;
    }
    
    int get(int p)
    {
        if (f[p]==p) return p;
        else return get(f[p]);
    }
    
    void init()
    {
        en=0;
        memset(v,0,sizeof(v));
        memset(in,0,sizeof(in));
    }
    
    void read()
    {
        scanf("%d",&n);
        for (int a=1;a<=n;a++)
        {
            int s,e;
            scanf("%d%d",&s,&e);
            add_edge(s,e,a);
        }
    }
    
    bool work1(int p,int d)
    {
        memset(use,false,sizeof(use));
        use[p]=true;
        head=tail=1;
        q[1]=p;
        for (;head<=tail;)
        {
            int p=q[head++];
            for (edge *e=v[p];e;e=e->next)
                if (e->p!=d)
                {
                    if (use[e->e]) return false;
                    use[e->e]=true;
                    pre[e->e][0]=p;
                    pre[e->e][1]=e->p;
                    q[++tail]=e->e;
                }
        }
        for (int a=1;a<=n;a++)
            if (!use[a]) return false;
        return true;
    }
    
    int work2(int p)
    {
        memset(use,false,sizeof(use));
        use[p]=true;
        head=tail=1;
        q[1]=p;
        for (;head<=tail;)
        {
            int p=q[head++];
            for (edge *e=v[p];e;e=e->next)
            {
                if (use[e->e]) return e->p;
                use[e->e]=true;
                q[++tail]=e->e;
            }
        }
        return -1;
    }
    
    int work()
    {
        for (int a=1;a<=n;a++)
            if (in[a]==2)
            {
                int p1=-1,p2=-1;
                for (int b=1;b<=n;b++)
                    if (ex[b][1]==a)
                    {
                        if (p1==-1) p1=b;
                        else p2=b;
                    }
                if (p1>p2) swap(p1,p2);
                for (int b=1;b<=n;b++)
                    if (in[b]==0)
                    {
                        if (work1(b,p2)) return p2;
                        else return p1;
                    }
            }
        for (int a=1;a<=n;a++)
            if (in[a]==0) return work2(a);
        for (int a=1;a<=n;a++)
            f[a]=a;
        for (int a=1;a<=n;a++)
        {
            int f1=get(ex[a][0]);
            int f2=get(ex[a][1]);
            if (f1==f2)
            {
                work1(ex[a][1],a);
                int p=ex[a][1],ans=a;
                while (p!=ex[a][1])
                {
                    ans=max(ans,pre[p][1]);
                    p=pre[p][0];
                }
                return ans;
            }
            f[f1]=f2;
        }
        return -1;
    }
    
    int main()
    {
        init();
        read();
        int p=work();
        printf("%d
    ",p);
    
        return 0;
    }

    T3

    想用 dp 骗 60% 的数据来着,却成了我这三个题的最高分??

    原谅我没看懂 zhx 老师的状态设置(他好像是直接设答案,不好想),我还是讲讲我的吧qwq:

    状态设置:

    dp [ i ][ j ]:当 Alice(下面称为 ' A ')杯内的茶剩下 i 吨, Bob(下面称为 ' B ')杯内的茶剩下 j 吨的概率是多少;

    边界设置:

    一开始的时候 A 和 B 都没有倒茶,所以 dp [ n ][ n ] = 1.0;

    状态转移方程:

    A 每次会等概率的倒出 k,2k,3k,4k 吨的茶,相应的 B 就会倒出 3k,2k,k,0 吨的茶,所以我们去枚举一下 A 倒出多少吨茶就好了:

    dp [ i-x ][ j - ( 4k - x ) ] += dp [ i ][ j ] * 0.25;  (i 倒出之后还剩下 i-x,j 倒出之后还剩下 j - ( 4k - x ))

        for(int i=n;i>0;i--)
            for(int j=n;j>0;j--)
                for(int p=k;p<=4*k;p+=k)
                        dp[max(i-p,0)][max(j-4*k+p,0)]+=(double)dp[i][j]*0.25;   //注意如果杯里的茶不够p的话,剩下的是0而不是一个负数

    答案:

    这时候就要考验大家的语文水平了:

    wrong answer = ( ans1 + ans2 ) / 2;

    right answer = ans1 + ( ans2 / 2 );

    虽然我一开始觉得是第一种,但是通过分析样例发现原来是第二种,真是坏啊qwq

    先想第一种 “ A 先把茶倒光的情况 ”:

    我们设最后一次 A 倒了 x 吨的茶,那么 B 倒了 4k-x 吨的茶;

    既然 A 一次就倒完了,并且 B 倒了之后还有剩余,那么也就是说倒之前 A 的杯子里的茶水是不超过 x 吨的,B 的杯子里的茶水是超过了 4k-x 吨的; 

    那么我们把这些所有情况加起来就好了:

        for(int p=k;p<=4*k;p+=k)    //A先把茶倒完的情况:枚举A最后一次倒了多少多少 
            for(int i=1;i<=p;i++)
                for(int j=n;j>4*k-p;j--)  //枚举B最后一次倒之前倒了多少 
                    ans1+=dp[i][j]*0.25;

    然后想第二种 “ A 和 B 同时把茶倒光的情况 ”:

    也就是说,最后一次倒茶时,A 的杯子内的茶水是不超过 x 吨的,同时 B 的杯子内的茶水是不超过 4k-x 吨的:

        for(int p=k;p<=4*k;p+=k)    //同时倒完的情况:枚举A最后一次倒了多少 
            for(int i=1;i<=p;i++)
                for(int j=1;j<=4*k-p;j++)
                    ans2+=dp[i][j]*0.25;

    然后输出 ans1 + ans2 / 2 就好了;

    但是。。。这个题 n,k <= 1e9,你的算法是 O ( n2 ) 的 ε=(´ο`*))) 唉;

    貌似 k 没用?

    考虑到其实 n=10,k=2 和 n=5,k=1 是一种情况,它们的最后答案是一样的;

    假如 n = 10, k = 5 和 n = 2 , k = 1 是一样的!

    那么我们就可以做一个转化:

    n => n / k (上取整),k => 1;

    那么现在的问题就变成:给你一个 n,问概率。

    打表大法好啊!而且这个题就可以打表做qwq 。

    两种方法打表:

    1. 爆搜,而且可以加上记忆化;

    状态设置:f [ i ][ j ] :

    f [ 0 ][ i ] = 1.0;

    f [ i ][ 0 ] = 0.0;

    f [ 0 ][ 0 ] = 0.5;

    考虑怎么转移:

    f [ i ][ j ] = 0.25 * ( f [ i-4 ][ j ] + f [ i-3 ][ j-1 ] + f [ i-2 ][ j-2 ] + f [ i-1 ][ j-3 ] );

    然后我们可以发现,当 n 超过了 235 的时候(n/k 后的新 n),答案就是 1 了 。

    所以我们再加一个特判,就满分喽~

    我的 lj 代码:

    #include<iostream>
    #include<cstdio>
    #include<cstring>
    #include<cmath>
    #include<queue>
    using namespace std;
    int read()
    {
        char ch=getchar();
        int a=0,x=1;
        while(ch<'0'||ch>'9')
        {
            if(ch=='-') x=-x;
            ch=getchar();
        }
        while(ch>='0'&&ch<='9')
        {
            a=(a<<1)+(a<<3)+(ch-'0');
            ch=getchar();
        }
        return a*x;
    }
    int n,k;
    double dp[4001][4001];
    double ans1,ans2;
    int main()
    {
        //freopen("tea.in","r",stdin);
        //freopen("tea.out","w",stdout);    
        n=read();k=read();
        if(n%k!=0) n=n/k+1;
        else n=n/k;
        k=1;
        if(n>300)
        {
            printf("1.000000");
            return 0;
        }
        dp[n][n]=1.0;
        for(int i=n;i>0;i--)
            for(int j=n;j>0;j--)
                for(int p=k;p<=4*k;p+=k)
                    {
                        //printf("dp[%d][%d]:%lf
    ",i,j,dp[i][j]);
                        dp[max(i-p,0)][max(j-4*k+p,0)]+=(double)dp[i][j]*0.25;
                        //printf("dp[%d][%d]:%lf
    ",max(i-p,0),max(j-4*k+p,0),dp[max(i-p,0)][max(j-4*k+p,0)]);
                    }
        for(int p=k;p<=4*k;p+=k)    //A先把茶倒完的情况:枚举A最后一次倒了多少多少 
            for(int i=1;i<=p;i++)
                for(int j=n;j>4*k-p;j--)  //枚举B最后一次倒之前倒了多少 
                    ans1+=dp[i][j]*0.25;
        for(int p=k;p<=4*k;p+=k)    //同时倒完的情况:枚举A最后一次倒了多少 
            for(int i=1;i<=p;i++)
                for(int j=1;j<=4*k-p;j++)
                    ans2+=dp[i][j]*0.25;
        //printf("%lf %lf
    ",ans1,ans2);
        printf("%.6lf",ans1+ans2*0.5);
        return 0;
    }

    标程超短代码:

    #include<cstdio>
    #include<cstdlib>
    #include<cstring>
    
    using namespace std;
    
    const int maxn=201;
    
    int n,k;
    
    double f[maxn][maxn];
    
    bool g[maxn][maxn];
    
    double dfs(int n,int m)
    {
        if (n<=0 && m<=0) return 0.5;
        if (n<=0) return 1.0;
        if (m<=0) return 0.0;
        if (g[n][m]) return f[n][m];
        g[n][m]=true;
        for (int a=1;a<=4;a++)
            f[n][m]+=0.25*dfs(n-a,m-4+a);
        return f[n][m];
    }
    
    double work(int n,int k)
    {
        if (n/k>=maxn) return 1.0;
        return dfs((n/k)+(n%k?1:0),(n/k)+(n%k?1:0));
    }
    
    int main()
    {
        scanf("%d%d",&n,&k);
        printf("%.6lf
    ",work(n,k));
    
        return 0;
    }

    图论题选讲 

    Problem -1 是什么操作? 

    先假设没有障碍的情况:

    因为在同一行或同一列的两个物体能互相看见,也就是说一个物体会用掉一行和一列;

    如果我们从第 i 行连向了第 j 列,那么就说明 (i,j)这个位置可以放上物体;

    我们放上一个障碍物,就可以看做把那一行和列分成了两个部分;

    最后答案:我们最多能匹配多少对,就是我们最多能放多少个物体;

    一遍匈牙利算法或网络流 Dinic 算法跑最大匹配就好了;

    考虑到一开始的两个 1 既不能在同一行,也不能在同一列;

    所以我们只需要判断是否有 n 个 1 都不在同一行或同一列上;

    这就类似于上一个题啊qwq;

    第 i 行连向第 j 列表示(i,j)这个位置的 1 可以选;

    最后我们只需要看看最大匹配数是否是 n 就好了;

    如果我们找不到的话,就说明无解! 

    2-SAT 问题:

    有 n 个变量,每一个变量都是 bool 类型的,除了这 n 个变量以外,我们还有 m 个关系表达式,关系表达式差不多是这样的:

    x1 & x2 = false(注意每个表达式只会有两个变量)

    问给出 m 个关系表达式后,能否给这 n 个变量找出一个赋值的方法,使得满足所有的表达式; 

    核心问题只需要考虑有没有解;

    对于每个变量都只有两种取值:0 / 1,那么对于每个点,我们把每个变量看成 0点和 1点;

    假如我们有一个关系:x1 & x2 = false;

    说明 x1 和 x2 中一定有一个为 false;

    那么我们可以从 x1 的 true 连向 x2 的 false,从 x2 的 true 连向 x1 的 false;

    要注意只有关系明确的时候才能建边;

    解释一下:

    如果 x1 的值为 true ,那么如果我们 x2 的值再为 true 的话,就不满足 “ x1 & x2 = false ” 这个式子了,所以如果 x1 为 true 的话是能明确推出 x2 为 false 的;

    我们可以从 x2 的 flase 向 x1 的 true 连边吗?这样也能满足关系式;

    显然不能,因为你如果 x2 为 false 的话,x为 true 或 false 是都可以的,这不是明确的关系,我们不能建边;

    所以一般的建边方式为:

    若 xa = p 或 xb = q 中至少有一个满足,那么我们建两条边:

    xa (¬p) -> xb (q)

    xb (¬q) -> xa (p) 

     

    如果一个变量必须等于 true,那么我们从这个点的 false 连向这个点的 true; 

    表示我们从 false 走也会走到 true,也就是说只能等于 true;

    怎么判断是否有解?

    假如说我们从一个点的 true 走到了它自己的 false,那么就说明这个点只能是 false;假如我们从一个点的 false 走到了它自己的 true,那么久说明这个点只能是 true;以上两种情况是有解的 。

    无解的情况就是:某一个变量的 false 能走到 true,从 true 也能走到 false,也就是说,某一个变量的两个取值在同一个强连通分量内的话,就说明无解。

    所有的 2-SAT 问题都是这么做;

    更详细版及代码实现 

    最大化最小值问题,二分喽~

    二分爆炸半径的最小值 r,看 r 是否合法;

    既然最小值是 r 了,我们不妨直接让所有的炸弹的爆炸范围都是 r;

    我们看到每一组都有两种取值,自然对应得就是 2-SAT 了;

    建图:

    假如我们第一组的第一个炸弹能扎到第二组的第一个炸弹,那么就说明这两个炸弹不能同时选,所以我们从第一组的第一个炸弹向第二组的第二个炸弹连一条边;反之,我们从第一组的第二个炸弹向第二组的第一个炸弹连边;(注意连有向边)

    每次 n2 暴力建边,然后 tarjan 判断是否合法就好了; 

    合法就向更大的 r 去枚举,直至不合法 ,此时的 r 就是答案了。

    给你一个 n * m 的棋盘,上面有些格子是黑色的,有些格子是白色的,还有一些障碍(不能放骨牌),现在你有一种类似于 ' L ' 型的骨牌,两端是白色的,中间是黑色的,要求将骨牌放在棋盘上,使得黑色部分放在黑色格子上,白色部分放在白色格子上,每个格子只能被覆盖一次(只能放一个骨牌),问能否能将整个棋盘覆盖满?

    我们考虑将黑点拆点,使得一个黑格子对于一个白格子;

    但是有一个问题:我们可能搞出来的不是 ' L ' 型;

    所以我们将白格子分为上下白格子和左右白格子;

    这样的话,一个黑格子对应一个上下白格子,一个黑格子对应一个左右白格子,合起来的话就能保证它是一个 ' L ' 型的了;

    对于更大的数据,只能用 2-SAT 做了:

    看到这道题只是判断有没有解,那么自然就想到了 2-SAT 。

    我们可以为每个黑点造两个变量:左右变量和上下变量,每个变量有两种取值,这样就变成了 2-sat 问题 。

    点建好了,边怎么建?

    如果两个黑格子的曼哈顿距离不超过 2,它们之间就有可能会冲突:

    假设我们有两个黑格子的分布是:左上和右下,那么它们的建边关系就是:

    实际也就 6 种情况,我们分别根据它们的位置关系建边就可以了:

    然后我们跑一遍 tarjan 判断其是否有解;

    总时间复杂度 O ( nm ); 

    我们依然可以用 2-SAT 做;

    我们考虑每个提案到底是通过还是否决;

    我们先考虑每个人投一票的情况:

    假如一个人对了一号提案投了反对票,由于要有一半以上的提案和投票情况相符,所以这个提案一定否决,我们从一号提案的通过连向否决,表示一号提案一定被否决:

    发现投两票和投一票的情况一样,也是需要所投的两个提案必须都和投票情况相符;

    考虑投三票的情况:假设一个人投了三、四、五号提案的通过,假如三号提案被否决了,由于至少要有一半以上的提案符合投票情况,所以剩下的四、五号提案是一定被通过的,所以我们可以从三号提案的否决分别连向四、五号提案的通过;同理我们再从四号提案的否决连向三、五号提案的通过,从五号提案的否决连向三、四号提案的通过:

    发现投四票的情况也是和投三票的情况一样,一个不满足剩下的三个都必须满足;

    答案:

    如果一个提案能从通过走到否决,那么说明这个提案一定会被否决;如果一个提案能从否决走到通过,那么说明这个提案一定会被通过;如果既能从通过走到否决,也能从否决走到通过,那么说明这个提案不一定被通过或者否决;

    由于树是个二分图,所以我们可以从树的角度去考虑; 把一张图变成一棵树有两种方法:

    1.生成树;

    2.缩点;

    一张图如果不是二分图,就说明有基环存在,所以我们要把所有的基环都干掉,所以我们所需要干掉的这条边就是所有基环的交集的那条边;

    那么怎么求所有基环的交集?

    我们先在这个图上随便求一棵生成树,然后我们求完生成树后,所有的边分成了两类:树边和非树边;

    如果没有不在树上的边,就说明这个图是二分图了;如果有一条不在树上的边,连了两个颜色一样的点,形成了一个基环,我们需要破坏掉基环;

    如果我们只能找到一条非树边,那就说明这一张图里面就一个基环,那么我们删掉这个基环上的任意一条边都可以使这个图变成二分图;

    如果说存在两条及以上的基环,我们并不能通过删除一条非树边来搞掉所有基环,此时我们要删的就是一条树边;

    如果一条非树边形成了一个基环,我们就将这个基环上的所有边的权值+1;这么瞎搞一通以后,如果我们发现有一条树边的权值等于基环的个数,也就是说这条树边被所有的基环包含在内,所以这一条边就是答案了;

    至于+1的操作,我们可以使用树上差分或者树链剖分来实现;

    看着 A 国人少条件少,先欺负分析 A 国:

    化简一下 A 国交朋友的条件:

    x1 ^ x2 %2 = 1;

    保证两个友善值异或后是奇数,也就是说两个数表示成二进制后最右边的那一位不同,也就是说两个人的友善值一奇一偶;

    那么最后的朋友圈中 A 国就只有 2 个人(超过了2人的话肯定有两个人的奇偶性相同,那么就不会是朋友了);

    所以现在的问题就是找 B 国最大的朋友圈;

    给你一张图让你求最大团是 NPC 问题,那么这个图不是随便给你的,一定是含有某个神奇的性质的;

    我们来分析 B 国交朋友的条件:

    第一个条件:x^ x2 % 2 = 0;

    与 A 国的分析情况相似,也就是说友善值同奇偶的人可以互相交朋友;

    第二个条件:x1 | x2 在二进制表示上有奇数个1;

    这个条件看起来有点奇怪,但可以确定的是这个条件使得在 B 国一奇一偶的人也可以交朋友,那么总体来说 B 国的情况就是这个亚子:

    看这个亚子有点像二分图呐,但是我们知道二分图两侧的点之间是没有连边的呀,但是这个图却两两连了边(没画全,其实是不想画了,意思意思就好),然后我们发现这个图的补图是一个二分图!

    二分图的补图: 

    反图的最大独立集就是原图的最大团;

    我们要在补图中找到一个最大的独立集,使得原图求出来的是最大团;

    每有一个匹配,我们所能够选的点就少一个,所以最后的答案就是:n+m - 最大匹配;

    每种颜色的格子最多两个?神奇的数字 2!

    假设我们考虑每种颜色的格子只有 1 个的话,那么每种颜色的格子要去的方向是固定的;

    那么我们肯定能通过 “ 行操作 + 列操作 + 行操作 ” 或 “ 列操作 + 行操作 + 列操作 ” 来完成;

    那么最后的答案一定不超过:min(ca+2cb ,cb + 2ca);

    如果只有一个系列的操作,我们直接通过暴力去求;

    如果需要通过两个系列去解决呢? 

    如果我们发现同一行的两个格子要到同一列去,或者同一列的两个格子要去同一行去,那么我们就不能省去第三步;

    但是原题每种颜色都至少有 2 个呀?

    我们可以 dfs 每个格子去哪里,但是这样的话时间复杂度为 O ( 245000 );

    假设一种颜色的两个初始位置 a,b ,在目标状态移动到了 A,B;

    那么只有两种取值:

    1. a -> A,b -> B;

    2. a -> B,b -> A; 

    如果 a 和 c 在同一行,A 和 C 在同一列,那么就说明我们不能通过两部操作来完成;

    如果 a 去 A , b 去 B,那么 c 只能去 D,同样 d 去 c;

    如果 a 去 B , b 去 A,那么 c 只能去 C,同样 d 去 D;

  • 相关阅读:
    ansible使用
    git undo last commit
    metadata简介
    tinyint(4),tinyint(80)有什么区别
    php 打印debug日志
    tinycore Network card configuration during exec bootlocal.sh
    Windows使用CMD命令查看进程和终止进程
    @NotEmpty,@NotNull和@NotBlank的区别
    spring boot 程序启动缓慢的问题(二)
    LocalDateTime的一些用法
  • 原文地址:https://www.cnblogs.com/xcg123/p/11620026.html
Copyright © 2020-2023  润新知