2020牛客暑期多校第一场赛后感

结果挺惨的。

《关于我和队友啥都不会就来比赛结果被AK大佬虐得体无完肤这档事》

来来来,写题解了。

 

A. B-Suffix Array

题意:对于一个只有'a'和'b'的字符串t1t2...tk,定义函数B(t1t2...tk)=b1b2...bk,满足:如果存在下标j<i使得tj=ti,那么bi=min1≤j<i,tj=ti{i-j},否则bi=0。现在给一个字符串s1s2...sn,对它的n个后缀按B()函数的字典序递增排序,并输出顺序。1≤n≤105

 

如果知道后缀数组的话,会发现这题很像。后缀数组nlog2n或nlogn,复杂度是可行的。然而我们队一个人都不知道。

但是不同后缀的B()的前缀是不同的。一个很自然的思路是另设一个函数C(),使它与B()等效,且能用后缀数组解。

于是大佬们如此定义C():如果存在下标j>i使得tj=ti,那么bi=min1≤i<j,tj=ti{j-i},否则bi=*(关于*是什么,一会儿解释)。然后将各C()递减排序。

 

为什么C()与B()等效?现在开始证明:

我们的证明方法是这个形式:对于两个字符串s和t,如果B(s)>B(t),那么一定C(s)<C(t)。

位置

s,t(省略的字符相同)

B()

C()

备注

开头

aaaab...

aaab...

01110...大

0110...

111xy...

11xy...大

只要让x>1即可

中部

...aaab...

...aab...

...x11y...

...x1z...大

...11mn...大

...1mn...

若前面没有b,则只要让z,m>1即可

若前面有b,则一定符合

尾部

...aaab

...aab

...x11y

...x1z

...11mn

...1mn

若前面没有b,也就是y=z=0,则只要让m>1即可

若前面有b,也就是y>z>0,则一定符合

特殊

a

ba

0

00大

m大

nm

只要让m>n即可。

为了让a和ba的C()最大,这里*我设置的是:如果a后面没有a和b,那么为n+n;如果a后面有b无a,那么为n+n-1。也就是C(a)=2n,C(ba)=2n-1 2n。

 

代码奉上:

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;

const int N=100000+10;

int n, k;
char s[N];
int c[N], d[N], y[N];

bool cmp(int a, int b){
    if(c[a]!=c[b]) return c[a]<c[b];
    int a1, b1;
    if(a+k>n) a1=-1;
    else a1=c[a+k];
    if(b+k>n) b1=-1;
    else b1=c[b+k];
    return a1<b1;
}

void suffix(int c[], int n){
    for(k=1; k<=n; k<<=1){
        sort(d+1,d+n+1,cmp);
        y[d[1]]=1;
        for(int i=2; i<=n; ++i) y[d[i]]=y[d[i-1]]+cmp(d[i-1],d[i]);
        for(int i=1; i<=n; ++i) c[i]=y[i];
    }
}

int main(){
    while(~scanf("%d", &n)){
        scanf("%s", s+1);
        int last[2]={0,0};
        for(int i=n; i; --i){
            s[i]-='a';
            if(last[s[i]]==0){
                if(last[1-s[i]]==0) c[i]=n+n;
                else c[i]=n+n-1;
            }else c[i]=last[s[i]]-i;
            last[s[i]]=i;
        }
        for(int i=1; i<=n; ++i) d[i]=i;
        suffix(c, n);
        for(int i=n; i>=1; --i) printf("%d%c", d[i], " \n"[i==1]);
    }
    return 0;
}
A

 话说这个标题适合作为第二题。

 

B. Infinite Tree

题意:有一棵无限大的树,对于所有的u>1,u与u/mindiv(u)有边相连。mindiv(u)意思是u除了1之外的最小的因数。现在给出 m 和$w_1w_2...w_m$,求$\min_u \sum_{i=1}^{m}w_i\delta(u,i!)$,其中$\delta(u,v)$表示两个点u和v之间的边的条数。多组数据,$1\le m \le 10^5 , 0 \le w_i \le 10^4, \sum_m\le 10^6 $

 

这道题m!实在大得吓人,想必因数很多,因此这是一颗巨大的树。我们假设已经有了这样一课树,现在怎么求解呢?

以1为根节点,设w[u]表示在u这个子树内的w之和(不能用阶乘表示的数的w设为0),那么w[1]是所有的w之和。如果选u为答案所在节点,此时答案记为f(u),那么可以由u推导f(v),v是u的一个孩子。容易得出f(v)=f(u)+w[1]-2*w[v]。

如果w[1]-2*w[v]<0,那么v比u更优。又容易想到如果u不是能用阶乘表示的数,那么w[u]=0。这时如果u不是两个阶乘数的lca的话,它与父亲或孩子的w是一样的,因此u必然可以不被选为答案节点。

这样的点可以忽略掉,那么我们可以建立一棵虚树,这棵树最多2m个结点。

其实看到这么大的树应该自然就想到建一棵虚树,然后树上dp一下。

可能这有打马后炮的意味。当时见到这道题的时候根本不知道该用什么算法,尤其是我根本不知道虚树。

虚树的自我修养:依据dfs序加点,能求lca,知道新边的长度。

根据定义,n!自然包括(n-1)!的所有因数,因此,从1到m遍历就已经是符合dfs序了。

根节点1到其他点u的路径是由u的质因数从大到小排序得到的。lca(n!,(n+1)!)的深度等于n!中大于等于mindiv(n+1)的因数个数(同一个因数可以出现多次)。而这里知道lca的深度即可。

如果我们也统计了每个u!的深度,那么虚树中边的长度直接是两个点的深度相减,也容易求出。

 

总结一下,先以1为根节点建立一棵虚树,树上的点只有1到m的阶乘和它们的lca这最多2m个点,边长就是两个点之间实际的边数。然后以1为u求下结果,再dfs一遍求出每个点的结果。虚树建立及dp的复杂度只有O(m),但建立虚树所作的准备我是$O(mlog^2m)$。

大体思路就这样,细节很多,代码见下:

// code by cyh
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<vector>
#include<stack>
using namespace std;

const int M=100000+10;
typedef long long LL;

struct Edge{
    int to, next, w;
    Edge(int v=0, int n=0, int w=0):to(v),next(n),w(w){}
};

struct Graph{
    int h[M<<1], hn;
    Edge e[M<<2];
    
    void init(int n){
        for(int i=0; i<=n; ++i) h[i]=h[M+i]=0;
        hn=0;
    }
    
    void add(int u, int v, int w){
        e[++hn]=Edge(v,h[u],w); h[u]=hn;
        e[++hn]=Edge(u,h[v],w); h[v]=hn;
    }
};

int m;
int d[M<<1], size[M<<1];
int b[M];
LL w[M<<1];
LL ans;
vector<int> V[M];
stack<int> S;
Graph g;

int prime[M], pn;
bool not_prime[M];
void getprime(){
    not_prime[1]=true;
    for(int i=2; i<M; ++i){
        if(!not_prime[i]) prime[pn++]=i;
        for(int j=0; j<pn&&i*1ll*prime[j]<M; ++j){
            not_prime[i*prime[j]]=true;
            if(i%prime[j]==0) break;
        }
    }
}

int lowbit(int x){ return x&(-x); }

void change(int w, int x){
    for(; w<M; w+=lowbit(w)) b[w]+=x;
}

int query(int w){
    int res=0;
    for(; w; w-=lowbit(w)) res+=b[w];
    return res;
}

void init(){
    //prime
    getprime();
    //the prime factors of every number
    for(int i=0; i<pn; ++i)
        for(int j=prime[i]; j<M; j+=prime[i])
            V[j].push_back(prime[i]);
    //the number of every number's prime factors
    //cal d[]
    d[1]=0;
    for(int i=2, t, sz, cnt; i<M; ++i){
        sz=V[i].size(); t=i;
        d[i]=d[i-1];
        d[M+i]=query(M-V[i][sz-1]);
        for(int j=0, p; j<sz; ++j){
            p=V[i][j]; cnt=0;
            while(t%p==0){
                t/=p;
                cnt++;
            }
            d[i]+=cnt;
            change(M-V[i][j],cnt);
        }
    }
}

