• 2017国庆广州班(我能写出来的)题目总结


    啊,好久没写博客了,主要原因是学校的内部训练……不过做过的题目还是总结一下比较好。这里先写2017国庆广州班我能写出来的题吧……

    Day1T1 平衡 balance
    题目大意:k根火柴放在一列n个火柴盒中,每次操作可以将一根火柴从一个火柴盒移到相邻的火柴盒,求至少多少次后可以使所有火柴盒中火柴数量相等。保证kn倍数。
    做法:原题啊,水的不行,直接暴力水过……
    以下是本人代码:

    #include <cstdio>
    #include <cstdlib>
    #include <cstring>
    #include <iostream>
    #include <algorithm>
    #define ll long long
    using namespace std;
    ll n,k=0,a[50010],c[50010],ans=0;
    
    int main()
    {
        scanf("%lld",&n);
        for(int i=1;i<=n;i++)
        {
            scanf("%lld",&a[i]);
            k+=a[i];
        }
        k/=n;
    
        c[0]=0;
        for(int i=1;i<n;i++)
        {
            c[i]=k-a[i]+c[i-1];
            ans+=abs(c[i]);
        }
        printf("%lld",ans);
    
        return 0;
    }

    Day1T2 道路 road
    题目大意:一个n×m的矩阵,每个格子有一个高度h和分数v,每一次可以从一个格子走到另一个相邻的,高度不超过这个格子的格子,并获得这个格子的分数,然后这个格子的分数会等概率变为不大于这个分数的某个非负整数。求从任意一个格子开始走,能得到的最大的期望分数和。
    做法:本题需要用到期望+BFS/DFS+DAG最长路。
    考虑每一个高度相同的连通块。可以证明,如果该连通块包含2个或以上的格子,那么在这个连通块中能得到的期望分数为连通块内分数和×2,特别地,只包含一个格子时,期望分数就是这个格子的分数。如何证明呢?想到将每一个格子能获得的期望分数加起来就可以知道整个连通块的期望分数,设f(i)为一个分数为i的格子能获得的期望分数(不限定走过的次数的前提下),可以得到状态转移方程:
    f(i)=1i+1f(i)+1i+1f(i1)+...+1i+1f(0)+i
    其中边界条件为f(0)=0
    我们可以用数学归纳法证明f(i)=2i,证明如下:
    首先对于i=0,显然正确。
    对于任意i>0,假设对于任意0k<if(k)=2k成立,那么:
    ii+1f(i)=i+1i+1i1k=0f(k)=i+2i+1i(i1)2=i+i(i1)i+1=2i2i+1
    两边同除ii+1,即得f(i)=2i,因此结论成立。
    那么我们用BFS或DFS把高度相同的连通块缩起来,再从高度高的块向低的连边,就构成一个DAG,按拓扑序跑个DP求最长路即可。
    以下是本人代码:

    #include <cstdio>
    #include <cstdlib>
    #include <cstring>
    #include <iostream>
    #include <algorithm>
    #define ll long long
    using namespace std;
    int n,m,tot=0,h[1010][1010],belong[1010][1010],in[1000010]={0},siz[1000010]={0};
    int tt=0,first[1000010]={0},q[1000010],head=1,tail=0;
    ll v[1010][1010],sum[1000010]={0},f[1000010]={0},ans=0;
    bool vis[1010][1010]={0};
    struct edge {int v,next;} e[2000010];
    
    void dfs(int x,int y,int hi)
    {
        vis[x][y]=1;
        belong[x][y]=tot;
        siz[tot]++,sum[tot]+=v[x][y];
        if (x>1&&h[x-1][y]==hi&&!vis[x-1][y]) dfs(x-1,y,hi);
        if (y>1&&h[x][y-1]==hi&&!vis[x][y-1]) dfs(x,y-1,hi);
        if (x<n&&h[x+1][y]==hi&&!vis[x+1][y]) dfs(x+1,y,hi);
        if (y<m&&h[x][y+1]==hi&&!vis[x][y+1]) dfs(x,y+1,hi);
    }
    
    void insert(int x,int y)
    {
        e[++tt].v=y;
        e[tt].next=first[x];
        first[x]=tt;
        in[y]++;
    }
    
    int main()
    {
        scanf("%d%d",&n,&m);
        for(int i=1;i<=n;i++)
            for(int j=1;j<=m;j++)
                scanf("%d",&h[i][j]);
        for(int i=1;i<=n;i++)
            for(int j=1;j<=m;j++)
                scanf("%lld",&v[i][j]);
    
        for(int i=1;i<=n;i++)
            for(int j=1;j<=m;j++)
                if (!vis[i][j]) ++tot,dfs(i,j,h[i][j]);
        for(int i=1;i<=n;i++)
            for(int j=1;j<=m;j++)
            {
                if (i>1&&h[i][j]>h[i-1][j]) insert(belong[i][j],belong[i-1][j]);
                if (j>1&&h[i][j]>h[i][j-1]) insert(belong[i][j],belong[i][j-1]);
                if (i<n&&h[i][j]>h[i+1][j]) insert(belong[i][j],belong[i+1][j]);
                if (j<m&&h[i][j]>h[i][j+1]) insert(belong[i][j],belong[i][j+1]);
            }
    
        for(int i=1;i<=tot;i++)
        {
            if (siz[i]>1) sum[i]*=2;
            f[i]=0;
            if (!in[i]) q[++tail]=i;
        }
    
        while(head<=tail)
        {
            int v=q[head];
            f[v]+=sum[v];
            ans=max(ans,f[v]);
            for(int i=first[v];i;i=e[i].next)
            {
                in[e[i].v]--;
                f[e[i].v]=max(f[e[i].v],f[v]);
                if (!in[e[i].v]) q[++tail]=e[i].v;
            }
            head++;
        }
        printf("%lld",ans);
    
        return 0;
    }

    Day1T3 小豪的花园 flower
    题目大意:维护一棵树,点有非负点权,维护以下操作:单点修改权值,对一棵子树中所有点取模(模数可能不同),询问路径上的点权和。
    做法:本题需要用到树链剖分。
    虽然我们一眼就能看出这是一道树链剖分的题目,但是没做过区间取模的同学可能会对这个操作比较迷惑。事实上,我们可以证明每个数在不被修改的情况下,最多被有效取模的次数是log()的级别。所谓有效取模,就是模数不超过这个数时才算有效取模。怎么证明呢?设模数为m,原数为n,分两种情况:
    mn/2时,nmodmmn/2,明显少了一半。
    m>n/2时,nmodm=nm<n/2,也明显少了一半。
    这样每次有效取模都至少少了一半,那么有效取模次数当然就是log级别的了。
    所以我们对于线段树内每个区间维护一个最大值,如果取模时这个区间最大值小于模数,那么这个区间就不用管,否则暴力向下取模即可。
    然而还有单点修改,其实并不用担心,每个数被取模的次数仍然不超过log(2),所以这个方法可以通过此题。
    以下是本人代码:

    #include <cstdio>
    #include <cstdlib>
    #include <cstring>
    #include <iostream>
    #include <algorithm>
    #define ll long long
    using namespace std;
    int n,m,tot=0,first[100010]={0},in[100010],out[100010],q[100010];
    int dep[100010],fa[100010],siz[100010],son[100010]={0},top[100010];
    ll a[100010]={0},sum[400010],mx[400010];
    struct edge {int v,next;} e[200010];
    
    void insert(int a,int b)
    {
        e[++tot].v=b;
        e[tot].next=first[a];
        first[a]=tot;
    }
    
    void dfs1(int v)
    {
        siz[v]=1;
        for(int i=first[v];i;i=e[i].next)
            if (e[i].v!=fa[v])
            {
                fa[e[i].v]=v;
                dep[e[i].v]=dep[v]+1;
                dfs1(e[i].v);
                siz[v]+=siz[e[i].v];
                if (siz[e[i].v]>siz[son[v]]) son[v]=e[i].v;
            }
    }
    
    void dfs2(int v,int tp)
    {
        in[v]=++tot;q[tot]=v;top[v]=tp;
        if (son[v]) dfs2(son[v],tp);
        for(int i=first[v];i;i=e[i].next)
            if (e[i].v!=fa[v]&&e[i].v!=son[v]) dfs2(e[i].v,e[i].v);
        out[v]=tot;
    }
    
    void pushup(int no)
    {
        mx[no]=max(mx[no<<1],mx[no<<1|1]);
        sum[no]=sum[no<<1]+sum[no<<1|1];
    }
    
    void buildtree(int no,int l,int r)
    {
        if (l==r)
        {
            sum[no]=mx[no]=a[q[l]];
            return;
        }
        int mid=(l+r)>>1;
        buildtree(no<<1,l,mid);
        buildtree(no<<1|1,mid+1,r);
        pushup(no);
    }
    
    void modify(int no,int l,int r,int x,ll y)
    {
        if (l==r)
        {
            sum[no]=mx[no]=y;
            return;
        }
        int mid=(l+r)>>1;
        if (x<=mid) modify(no<<1,l,mid,x,y);
        else modify(no<<1|1,mid+1,r,x,y);
        pushup(no);
    }
    
    void Mod(int no,int l,int r,int s,int t,ll k)
    {
        if (l==r)
        {
            sum[no]%=k,mx[no]%=k;
            return;
        }
        int mid=(l+r)>>1;
        if (s<=mid&&mx[no<<1]>=k) Mod(no<<1,l,mid,s,t,k);
        if (t>mid&&mx[no<<1|1]>=k) Mod(no<<1|1,mid+1,r,s,t,k);
        pushup(no);
    }
    
    ll Sum(int no,int l,int r,int s,int t)
    {
        if (l>=s&&r<=t) return sum[no];
        int mid=(l+r)>>1;
        ll S=0;
        if (s<=mid) S+=Sum(no<<1,l,mid,s,t);
        if (t>mid) S+=Sum(no<<1|1,mid+1,r,s,t);
        return S;
    }
    
    ll query(int x,int y)
    {
        ll ans=0;
        while(top[x]!=top[y])
        {
            if (dep[top[x]]<dep[top[y]]) swap(x,y);
            ans+=Sum(1,1,n,in[top[x]],in[x]);
            x=fa[top[x]];
        }
        if (dep[x]<dep[y]) swap(x,y);
        ans+=Sum(1,1,n,in[y],in[x]);
        return ans;
    }
    
    int main()
    {
        scanf("%d%d",&n,&m);
        for(int i=1;i<n;i++)
        {
            int a,b;
            scanf("%d%d",&a,&b);
            insert(a,b),insert(b,a);
        }
        dep[1]=fa[1]=siz[0]=0;
        dfs1(1);
        tot=0;dfs2(1,1);
        for(int i=1;i<=n;i++) scanf("%lld",&a[i]);
    
        buildtree(1,1,n);
        for(int i=1;i<=m;i++)
        {
            int op;
            scanf("%d",&op);
            if (op==1)
            {
                int a;ll b;
                scanf("%d%lld",&a,&b);
                Mod(1,1,n,in[a],out[a],b);
            }
            if (op==2)
            {
                int a;ll b;
                scanf("%d%lld",&a,&b);
                modify(1,1,n,in[a],b);
            }
            if (op==3)
            {
                int a,b;
                scanf("%d%d",&a,&b);
                printf("%lld
    ",query(a,b));
            }
        }
    
        return 0;
    }

    Day2T1 数学课 number
    题目大意:给出n个数字,每次选择其中两个擦掉,并写上它们的乘积+1,求最后剩下的数的最小值。
    做法:本题需要用到贪心。
    贪心策略为每次选择最大的两个数字擦除,可以证明这样得到的最后的数字是最小的。
    以下是本人代码:

    #include <cstdio>
    #include <cstdlib>
    #include <cstring>
    #include <iostream>
    #include <algorithm>
    #define ll long long
    #define mod 1000000007
    using namespace std;
    int n;
    ll a[210];
    
    bool cmp(ll a,ll b)
    {
        return a<b;
    }
    
    int main()
    {
        scanf("%d",&n);
        for(int i=1;i<=n;i++)
            scanf("%lld",&a[i]);
        sort(a+1,a+n+1,cmp);
    
        for(int i=n;i>=2;i--) a[i-1]=(a[i]*a[i-1]+1)%mod;
        printf("%lld",a[1]);
    
        return 0;
    }

    Day2T2 遗传病 disease
    题目大意:维护一个集合,每次操作往集合中加入或从集合中删除一个数,每次操作后询问集合中互质的数对个数。数字不大于500000
    做法:本题需要用到莫比乌斯反演定理+线性筛求积性函数。
    f(i)gcd(a,b)=i的数对(a,b)的个数。要直接求f(i)比较困难,那么我们设g(i)gcd(a,b)i的倍数的数对个数,根据莫比乌斯反演定理,由g(i)=i|df(d),可得f(i)=i|dμ(d/i)g(d),那么当i=1时,f(1)=dμ(d)g(d)。再设s(i)为集合内i的倍数的个数,那么显然g(i)=s(i)(s(i)1)/2,因此我们每增加或删除一个数时,暴力枚举它的每一个因数,然后修改g(d)的值,即可算出新的答案。在处理之前先线性筛处理出μ(i)的值,即可达到O(nm)的算法复杂度,其中m为最大的数字。
    以下是本人代码:

    #include <cstdio>
    #include <cstdlib>
    #include <cstring>
    #include <iostream>
    #include <algorithm>
    #include <cmath>
    #define ll long long
    using namespace std;
    int n,m,prime[500010],a[500010],t=0;
    ll miu[500010],s[500010],ans=0;
    bool non_prime[500010]={0},chosen[100010]={0};
    
    void calc_miu()
    {
        for(int i=1;i<=500000;i++) miu[i]=1;
        for(int i=2;i<=500000;i++)
        {
            if (!non_prime[i])
            {
                prime[++t]=i;
                miu[i]=-1;
            }
            for(int j=1;i*prime[j]<=500000&&j<=t;j++)
            {
                non_prime[i*prime[j]]=1;
                if (i%prime[j]==0)
                {
                    miu[i*prime[j]]=0;
                    break;
                }
                miu[i*prime[j]]=-miu[i];
            }
        }
    }
    
    int main()
    {
        scanf("%d%d",&n,&m);
        for(int i=1;i<=n;i++)
            scanf("%d",&a[i]);
        calc_miu();
        for(int i=1;i<=m;i++)
        {
            int x;
            scanf("%d",&x);
            if (!chosen[x])
            {
                chosen[x]=1;
                for(int i=1;i<=sqrt(a[x]);i++)
                    if (a[x]%i==0)
                    {
                        s[i]++;
                        ans+=miu[i]*(s[i]-1);
                        if (a[x]/i!=i)
                        {
                            s[a[x]/i]++;
                            ans+=miu[a[x]/i]*(s[a[x]/i]-1);
                        }
                    }
            }
            else
            {
                chosen[x]=0;
                for(int i=1;i<=sqrt(a[x]);i++)
                    if (a[x]%i==0)
                    {
                        s[i]--;
                        ans-=miu[i]*s[i];
                        if (a[x]/i!=i)
                        {
                            s[a[x]/i]--;
                            ans-=miu[a[x]/i]*s[a[x]/i];
                        }
                    }
            }
            printf("%lld
    ",ans);
        }
    
        return 0;
    }

    Day2T3 瓜分土地 B
    题目大意:将一个数列分成三个非空连续段,令每段内数字和分别为a,b,c,第二段中元素的个数为nx是一个给定的常数,则一种划分方案的合理值为max(a,b,c)min(a,b,c)+x×n,求最小的合理值。
    做法:本题需要用到线段树+离散化+二分+分类讨论。
    归根到底,一种划分方案只有两个实质上的变量:第一段的右端点和第二段的左端点。我们考虑枚举第一段的右端点,那么我们就需要求出这种情况下划分方案合理值的最小值。
    观察目标函数,我们可以对a,b,c的大小关系进行分类讨论,显然有6种不同的大小关系。对于每种关系,目标函数都不相同,限制条件也不相同,但因为我们已经枚举了第一段,所以a是固定的,而a+b+c也是固定的,所以我们可以从每种情况的限制条件得出这种条件下c的取值范围。又因为a为常数,那么目标函数肯定能表示成一个只和cn有关的式子,所以我们先预处理出数列的后缀和,并离散化成一棵权值线段树,在每个节点存下要求的式子的值,那么我们就可以实现在某一个c的取值范围内寻找最优的取值了。对于这个范围,二分查找出它在离散化后覆盖到的区间即可。
    不过还有一点要注意,第三段左端点必须在第一段左端点右边那个元素的右边,所以我们每次计算完第一段右端点为某点的情况,要把第三段右端点为该点右边相邻节点的情况去除,即把存储的式子值都改成inf,这样就不会得出非法的答案了。
    以下是本人代码:

    #include <cstdio>
    #include <cstdlib>
    #include <cstring>
    #include <iostream>
    #include <algorithm>
    #define ll long long
    using namespace std;
    ll n,a[100010],x,rht[100010],sum,inf,ans,lft,mn[400010][6],now;
    bool flag;
    struct forsort
    {
        ll val,id;
    } f[100010];
    
    bool cmp(forsort a,forsort b)
    {
        return a.val<b.val;
    }
    
    void pushup(int no)
    {
        for(int i=0;i<6;i++)
            mn[no][i]=min(mn[no<<1][i],mn[no<<1|1][i]);
    }
    
    void buildtree(int no,ll l,ll r)
    {
        if (l==r)
        {
            mn[no][0]=-f[l].val-x*f[l].id;
            mn[no][1]=f[l].val-x*f[l].id;
            mn[no][2]=-2*f[l].val-x*f[l].id;
            mn[no][3]=-f[l].val-x*f[l].id;
            mn[no][4]=2*f[l].val-x*f[l].id;
            mn[no][5]=f[l].val-x*f[l].id;
            return;
        }
        ll mid=(l+r)>>1;
        buildtree(no<<1,l,mid);
        buildtree(no<<1|1,mid+1,r);
        pushup(no);
    }
    
    ll query(int no,ll l,ll r,ll s,ll t,int mode)
    {
        if (l>=s&&r<=t) return mn[no][mode];
        ll mid=(l+r)>>1,minn=inf;
        if (s<=mid) minn=min(minn,query(no<<1,l,mid,s,t,mode));
        if (t>mid) minn=min(minn,query(no<<1|1,mid+1,r,s,t,mode));
        return minn;
    }
    
    void solve(ll s,ll t,int mode)
    {
        ll l=0,r=n+1,sl,sr;
        flag=1;
        while(l<r)
        {
            ll mid=(l+r)>>1;
            if (f[mid].val<s) l=mid+1;
            else r=mid;
        }
        sl=l;l=0,r=n+1;
        while(l<r)
        {
            ll mid=(l+r)>>1;
            if (f[mid].val<=t) l=mid+1;
            else r=mid;
        }
        sr=l-1;
        if (sl>sr) {flag=0;return;}
        now=query(1,1,n,sl,sr,mode);
    }
    
    ll fl(ll x)
    {
        if (x>=0) return x/2;
        else return (x/2)-((abs(x)%2==0)?0:1);
    }
    
    int main()
    {
        inf=1000000000;inf*=inf;
    
        scanf("%lld",&n);
        for(ll i=1;i<=n;i++)
            scanf("%lld",&a[i]);
        scanf("%lld",&x);
    
        rht[0]=0;
        for(ll i=1;i<=n;i++)
            rht[i]=rht[i-1]+a[n-i+1];
        sum=rht[n];
        for(ll i=1;i<=n;i++) f[i].val=rht[i],f[i].id=i;
        sort(f+1,f+n+1,cmp);
        f[0].val=-inf-1;f[n+1].val=inf+1;
    
        buildtree(1,1,n);
        lft=0;
        ans=inf;
        for(ll i=1;i<=n;i++)
        {
            lft+=a[i];
            solve(sum-2*lft,fl(sum-lft),0);
            if (flag) ans=min(ans,x*(n-i)+lft+now);
            solve(fl(sum-lft+1),lft,1);
            if (flag) ans=min(ans,x*(n-i)+2*lft-sum+now);
            solve(-inf,min(sum-2*lft,lft),2);
            if (flag) ans=min(ans,x*(n-i)+sum-lft+now);
            solve(lft,fl(sum-lft),3);
            if (flag) ans=min(ans,x*(n-i)+sum-2*lft+now);
            solve(max(lft,sum-2*lft),inf,4);
            if (flag) ans=min(ans,x*(n-i)+lft-sum+now);
            solve(fl(sum-lft+1),sum-2*lft,5);
            if (flag) ans=min(ans,x*(n-i)-lft+now);
        }
        printf("%lld
    ",ans);
    
        return 0; 
    }

    Day3T1 树 bst
    题目大意:对一个1~n的排列构建一棵二叉搜索树,每次依次插入一个节点,从根节点开始依次比较,如果该节点值比根节点小,那么继续跟它的左儿子比较,否则跟它的右儿子比较。如果对应的儿子不存在,那么该点就成为对应的儿子。求在每次插入操作后,每个节点的深度之和(根节点深度为0)。
    做法:本题需要用到链表+二分(但我脑抽写了个线段树)。
    观察之后我们发现,一开始,插入每个点后它的深度都会是0,但之后每插入一个点,它在[1,n]区间上到左边最相邻的点和右边最相邻的点之间的区间,插入的代价会增加1,因此用线段树或链表+二分维护这种操作(但我很SB的写了个线段树)。
    以下是本人代码:

    #include <cstdio>
    #include <cstdlib>
    #include <cstring>
    #include <iostream>
    #include <algorithm>
    #define ll long long
    using namespace std;
    int n;
    bool vis[1200010];
    ll ans=0,s[300010]={0};
    
    void pushup(int no)
    {
        vis[no]=vis[no<<1]||vis[no<<1|1];
    }
    
    void buildtree(int no,int l,int r)
    {
        if (l==r)
        {
            if (l==0||l==n+1) vis[no]=1;
            else vis[no]=0;
            return;
        }
        int mid=(l+r)>>1;
        buildtree(no<<1,l,mid);
        buildtree(no<<1|1,mid+1,r);
        pushup(no);
    }
    
    int findleft(int no,int l,int r,int x)
    {
        if (r<x) return 0;
        if (l==r&&l>=x&&vis[no]) return l;
        if (l>=x&&!vis[no]) return 0;
        int mid=(l+r)>>1,find=0;
        if (x<=mid) find=findleft(no<<1,l,mid,x);
        if (!find) find=findleft(no<<1|1,mid+1,r,x);
        return find;
    }
    
    int findright(int no,int l,int r,int x)
    {
        if (l>x) return 0;
        if (l==r&&r<=x&&vis[no]) return l;
        if (r<=x&&!vis[no]) return 0;
        int mid=(l+r)>>1,find=0;
        if (x>mid) find=findright(no<<1|1,mid+1,r,x);
        if (!find) find=findright(no<<1,l,mid,x);
        return find;
    }
    
    void modify(int no,int l,int r,int x)
    {
        if (l==r) {vis[no]=1;return;}
        int mid=(l+r)>>1;
        if (x<=mid) modify(no<<1,l,mid,x);
        else modify(no<<1|1,mid+1,r,x);
        pushup(no);
    }
    
    int lowbit(int i)
    {
        return i&(-i);
    }
    
    ll sum(int i)
    {
        ll ss=0;
        for(;i>0;i-=lowbit(i))
            ss+=s[i];
        return ss;
    }
    
    void add(int i,ll x)
    {
        for(;i<=n+1;i+=lowbit(i))
            s[i]+=x;
    }
    
    int main()
    {
        scanf("%d",&n);
        buildtree(1,0,n+1);
        for(int i=1;i<=n;i++)
        {
            int x;
            scanf("%d",&x);
            ans+=sum(x);
            int l,r;
            l=findright(1,0,n+1,x);
            r=findleft(1,0,n+1,x);
            modify(1,0,n+1,x);
            if (l<=r)
            {
                if (l>0) add(l,1);
                else add(1,1);
                add(r,-1);
            }
            printf("%lld
    ",ans);
        }
    
        return 0;
    }

    Day3T2 赛 ctsc
    题目大意:n道题和m个选手,每道题有一个分值,有些题已经评测过了,每个选手每道题有一个状态,如果该题已经评测过,状态Y表示该选手该题得满分,状态N表示该选手该题得0分,如果该题未评测过,状态Y表示该选手提交了该题代码,状态N表示该选手未提交该题代码。得到全部结果后,选手会按总分从大到小排序,如果总分相同则按序号从小到大排序,按这样的顺序选出前s名选手,并从这s名选手中选出t名选手进入参赛队。问最后参赛队的t名选手有多少种组合方案。
    做法:本题需要用到组合数学+DP。
    这个题面看上去非常之复杂,复杂到无以复加,但是理性分析和观察(看完题解)后,我们发现,每个选手都有一个得分最大值和得分最小值,最大值就是该选手没评测的题目都过了的分数,最小值就是没评测的题目都没过的分数。按照选手的得分最大值从大到小排序,如果最大值相同按序号从小到大排序。令p(i)为第i位选手要是在得分前s名,前面i1名选手必须在得分前s名的选手个数,显然,如果前面有选手的得分最小值大于该选手的得分最大值,或者有选手的得分最小值等于该选手得分最大值并且序号比该选手小,那么这名选手就必须在得分前s名。这时候就体现出我们前面排序的意义了:保证选手后面不存在名次必须比他高的选手。
    求出p(i)之后,按照排好序的选手从前到后计算方案。考虑计算在前i个选手内选出最后的t人,且第i名选手必须要选的方案数。首先若p(i)>s1则肯定无解,否则前s人就是必选的p(i)人,选手i以及其余ip(i)1中所选择的sp(i)1人了。为什么一名选手前面的选手一定可选?想一想,我们是按得分最大值排序的,所以前面的选手一定可能名次在选手i之前。在选择最后的t人时,选择选手i,然后先从必须要选的p(i)人中选出k人,之后再从剩下的sp(i)1人里选出tk1人,那么k的取值范围为max(p(i)s+t,0)kmin(p(i),t1)。那么对于每个合法的k,方案数为Ckp(i)×Ctk1ip(i)1,预处理组合数,然后累加方案数即可。时间复杂度O(m2)
    以下是本人代码:

    #include <cstdio>
    #include <cstdlib>
    #include <cstring>
    #include <iostream>
    #include <algorithm>
    #define ll long long
    using namespace std;
    ll n,m,score[55],p[55],s,t;
    char ss[55];
    struct oier
    {
        ll minn,maxx,id;
    } f[55];
    
    bool cmp(oier a,oier b)
    {
        if (a.maxx!=b.maxx) return a.maxx>b.maxx;
        else return a.id<b.id;
    }
    
    ll calc_c(ll n,ll m)
    {
        if (m>n) return 0;
        if (m==0) return 1;
        if (n==0) return 0;
        if (m==n) return 1;
        ll ans=1;
        for(ll i=0;i<n-m;i++)
            ans=ans*(n-i)/(1+i);
        return ans;
    }
    
    int main()
    {
        scanf("%lld",&n); 
        for(ll i=1;i<=n;i++)
            scanf("%lld",&score[i]);
        scanf("%lld",&m);
        for(ll i=1;i<=m;i++)
        {
            scanf("%s",ss);
            f[i].maxx=f[i].minn=0;
            for(ll j=1;j<=n;j++)
            {
                if (score[j]>0&&ss[j-1]=='Y')
                {
                    f[i].maxx+=score[j];
                    f[i].minn+=score[j];
                }
                if (score[j]<0&&ss[j-1]=='Y')
                    f[i].maxx-=score[j];
            }
            f[i].id=i;
        }
        scanf("%lld%lld",&s,&t);
    
        sort(f+1,f+m+1,cmp);
        for(ll i=1;i<=m;i++)
        {
            p[i]=0;
            for(int j=1;j<i;j++)
                p[i]+=((f[j].minn>f[i].maxx)||((f[j].minn==f[i].maxx)&&(f[j].id<f[i].id)));
        }
        ll ans=0;
        for(ll i=t;i<=m;i++)
        {
            if (p[i]>s-1) break;
            ll k_min,k_max;
            k_min=max(p[i]-s+t,(ll)0);
            k_max=min(p[i],t-1);
            for(ll k=k_min;k<=k_max;k++)
                ans+=calc_c(p[i],k)*calc_c(i-p[i]-1,t-1-k);
        }
        printf("%lld",ans);
    
        return 0;
    }

    Day3T3 史 history
    题目大意:n个城市,每次一个操作,为以下几种中的一种:修建一条道路,询问两个城市在某次操作前是否连通。强制在线。
    做法:本题需要用到带权并查集(可持久化并查集的弱化?不会可持久化并查集)。
    首先我们知道,两点间是否连通取决于中间连的晚的边的最小时间,于是我们想到维护一个最小生成树和路径上的最大值。看到在线维护最小生成树,有些同学立马想到LCT之类的鬼畜做法,实际上这题没有这么难……
    首先,注意到道路只有修建没有拆除,并且时间是不断递增的,因此已经连通的两点间答案是不受新建道路影响的,而道路只有修建没有拆除,这就让我们产生一种猜想:能不能用并查集的变形来解决这一问题?答案是肯定的。
    标程中使用的并查集变形是这样做的:令f(i)为点i的父亲,g(i)为点i到父亲节点的边权(这里是添加的时间),h(i)为以点i为根的集合深度。然后我们就要实现并查集的“并”和“查”操作了。先说“并”操作,我们在点i和点j之间连一条权为t的边,那么我们先找到点i和点j所属的集合的根节点,如果相同就合并,否则就从h较小的点连一条边权为t的边向h较大的点。为什么要这样做呢?这样保证了两个性质:1.集合的深度不会太深(应该是logn级别吧……不会证);2.一个点父亲节点连出的父边比该点连出的父边要大。在满足这两个性质的同时还满足并查集的基本性质:连通性。
    满足这些性质有什么用呢?接下来“查”操作说明了一切。我们这里不单单是要查某点所属集合的根节点,而是要查某点在某时刻之前所属集合的根节点。这时候上面的性质2就保证了我们在向上搜的时候,搜到父边大于这个时刻时停止,最后的这个点就是该点在该时刻的父亲,而性质1保证这个过程不会太慢。
    这样一来,我们新建道路就是在合并两个集合,而询问就是询问某两个点在某一时刻祖先是否一致,这些我们都可以用上面的并查集变形来解决,于是这个问题就完美解决了,时间复杂度应该是渐近的O(nlogn)
    以下是本人代码:

    #include <cstdio>
    #include <cstdlib>
    #include <cstring>
    #include <iostream>
    #include <algorithm>
    using namespace std;
    int n,m,f[300010],g[300010],h[300010];
    char op[3];
    
    int find(int x,int t)
    {
        while(f[x]>=0&&g[x]<=t) x=f[x];
        return x;
    }
    
    void merge(int x,int y,int t)
    {
        x=find(x,t);
        y=find(y,t);
        if (x!=y)
        {
            if (h[x]>h[y]) swap(x,y);
            f[x]=y,g[x]=t;
            h[y]+=(h[x]==h[y]);
        }
    }
    
    int main()
    {
        freopen("history.in","r",stdin);
        freopen("history.out","w",stdout);
    
        scanf("%d%d",&n,&m);
        int c=0,ans=0,x,y,z,t=0;
        for(int i=0;i<n;i++) f[i]=-1,g[i]=0,h[i]=1;
        for(int i=1;i<=m;i++)
        {
            scanf("%s",op);
            if (op[0]=='K')
            {
                scanf("%d",&z);
                c=(c*ans+z)%n;ans=0;
            }
            else
            {
                scanf("%d%d",&x,&y);
                x=(c*ans+x)%n,y=(c*ans+y)%n;
                if (op[0]=='R') merge(x,y,++t);
                else
                {
                    scanf("%d",&z);
                    z=(c*ans+z)%n;
                    ans=(find(x,t)==find(y,t))==(find(x,t-z)==find(y,t-z));
                    printf("%c
    ",ans?'N':'Y');
                }
            }
        }
    
        return 0;
    }

    Day4T3 搞 gao
    题目大意:n个人之间的领导关系构成一棵树,现在要从里面选出若干个人,使得雇佣他们的总费用不超过D,并且还要选出一人作为领导者(不一定要雇佣该人),要求该领导者必须直接或间接领导所有选出的人,每个人i作为领导者有一个领导加成b(i),假设选出了k人,那么最后该方案的收益为k×b(i)。求最大的收益。
    做法:本题需要用到贪心+左偏树。
    这题跟APIO那道派遣(dispatching)很像啊……好像就是原题来着……
    我们从叶子节点开始考虑以每个点为领导者的最大收益。领导者确定之后,那选出的人肯定越多越好,那么我们对于每个点按费用存储一个大根堆,一开始每个点的堆内都只有该点。然后考虑某个节点的方案时,将该节点的堆与它所有儿子的堆合并起来,如果总费用超过D就弹出根,直到费用不超过D为止,此时堆中的点数是最大的。合并堆,维护堆内元素个数和元素和,都可以用左偏树来解决,具体的在这里不赘述了,看代码吧。时间复杂度为O(nlogn)
    以下是本人代码:

    #include <cstdio>
    #include <cstdlib>
    #include <cstring> 
    #include <iostream>
    #include <algorithm>
    #define ll long long
    using namespace std;
    int n,tot=0,first[100010]={0},ch[100010][2];
    ll c,f[100010],a[100010],b[100010],siz[100010],rt[100010],dis[100010];
    ll sum[100010],val[100010],ans=0;
    struct edge {int v,next;} e[100010];
    
    void insert(int a,int b)
    {
        e[++tot].v=b;
        e[tot].next=first[a];
        first[a]=tot;
    }
    
    int merge(int x,int y)
    {
        if (!x) return y;
        if (!y) return x;
        if (a[x]<a[y]) swap(x,y);
        ch[x][1]=merge(ch[x][1],y);
        if (dis[ch[x][0]]<dis[ch[x][1]]) swap(ch[x][0],ch[x][1]);
        int l=ch[x][0],r=ch[x][1];
        dis[x]=dis[r]+1;
        sum[x]=sum[l]+sum[r]+a[x];
        siz[x]=siz[l]+siz[r]+1;
        return x;
    }
    
    void delete_root(int v)
    {
        rt[v]=merge(ch[rt[v]][0],ch[rt[v]][1]);
    }
    
    void solve(int v)
    {
        siz[v]=1,rt[v]=v,ch[v][0]=ch[v][1]=0,dis[v]=-1,sum[v]=a[v];
        for(int i=first[v];i;i=e[i].next)
        {
            solve(e[i].v);
            rt[v]=merge(rt[v],rt[e[i].v]);
        }
        while(sum[rt[v]]>c)
        {
            delete_root(v);
        }
        ans=max(ans,siz[rt[v]]*b[v]);
    }
    
    int main()
    {
        scanf("%d%lld",&n,&c);
        for(int i=1;i<=n;i++)
        {
            scanf("%d%lld%lld",&f[i],&a[i],&b[i]);
            if (i>1) insert(f[i],i);
        }
    
        solve(1);
        printf("%lld",ans);
    
        return 0;
    }

    啊,所有6天18道题,我也暂时就只会做这10题了T_T,要是之后有新的会做的题可能会补充吧,不过全做完的话,在我的OI生涯中怕是不存在了,真的是弱啊……

  • 相关阅读:
    路由系统
    快速入门
    IIS7.0下 HTTP 错误 404.15
    关于:TypeConverter 无法从 System.String 转换
    mssql批量刷新多个表的数据
    aspxpivotgrid排序
    .net面试题
    AspxGridView在cell内显示颜色
    AspxGridView 表中的ASPxHyperLink不导出到excel
    C# 网络编程小计 20150202
  • 原文地址:https://www.cnblogs.com/Maxwei-wzj/p/9793575.html
Copyright © 2020-2023  润新知