NOI 2006 题解

来源:互联网 发布:网络主播吸睛术 编辑:程序博客网 时间:2024/06/07 07:13

网络收费

(传送门)

题意

一颗满二叉树上所有叶子节点都是用户,网络收费实行配对收费的方式,对于没两个子节点需找他们最近公共祖先然后观察nA,nB的关系来判断收费方式,最后求满足一些列要求的最小收费。具体的题目描述比较繁琐,自己看一下题目就不在赘述了。

分析

这道题显然是一个树上的动态规划问题。因此,首先来看DP的必要条件:

原题可等价为,假设一对付费节点i和j的最近公共祖先为p,如果p的nA<nB,称p为A付费节点,否则为B付费节点。对于i和p,如果i和p的节点性质相同,则需要一倍的付费,j同理。所以,i和j的付费可以单独分开考虑了。安排每个叶节点的方案,使它到其所有祖先需付费之和最小。

考虑一个叶节点i到每个祖先p需要付费多少,取决于i相对于p的另一棵子树中所有节点j与i的流量F[i][j]之和。Cost[i][k]为叶节点i到其第k个祖先需要的付费,可以求出每两个叶节点i和j的最近公共祖先p,令Cost[i][p]和Cost[j][p]的值增加F[i][j]。

dp[i][j][state]表示以第i个节点为根的子树中分配j个A节点,从根节点到i的每个祖先的状态集合为state的最小费用(state用二进制表示状态)

如果一个节点nA<nB,则标记该节点状态为A付费节点。state+now表示加上当前节点状态,则状态转移方程:

dp[i][j][state] = min{ dp[i.left][a][state+now] + dp[i.right][j-a][state+now] }

对于边界状态即i为叶节点,计算方法是i的所有父节点中与i状态相同的节点state的Cost[i][state]之和,如果i与原先付费方式不同,还要加上C[i]。

这样一定是要爆空间的,所以我们考虑一下优化:仔细观察发现,后两维是有浪费的。假设叶节点为第0层,根节点为第N层,那么对于第k层的节点,它能控制的叶节点的个数最多为2^state,于是j的取值范围 就是0..2^state,一共2^state + 1种可能。它的祖先一共有N-state个,每个祖先状态可能有两种,于是k的取值一共有2^(N-state)种可能。由此计算,j和state的取值一共有(2^state + 1) * (2^(N-state)) = 2^N + 2^(N-state) <=2^(N+1),所以把后两维状态数压缩到2^(N+1)

具体方法是可以把两个二进制数连接到一起来表示一种状态。

代码

#include <bits/stdc++.h>using namespace std;const int MAXN=10+1,MAXM=1000+50,MAXP=MAXM*2;const int INF=0x3f3f3f3f;struct REC{    int r[MAXP],lim;    void set(int a,int b,int v)    {        r[a*lim+b]=v;    }    int get(int a,int b)    {        return r[a*lim+b];    }} record[MAXP];int n,m;int ancestor[MAXM][MAXN],cost[MAXM][MAXN];int convert[MAXM],flow[MAXM][MAXM];bool type[MAXM];int cal(int a,int na,int S){    int i,Ca=0,Cb=0;    for (i=1;i<=n;i++,S>>=1)    {        if(S&1) Ca+=cost[a][i];        else Cb+=cost[a][i];    }    if(na==1) return Ca+(type[a]?convert[a]:0);    else return Cb+(type[a]?0:convert[a]);}int DP(int i,int j,int k,int height){    int rs=record[i].get(j,k);    if(rs==0)    {        if(height)        {            rs=INF;            int a;            bool mark=j<((1<<height)-j);            int tk=(k<<1)+mark;            int ls=1<<(height-1);            if((a=j-ls)<0) a=0;            for(;a<=j&&a<=ls;a++)            {                int temp = DP(i<<1,a,tk,height-1);                temp += DP((i<<1)+1,j-a,tk,height-1);                rs=min(rs,temp);            }        }        else rs=cal(i-m+1,j,k);        record[i].set(j,k,rs);    }    return rs;}void work(){    for(int i=1;i<=m;i++)    {        int k=i+m-1;        for(int j=1;j<=n;j++)        {            k>>=1;            ancestor[i][j]=k;        }    }    for(int k=1;k<=n+1;k++)        for(int p=1<<(k-1);p<=(1<<k)-1;p++)        {            record[p].lim=1<<(k-1);            memset(record[p].r,0,sizeof(record[p].r));        }        for(int i=1;i<=m;i++)        for(int j=i+1;j<=m;j++)            for(int k=1;k<=n;k++)                if(ancestor[i][k] == ancestor[j][k])                {                    cost[i][k] += flow[i][j];                    cost[j][k] += flow[i][j];                    break;                }    int ans=INF;    for(int i=0;i<=m;i++)        ans=min(ans,DP(1,i,0,n));    printf("%d\n",ans);}int main(){    cin>>n;    m=1<<n;    for(int i=1;i<=m;i++)        scanf("%d",&type[i]);    for(int i=1;i<=m;i++)        scanf("%d",&convert[i]);    for(int i=1;i<=m-1;i++)        for(int j=i+1;j<=m;j++)        {            scanf("%d",&flow[i][j]);            flow[j][i]=flow[i][j];        }        work();        return 0;}

