您的位置:首页 > 其它

NOI 2006 题解

2015-06-01 08:31 232 查看

网络收费

(传送门)

题意

一颗满二叉树上所有叶子节点都是用户,网络收费实行配对收费的方式,对于没两个子节点需找他们最近公共祖先然后观察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
,平移到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;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: