NOI 2006 题解

网络收费

(传送门)

题意

一颗满二叉树上所有叶子节点都是用户,网络收费实行配对收费的方式,对于没两个子节点需找他们最近公共祖先然后观察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+1
const 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
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值