千年虫

(传送门)

题意

在一个图形的两边加上方块使它变成一个梳子状,并保证梳子的凹凸数是奇数,求出最小的代价。

分析

这道题题干比较烦人,是一道基于贪心的动态规划加上优化的题目,理解透彻题目的意思的话想出方程又不是很难。据说这是HNOI的一道名叫“梳子”的题目的变式。(事实证明多做题好处真多,但关键得做完还能记住。。)

因为左右互不干涉,所以可以分别dp求解。定义dp[i][j][s]表示前i行长度为j变成梳子状态为s(0凹1凸)的最小代价,转移如下:dp[i][j][s]=min{ dp[i-1][j-1][s],dp[i-1][k][1-s] }(k<j,s=1;k>j,s=0)

这样复杂度为O(n^3),预处理可以降到O(n^2),这样大概只能过一半的数据,考虑进一步优化。可以证明每行i的j的最优值只会在所有的[now[p],now[p]+2](|p-i|<=2)中产生,用它来优化j的枚举边界,j的有限个状态可以让复杂度变到O(n)。

代码

#include <bits/stdc++.h>using namespace std;const int MAXN=400000+10;const int INF=0x3f3f3f3f;int n,ans,l[MAXN],now[MAXN],p[2],dp[2][20][2],q[2][MAXN];void work(){    memset(dp,0x3f,sizeof(dp));    p[1]=0;    for(int j=1;j<4;j++)         for(int k=now[j];k<now[j]+3;k++)            if(k>=now[1]) q[1][++p[1]]=k;    for(int i=1;i<=p[1];i++)        dp[1][i][0]=q[1][i]-now[1];    int ths=1,pst=0;    for(int i=2;i<n+1;i++)    {        ths^=1;pst^=1;p[ths]=0;        int a=(i-2<1)?1:i-2,b=(n<i+2)?n:i+2;        for(int j=a;j<b+1;j++)             for(int k=now[j];k<now[j]+3;k++)                if(k>=now[i])                    q[ths][++p[ths]]=k;        for(int j=1;j<p[ths]+1;j++)        {            dp[ths][j][1]=dp[ths][j][0]=INF;            for(int k=1;k<p[pst]+1;k++)            {                if(q[pst][k]>q[ths][j])                    dp[ths][j][0]=min(dp[ths][j][0],dp[pst][k][1]);                else                {                    if(q[pst][k]<q[ths][j])                        dp[ths][j][1]=min(dp[ths][j][1],dp[pst][k][0]);                    else                    {                        dp[ths][j][0]=min(dp[ths][j][0],dp[pst][k][0]);                        dp[ths][j][1]=min(dp[ths][j][1],dp[pst][k][1]);                    }                }            }            dp[ths][j][0] += q[ths][j]-now[i],dp[ths][j][1] += q[ths][j]-now[i];        }    }    int temp=INF;    for(int i=1;i<p[ths]+1;i++)        temp=min(temp,dp[ths][i][0]);    ans+=temp;}int main(){    cin>>n;    for(int i=1;i<n+1;i++)        scanf("%d%d",&l[i],&now[i]);    work();    for(int i=1;i<n+1;i++)        now[i]=MAXN-l[i];    work();    cout<<ans<<endl;    return 0;}

最大获利

(传送门)

题意

n个可以当做中转站的地方,有建立成本Pi,m个用户群,使用Ai,Bi通讯会让公司获利Ci,求建立中转站让净获利最大.

分析

显而易见的网络流问题,难点在如何建图。

若a,b质检有一条收益为c的边,则新建一个点权为c,分别向a,b连边,a,b的点权为他们的花费,这样能转化为最大权封闭子图,S向正权点连容量为权值的边,负权点向T连容量为权值绝对值的边,可以证明一个方案和一个割一一对应。在此推荐胡伯涛的论文,值得一看。在dinic求最大流时可加它的两个优化:一是dfs中当容量为空及时把它的层次d[]改为-1,则再不会考虑此点

二是每一次bfs建图之后,可以一直dfs增广直到dfs返回值为0(即无法增广)

这样优化之后,快的飞起

代码

#include <bits/stdc++.h>using namespace std;#define t n+m+1const int MAXN=55000+5;const int INF=0x3f3f3f3f; struct Edge{    int to,cap;};vector <Edge> edges ;vector <int> G[MAXN];int cur[MAXN],d[MAXN];bool vis[MAXN];queue <int> q;int n,m,sum=0;void addEdge(int from,int to,int cap){    G[from].push_back(edges.size());    G[to].push_back(edges.size()+1);    edges.push_back((Edge){to,cap});    edges.push_back((Edge){from,0});} bool bfs(){    memset(d,0,sizeof(d));    memset(vis,0,sizeof(vis));    vis[0]=1;    q.push(0);    while(!q.empty())    {        int u=q.front();q.pop();        for(int i=0;i<G[u].size();i++)        {            Edge e=edges[G[u][i]];            if(e.cap&&!vis[e.to])            {                vis[e.to]=1;                d[e.to]=d[u]+1;                q.push(e.to);            }        }    }    return vis[t];} int dfs(int u,int a){    if(u==t||a==0) return a;    int flow=0,f;    for(int& i=cur[u];i<G[u].size();i++)    {        Edge &e=edges[G[u][i]];        if(d[u]+1==d[e.to]&&(f=dfs(e.to,min(a,e.cap)))>0)        {            e.cap-=f;            edges[G[u][i]^1].cap+=f;            flow+=f;            a-=f;            if(a==0) break;        }    }    if(flow) return flow;    d[u]=-1;    return 0;} int dinic(){    int flow=0;    while(bfs())    {        memset(cur,0,sizeof(cur));        flow+=dfs(0,INF);    }    return flow;} int main(){    scanf("%d%d",&n,&m);    for(int i=1;i<=n;i++)     {        int P;        scanf("%d",&P);        addEdge(i,n+m+1,P);    }    for(int i=1;i<=m;i++)    {        int A,B,C;        scanf("%d%d%d",&A,&B,&C);        addEdge(i+n,A,INF);        addEdge(i+n,B,INF);        addEdge(0,i+n,C);        sum+=C;    }    printf("%d",sum-dinic());    return 0;}

神奇的口袋

(传送门)

题意

给定一个游戏,最后求概率什么的,自己看一看题吧,反正上面有传送门。

分析

这是一道模拟题,但是由于数据实在太大变成了一道数学题,证明出来之后只要写一个高精度乘法和高精快速幂就完事了。

性质一:对于x[1],x[2],x[3],……,x[n],平移到1,2,3,……,n是等价的.

证明:

对于相邻的i,n,若x[i]<k<x[j],那第k步取颜色y[j]的概率是a[y[j]] / tot.

然后考虑k+1步取颜色y[j]的概率:

第k步取y[j],p1 = a[y[j]]/tot * (a[y[j]]+d)/(tot+d);

k步不取y[j],p2 = (tot-a[y[j]])/tot * a[y[j]]/(tot+d);

然后p1+p2化简得到a[y[j]] / tot.

那么第k+1步等价第k步,则以此递推,x[j]步等价第k步.证毕.

性质二:颜色y[i]出现的先后与结果无关。