void build(int m){
    S.push(1);
    for(int i=2, x, y, l; i<=m; ++i){
        l=M+i;
        while(1){
            x=S.top();
            if(d[l]==d[x]){
                S.push(i);
                break;
            }
            S.pop();
            y=S.top();
            if(d[y]>d[l]){
                g.add(y,x,d[x]-d[y]);
            }else if(d[y]==d[l]){
                g.add(y,x,d[x]-d[y]);
                S.push(i);
                break;
            }else{
                g.add(l,x,d[x]-d[l]);
                S.push(l); S.push(i);
                break;
            }
        }
    }
    int x=S.top(), y; S.pop();
    while(!S.empty()){
        y=S.top(); S.pop();
        g.add(y,x,d[x]-d[y]);
        x=y;
    }
}

LL initTree(int u, int fa){
    LL res=0;
    size[u]=1;
    for(int p=g.h[u]; p; p=g.e[p].next){
        int v=g.e[p].to;
        if(v==fa) continue;
        res+=initTree(v,u);
        res+=w[v]*1ll*g.e[p].w;
        w[u]+=w[v];
        size[u]+=size[v];
    }
    return res;
}

void calAns(int u, int fa, LL res){
    ans=min(ans,res);
    for(int p=g.h[u]; p; p=g.e[p].next){
        int v=g.e[p].to;
        if(v==fa) continue;
        calAns(v,u,res+(w[1]-2ll*w[v])*g.e[p].w);
    }
}

int main(){
//    freopen("data.txt","r",stdin);
//    freopen("my.txt","w",stdout);
    init();
    while(~scanf("%d", &m)){
        for(int i=1; i<=m; ++i) scanf("%lld", w+i);
        build(m);
        ans=initTree(1,0);
        calAns(1,0,ans);
        printf("%lld\n", ans);
        g.init(m<<1);
        for(int i=0; i<=m; ++i) w[M+i]=0;
    }
    return 0;
}
B

 

F. Infinite String Comparision 

题意:给两个字符串s和t,比较s+s+...和t+t+...的大小,分别对应输出">" "=" "<"。|s|,|t|≤106

 

据说比较s+t和t+s就可以了。

更普通的做法是先判断gcd(len(s),len(t)),比较是否相等,然后普普通通地一个一个比较是">"还是"<",应该最多比较两遍就行了。

代码就不写了。

 

H. Minimum-cost Flow 

 题意:给一张n个点,m条边有边权的有向图,有q个询问,每个询问包含两个整数u,v,意为这个网络流每条边容量都为u/v,对每个询问输出从点1到点n运送1单位需要的最小费用,并用 "a/b" 形式表示,a,b为整数且互质。$2\le n \le 50, 1 \le m \le 100, q \le 10^5, 0 \le u_i \le v_i \le 10^9, v_i > 0, 1 \le 边权 \le 10^5$

 

几乎就是最小费用最大流了呵,只不过有q个询问,使得每次询问有了不同的容量。一个很重要的点是所有边容量相同。

一个最简单的最小费用最大流算法是这么做的:每次用bellman-ford找一条从源点到终点的费用最小的路,直到找不到,算法结束。

这道题也可以这样。先用这个算法求出最大流,并再这过程中每次找到一条路后记录下这一路的边权。也就是依次记录下了所有的路。

由于每次都是运输u/v,那么需要运输v/u(整数)次u/v,或v/u(下取整)次u/v+1次(v%u)/v。只需要上述算法有没有v/u(上取整)个路径就可以了。

复杂度$O(n^2m+qm)$

 

代码见下:

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<vector>
#include<queue>
using namespace std;
 
const int N=50+5, M=100+5, INF=1000000000;
typedef long long LL;
 
struct Edge{
    int from, to, cap, flow, cost;
    Edge(int u, int v, int c, int f, int w):from(u), to(v), cap(c), flow(f), cost(w){}
};
 
vector<int> res;
struct MCMF{
    int n, m;
    vector<Edge> edges;
    vector<int> G[N];
    int inq[N];
    int d[N];   //BellmanFord
    int p[N];   //上一条弧
    int a[N];   //可改进量
     
    void init(int n){
        this->n=n;
        for(int i=1; i<=n; ++i) G[i].clear();
        edges.clear();
    }
     
    void AddEdge(int from, int to, int cap, int cost){
        edges.push_back(Edge(from,to,cap,0,cost));
        edges.push_back(Edge(to,from,0,0,-cost));
        m=edges.size();
        G[from].push_back(m-2);
        G[to].push_back(m-1);
//      printf("%d->%d cap=%d cost=%d\n", from, to, cap, cost);
    }
     
    bool BellmanFord(int s, int t, int& flow, LL& cost){
        for(int i=1; i<=n; ++i) d[i]=INF;
        memset(inq,0,sizeof(inq));
        d[s]=0; inq[s]=1; p[s]=0; a[s]=INF;
         
        queue<int> Q;
        Q.push(s);
        while(!Q.empty()){
            int u=Q.front(); Q.pop();
            inq[u]=0;
            for(int i=0; i<G[u].size(); ++i){
                Edge& e=edges[G[u][i]];
                if(e.cap>e.flow && d[e.to]>d[u]+e.cost){
                    d[e.to]=d[u]+e.cost;
                    p[e.to]=G[u][i];
                    a[e.to]=min(a[u],e.cap-e.flow);
                    if(!inq[e.to]) { Q.push(e.to); inq[e.to]=1; }
                }
            }
        }
        if(d[t]==INF) return false;
        flow+=a[t];
        res.push_back(d[t]);
        cost+=(LL)d[t]*(LL)a[t];
        for(int u=t; u!=s; u=edges[p[u]].from){
            edges[p[u]].flow+=a[t];
            edges[p[u]^1].flow-=a[t];
        }
        return true;
    }
     
    int MincostMaxflow(int s, int t, LL& cost){
        int flow=0; cost=0;
        while(BellmanFord(s,t,flow,cost));// printf("flow=%d cost=%d\n", flow, cost);
        return flow;
    }
};
 
int n, m, q;
MCMF mcmf;
 
LL gcd(LL a, LL b){
    if(b==0) return a;
    return gcd(b,a%b);
}
 
int main(){
    while(~scanf("%d%d", &n, &m)){
        mcmf.init(n); res.clear();
        for(int i=1, u, v, w; i<=m; ++i){
            scanf("%d%d%d", &u, &v, &w);
            mcmf.AddEdge(u,v,1,w);
        }
        LL cost;
        LL f=mcmf.MincostMaxflow(1,n,cost);
        scanf("%d", &q);
        while(q--){
            LL u, v;
            scanf("%lld%lld", &u, &v);
            if(f*u<v){
                printf("NaN\n");
                continue;
            }
            LL cur=0, ans1, ans2, g;
            double ans=0;
            int i=0;
            while(1){
                if(cur+u<v){
                    ans+=res[i++];
                    cur+=u;
                }else{
                    ans1=u*ans+(v-cur)*res[i];
                    ans2=v;
                    g=gcd(ans1,ans2);
                    printf("%lld/%lld\n", ans1/g, ans2/g);
                    break;
                }
            }
        }
    }
}
H

 

I.1 or 2 

题意:给一张n个点,m条边的图,判断能否删去一些边使得每个点$i$的度为$d_i$。$1 \le d_i \le 2, 1 \le n \le 50, 1 \le m \le 100, 无自环,无重边$

 

据说有类似的题。 

看这道题的数据范围就知道一定是某个复杂度顶大的算法。这个算法就是一般图最大匹配,算法复杂度$O(n(nlogn+m))$。也不是很大哈 

首先拆点:如果一个点u的度数为2,那么就把它拆成两个点,多的那个点我们记作u2。

然后拆边:如果有一条连接u,v的边,就把这条边拆成2个点 u', v',然后连接 u-u', v-v',如果度数为2那就把u2和v2也对应连上;再连上u'-v'。

最后一般图最大匹配,如果是完美匹配就是"yes",否则就是"no"。复杂度$O((n+m)((n+m)log(n+m)+m))$,不到$10^6$吧。

 

奉上代码:

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<vector>
#include<queue>
using namespace std;
 
const int PN=50+5, PM=100+5;
const int N=PN+PM<<1;
 
struct CGM{
    vector<int> G[N];
    queue<int> q;
    int p[N], dfn[N], pre[N], match[N], vst[N];
    static int cnt, ans;
     
