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; }
相关文章推荐
- Visual Studio Xaml编辑器不能识别引入的外来库,程序却能成功运行
- JTable的常见用法
- jQuery 获取、设置表单元素的值
- java Swing实现右键复制粘贴
- latex中如何引用公式
- 广州本田整车销售管理系统技术分析(三)
- RabbitMQ 入门指南(Java)
- Linux学习笔记(一)
- 浅谈Java工具类CommonUtils的使用
- 互联网思维学习分享会
- HackerRank - "Sherlock and MiniMax"
- 网页上播放音频、视频Mp3,Mp4
- Selenium webdriver 常见问题
- Selenium webdriver 常见问题
- 编码转换 Native / UTF-8 / Unicode
- Java NIO 12=====Java NIO与IO
- 百度地图API学习总结
- Js 操作 Cookies
- 为什么删除我博客?
- Y430p下win8、ubuntu双系统