证明:

对于y[i]与y[j],x[i]与x[j]平移后相邻.

y[i]=y[j],一定是等价的。

y[i]!=y[j] .第x[i]次取出y[i]的概率为p1=a[y[i]]/tot,第x[j]次取出y[j]的概率为p2=a[y[j]]/(tot+d).

若交换.则x[i]次取出y[j]的概率为p3=a[y[j]]/tot,第x[j]次取出y[i]的概率为p4=a[y[i]]/(tot+d).

显然p1*p2=p3*p4,所以最后的结果不变.

有了这两个性质以后,这就是一道模拟题,每一次按它的步骤操作累乘值就可以了。用高精度乘法。分子分母约分,可以写高精度除法,取巧的方法是打一个20000以内的质数表,然后每一次就不乘,把元素全部分解质因数记录每个质数出现了多少次,最后分子分母的质数次数相减至0,再高精乘低精。

代码

#include <bits/stdc++.h>using namespace std;const int MAXN=20000+5;const int MAXNUM=10000;int num[1005],A[MAXNUM],B[MAXNUM],cnt=0,sum=0;int flag[MAXN],prime[MAXNUM];struct bignum{    int len,a[MAXNUM];      bignum(){  len=1;  memset(a,0,sizeof(a));}       void print()    {        printf("%d",a[len]);        for(int i=len-1;i;i--)            printf("%04d",a[i]);    }}fz,fm;bignum Mul(bignum a,bignum b){    bignum c;  c.len=a.len+b.len+1;    for(int i=1;i<=a.len;i++)        for(int j=1;j<=b.len;j++)            c.a[i+j-1]+=a.a[i]*b.a[j];    for(int i=1;i<c.len;i++)        if(c.a[i]>=MAXNUM)        {            c.a[i+1]+=c.a[i]/MAXNUM;            c.a[i]%=MAXNUM;        }    while(c.a[c.len]>=MAXNUM)    {        c.len++,c.a[c.len]+=c.a[c.len-1]/MAXNUM;        c.a[c.len-1]%=MAXNUM;    }    while(c.a[c.len]==0&&c.len>1) c.len--;    return c;}bignum bigpow(int b,int p){    bignum ans,bb;    ans.len=bb.len=ans.a[1]=1;      bb.a[1]=b;    while(p)    {        if(p&1)            ans=Mul(ans,bb);        p>>=1;        bb=Mul(bb,bb);    }    return ans;}void init(){    for(int i=2;i<=20000;i++)    {        if(!flag[i]) prime[cnt++]=i;        for(int j=0;j<cnt&&prime[j]*i<=20000;j++)        {            flag[i*prime[j]]=1;            if(i%prime[j]==0) break;        }    }}void pushA(int k){    for(int i=0;k>1&&i<cnt&&prime[i]<=k;i++)        while(k%prime[i]==0)        {            A[i]++;            k/=prime[i];        }}void pushB(int k){    for(int i=0;k>1&&i<cnt&&prime[i]<=k;i++)        while(k%prime[i]==0)        {            B[i]++;            k/=prime[i];        }}int main(){    int n,m,d,x,y;    init();    scanf("%d%d%d",&n,&m,&d);    for(int i=1;i<=n;i++)    {        scanf("%d",&num[i]);        sum+=num[i];    }    for(int i=1;i<=m;i++)    {        scanf("%d%d",&x,&y);        if(num[y]==0)        {            printf("0/1\n");            return 0;        }        pushA(num[y]);        pushB(sum);        num[y]+=d;        sum+=d;    }    for(int i=0;i<cnt;i++)        if(A[i]&&B[i])        {            if(A[i]>B[i])            {                A[i]-=B[i];                B[i]=0;            }            else            {                B[i]-=A[i];                A[i]=0;            }        }    fz.a[1]=fm.a[1]=1;    for(int i=0;i<cnt;i++)        if(A[i]) fz=Mul(fz,bigpow(prime[i],A[i]));    for(int i=0;i<cnt;i++)        if(B[i]) fm=Mul(fm,bigpow(prime[i],B[i]));    fz.print();    cout<<'/';    fm.print();    cout<<endl;    return 0;}


0 0
原创粉丝点击