zoukankan      html  css  js  c++  java
  • 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;

  • 相关阅读:
    python模块--time模块
    python模块--如何相互调用自己写的模块
    Animating Views Using Scenes and Transitions
    fragment 切换
    android textview 设置text 字体
    android intent 5.1
    android EditView ime
    animation of android (4)
    animation of android (3)
    animation of android (2)
  • 原文地址:https://www.cnblogs.com/xcg123/p/11620026.html
Copyright © 2011-2022 走看看