    void init(int n){
        for(int i=0; i<=n; ++i) G[i].clear();
        while(!q.empty()) q.pop();
        memset(p,0,sizeof(p));
        memset(dfn,0,sizeof(dfn));
        memset(pre,0,sizeof(pre));
        memset(match,0,sizeof(match));
        memset(vst,0,sizeof(vst));
        cnt=0; ans=0;
    }
    void add(int u, int v) { G[u].push_back(v); G[v].push_back(u); }   
    int find(int x) { return x==p[x]?x:p[x]=find(p[x]); }
    int lca(int u, int v){
        for(++cnt,u=find(u),v=find(v); dfn[u]!=cnt; ){
            dfn[u]=cnt;
            u=find(pre[match[u]]);
            if(v)swap(u,v);
        }
        return u;
    }
    void blossom(int x, int y, int w){
        while(find(x)!=w){
            pre[x]=y,y=match[x];
            if(vst[y]==2)vst[y]=1,q.push(y);
            if(find(x)==x)p[x]=w;
            if(find(y)==y)p[y]=w;
            x=pre[y];
        }
    }
    int aug(int s, int n){
        if((ans+1)*2>n) return 0;
        for(int i=1; i<=n; ++i) { p[i]=i; vst[i]=pre[i]=0; }
         
        while(!q.empty()) q.pop();
        q.push(s); vst[s]=1;
        while(!q.empty()){
            int u=q.front(); q.pop();
            for(int i=0, v, w; i<G[u].size(); ++i){
                if(find(u)==find(v=G[u][i])||vst[v]==2)continue;
                if(!vst[v]){
                    vst[v]=2; pre[v]=u;
                    if(!match[v]){
                        for(int x=v, lst; x; x=lst) { lst=match[pre[x]]; match[x]=pre[x]; match[pre[x]]=x; }
                        return 1;
                    }
                    vst[match[v]]=1; q.push(match[v]);
                }else{ w=lca(u,v); blossom(u,v,w); blossom(v,u,w); }
            }
        }
        return 0;
    }
     
    int CommonGraphMatch(int n){
        for(int i=n; i; --i) if(!match[i]) ans+=aug(i,n);
        return ans;
    }
};
int CGM::cnt=0, CGM::ans=0;
 
int n, m;
int o[PN];
CGM cgm;
 
int main(){
    while(~scanf("%d%d", &n, &m)){
        int tot=n; cgm.init(n+m<<1);
        for(int i=1, x; i<=n; ++i){
            scanf("%d", &x);
            o[i]=x==2?++tot:0;
        }
        for(int i=0, u, v, t; i<m; ++i){
            scanf("%d%d", &u, &v);
            cgm.add(u,tot+1); cgm.add(v,tot+2); cgm.add(tot+1,tot+2);
            if(o[u]) cgm.add(o[u],tot+1);
            if(o[v]) cgm.add(o[v],tot+2);
            tot+=2;
        }
//      int ans=cgm.CommonGraphMatch(tot);
//      for(int i=1; i<=tot; ++i) printf("%d%c", cgm.match[i], " \n"[i==tot]);
//      printf("tot=%d ans=%d\n", tot, ans);
//      puts((tot%2==0 && ans==tot/2)?"Yes":"No");
        puts((tot%2==0 && cgm.CommonGraphMatch(tot)==tot/2)?"Yes":"No");
    }
    return 0;
}
I

 

结语 

这就是目前我搞懂的所有题目,其他的由于智商和时间问题仍不会做,扔给队友吧

这场比赛,我们队就做出了2道,真是辣鸡得不行~

说到做不出来的原因,A题最主要是不知道后缀数组这个东西,其次是不知道那个结论,或是没有得出结论的那种思维;B题最主要是不知道虚树,其次是没有分析出答案的计算方法;H题主要是不会网络流;I题主要是没有把问题转换成二分图的能力,其次是不知道一般图匹配;J题主要是数学不好。

总结就是:1.没有分析问题的思维;2.不会相应模板;3.数学不好。

再总结:菜鸡一个

身为菜鸡,我很抱歉 ╮(╯▽╰)╭

2020.7.31

posted @ 2020-07-31 08:02  white514  阅读(128)  评论(0编辑  收藏  举报