2016.4.3NOI上较难的动规题目(仔细分析样例)--王老师讲课整理
1.NOI 191:钉子和小球
总时间限制:
- 1000ms
- 内存限制:
- 65536kB
- 描述
-
有一个三角形木板,竖直立放,上面钉着n(n+1)/2颗钉子,还有(n+1)个格子(当n=5时如图1)。每颗钉子和周围的钉子的距离都等于d,每个格子的宽度也都等于d,且除了最左端和最右端的格子外每个格子都正对着最下面一排钉子的间隙。
让一个直径略小于d的小球中心正对着最上面的钉子在板上自由滚落,小球每碰到一个钉子都可能落向左边或右边(概率各1/2),且球的中心还会正对着下一颗将要碰上的钉子。例如图2就是小球一条可能的路径。
我们知道小球落在第i个格子中的概率pi=,其中i为格子的编号,从左至右依次为0,1,...,n。
现在的问题是计算拔掉某些钉子后,小球落在编号为m的格子中的概率pm。假定最下面一排钉子不会被拔掉。例如图3是某些钉子被拔掉后小球一条可能的路径。 - 输入
- 第1行为整数n(2 <= n <= 50)和m(0 <= m <= n)。以下n行依次为木板上从上至下n行钉子的信息,每行中'*'表示钉子还在,'.'表示钉子被拔去,注意在这n行中空格符可能出现在任何位置。
- 输出
- 仅一行,是一个既约分数(0写成0/1),为小球落在编号为m的格子中的概pm。既约分数的定义:A/B是既约分数,当且仅当A、B为正整数且A和B没有大于1的公因子。
- 样例输入
-
5 2 * * . * * * * . * * * * * * *
- 样例输出
-
7/16
- 来源
- Noi 99
- 代码:
/*基本思路:总概率是1,遇到一个钉子,就把概率均分下一层的两个,遇到空,就把空点对应的概率降两层加到钉子上,注意别忘记赋值是0 技巧:把第一个点的概率赋值为1<<n,每次除2,到了底部再与1<<n约分就可以了*/ #include<iostream> #include<cstdio> using namespace std; #define N 61 long long dp[N][N]; int f[N][N]; int n,m,t=0; void input() { scanf("%d%d\n",&n,&m); for(int i=1;i<=n;++i) { int t=0; char p[N*N]; gets(p+1); for(int j=1;j<=2*n;++j)/*注意这里的2*n包括空格在内有2*n个,而不是n个*/ { if(p[j]=='.') { ++t; f[i][t]=1; } if(p[j]=='*') { ++t; f[i][t]=2; } } } dp[1][1]=(long long)1<<n; } void DP() { for(int i=1;i<=n;++i) for(int j=1;j<=i;++j) { if(f[i][j]==2) { dp[i+1][j]+=dp[i][j]/2; dp[i+1][j+1]+=dp[i][j]/2; } else { if(i+2<=n+1) dp[i+2][j+1]+=dp[i][j]; else dp[i+1][j+1]+=dp[i][j];/*为了防止最后一层没有钉子,而导致的概率落2层落超了界*/ dp[i][j]=0; } } if(dp[n+1][m+1]==0) { printf("0/1\n");/*概率是0的特判*/ return ; } long long int l=(long long)1<<n;/*用位运算符给long long赋值,前面必须用强制类型转换*/ while(dp[n+1][m+1]%2==0&&l%2==0)/*约分*/ { dp[n+1][m+1]/=2; l/=2; } cout<<dp[n+1][m+1]<<"/"<<l<<endl; } int main() { input(); DP(); return 0; }
2.NOI 193:棋盘分割
- 总时间限制:
- 1000ms
- 内存限制:
- 65536kB
- 描述
- 将一个8*8的棋盘进行如下分割:将原棋盘割下一块矩形棋盘并使剩下部分也是矩形,再将剩下的部分继续如此分割,这样割了(n-1)次后,连同最后剩下的矩形棋盘共有n块矩形棋盘。(每次切割都只能沿着棋盘格子的边进行)
原棋盘上每一格有一个分值,一块矩形棋盘的总分为其所含各格分值之和。现在需要把棋盘按上述规则分割成n块矩形棋盘,并使各矩形棋盘总分的均方差最小。
均方差,其中平均值,xi为第i块矩形棋盘的总分。
请编程对给出的棋盘及n,求出O'的最小值。 - 输入
- 第1行为一个整数n(1 < n < 15)。
第2行至第9行每行为8个小于100的非负整数,表示棋盘上相应格子的分值。每行相邻两数之间用一个空格分隔。 - 输出
- 仅一个数,为O'(四舍五入精确到小数点后三位)。
- 样例输入
-
3 1 1 1 1 1 1 1 3 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 0 3
- 样例输出
-
1.633
- 来源
- Noi 99
问题分析:
题目中的关键点:如何合法的分割?
可以看出把一个矩形切成两个小矩形之后,其中一个不再切,另一个进行切割,又会遇到相同的问题,这就是子结构了,我们只要在所有划分中枚举找到最优值就可以了。
一.状态的确定:
考虑设计几个参数可以完成状态的转移,虽然说只要矩阵和n确定,那么最优值就确定了,但是这个定义明显不适于状态的转移。
矩形的表示:借助excel中的方法,用左上角的点和右下角的点表示矩形,这起码是4维数组了,还要有一个参数,表示分了几块,这是题目中的要求,
那么这就是5个参数才能完成状态的转移。
这里涉及的数学知识:标准差公式的变形
二:状态转移方程
f[i][x1][y1][x2][y2]表示x1,y1,x2,y2矩形切割成i分所能得到的最小标准差
边界:i==1的状态可以直接得到。
三.代码实现:
/**/ #include<cstdio> #include<cstring> #include<iostream> #include<cmath> using namespace std; #define N 9 #define K 16 double s[N][N]; int a; double f[K][N][N][N][N]; int k; int line[N][N]; double sum=0; void input() { scanf("%d",&k); for(int i=1;i<=8;++i) for(int j=1;j<=8;++j) { scanf("%d",&a); line[i][j]=line[i][j-1]+a; sum+=a; } for(int i=1;i<=8;++i) for(int j=1;j<=8;++j) for(int l=1;l<=i;++l) s[i][j]+=line[l][j]; sum/=k; } void DP() { memset(f,127,sizeof(f)); for(int x1=1;x1<=8;++x1) for(int y1=1;y1<=8;++y1) for(int x2=x1;x2<=8;++x2) for(int y2=y1;y2<=8;++y2)/*f[1]求法的技巧:设定一个s[i][j]数组储存1,1,i,j矩形中的数值之和,注意不是s[x2][y2]-s[x1-1][y1-1],这不是一个矩形*/ f[1][x1][y1][x2][y2]=(s[x2][y2]+s[x1-1][y1-1]-s[x2][y1-1]-s[x1-1][y2])*(s[x2][y2]+s[x1-1][y1-1]-s[x2][y1-1]-s[x1-1][y2]); for(int i=2;i<=k;++i) for(int x1=1;x1<=8;++x1) for(int y1=1;y1<=8;++y1) for(int x2=x1;x2<=8;++x2)// for(int y2=y1;y2<=8;++y2)// { for(int x=x1;x<x2;++x)/*切成两部分,包括横切和纵切两种情况,还有注意好切割的边界,只有一行或者一列的时候不能切割*/ { double temp=min(f[1][x1][y1][x][y2]+f[i-1][x+1][y1][x2][y2],f[i-1][x1][y1][x][y2]+f[1][x+1][y1][x2][y2]);/*切割成的两个矩形,一个是1,一个是i -1份也是要枚举的*/ f[i][x1][y1][x2][y2]=min(f[i][x1][y1][x2][y2],temp); } for(int y=y1;y<y2;++y) { double temp=min(f[1][x1][y1][x2][y]+f[i-1][x1][y+1][x2][y2],f[i-1][x1][y1][x2][y]+f[1][x1][y+1][x2][y2]); f[i][x1][y1][x2][y2]=min(f[i][x1][y1][x2][y2],temp); } } } int main() { input(); DP(); double ans=f[k][1][1][8][8]/k-sum*sum; printf("%.3lf\n",sqrt(ans));/*注意数组下标只能是int类型*/ return 0; }
3.NOI 4976:硬币--容斥原理的完美应用
4976:硬币
- 总时间限制:
- 1000ms
- 内存限制:
- 262144kB
- 描述
-
宇航员Bob有一天来到火星上,他有收集硬币的习惯。于是他将火星上所有面值的硬币都收集起来了,一共有n种,每种只有一个:面值分别为a1,a2… an。 Bob在机场看到了一个特别喜欢的礼物,想买来送给朋友Alice,这个礼物的价格是X元。Bob很想知道为了买这个礼物他的哪些硬币是必须被使用的,即Bob必须放弃收集好的哪些硬币种类。飞机场不提供找零,只接受恰好X元。
- 输入
- 第一行包含两个正整数n和x。(1 <= n <= 200, 1 <= x <= 10000)
第二行从小到大为n个正整数a1, a2, a3 … an (1 <= ai <= x) - 输出
- 第一行是一个整数,即有多少种硬币是必须被使用的。
第二行是这些必须使用的硬币的面值(从小到大排列)。 - 样例输入
-
5 18 1 2 3 5 10
- 样例输出
-
2 5 10
- 提示
- 输入数据将保证给定面值的硬币中至少有一种组合能恰好能够支付X元。
如果不存在必须被使用的硬币,则第一行输出0,第二行输出空行。
方法一:
/*对每种硬币进行枚举,去掉每种硬币再进行01背包dp,如果最后方案数为0,那么这种硬币一定是必须用的,但是这种方法时间复杂度为O(n×n×x),接近4*10^8,即使加了一些优化(当方案数>0即退出当前循环)也还是TLE*/ #include<iostream> using namespace std; #include<cstdio> #define INF 10100 #define N 201 #include<algorithm> #include<cstring> int a[N],g[N],f[N]; int n,x; void input() { scanf("%d%d",&n,&x); for(int i=1;i<=n;++i) scanf("%d",&a[i]); } int sum=0; int ans[N]; void DP2() { for(int l=1;l<=n;++l) { memset(g,0,sizeof(g)); int p=a[l]; a[l]=0; g[0]=1; for(int i=1;i<=n;++i) for(int j=x;j>=a[i];--j) if(a[i]) g[j]+=g[j-a[i]]; else break; a[l]=p; if(!g[x]) { sum++; ans[sum]=a[l]; } } } int main() { input(); DP2(); if(!sum) { printf("0\n\n"); } else { printf("%d\n",sum); sort(ans+1,ans+1+sum); for(int i=1;i<=sum;++i) printf("%d ",ans[i]); } return 0; }
方法二:容斥原理的应用:
分析:想到的第一个方法就是f[x]-f[x-a[i]]是否是0,f[j]代表了到达j价格的方案总数,但是f[x-a[i]]看似没有使用过a[i],但是实际上可能早就已经被a[i]更新过了,所以不能使用f[],
就另外设定一个数组g[j]表示不用a[i]达到j价格的方案数目,很明显如果g[x]==0,那么这种硬币就必须使用,为了避免朴素的01背包(时间复杂度过高),我们采用递推的“容斥原理”,
for(int j=0;j<=x;++j)
{
if(j<a[i])/*j<a[i]的话,达到j价格的方案数目不受影响,仍然是f[j]*/
g[j]=f[j];
else g[j]=f[j]-g[j-a[i]];/*f[j]-使用了a[i]得到j价格的方案数目=没使用a[i]得到j价格的方案数目(也就是g[j]),但是使用了a[i]得到j价格的方案数目,我们怎么求呢?他就是g[j-a[i]],没用a[i]
达到j-a[i]这个价格的方案数,必须使用a[i]才能达到价格j,也就是二者相等。*/
}
注意:g[0]=1是初始化之一,为什么?
因为当j==a[i]的时候,g[j]=f[j]-1;减去的1,刚好是他自己组成a[i]的情况
/*g[j]=f[j]-g[j-a[i]],即g代表去掉某一种硬币后的方案数*/ #include<iostream> using namespace std; #include<cstdio> #include<algorithm> int n,x; #define INF 10100 #define N 201 int a[N],f[INF],g[INF]; #include<cstring> int main() { scanf("%d%d",&n,&x); for(int i=1;i<=n;++i) scanf("%d",&a[i]); f[0]=1; for(int i=1;i<=n;++i) for(int j=x;j>=a[i];--j) f[j]+=f[j-a[i]]; int ans[N]; ans[0]=0; for(int i=1;i<=n;++i) { memset(g,0,sizeof(g)); for(int j=0;j<=x;++j) { if(j<a[i]) g[j]=f[j]; else g[j]=f[j]-g[j-a[i]]; } if(!g[x]) { ans[0]++; ans[ans[0]]=a[i]; } } sort(ans+1,ans+ans[0]+1); if(!ans[0]) { printf("0\n\n"); return 0; } printf("%d\n",ans[0]); for(int i=1;i<=ans[0];++i) printf("%d ",ans[i]); printf("\n"); return 0; }
4. NOI 1665:完美覆盖---二进制解题思想
1665:完美覆盖--二进制解题思想
- 总时间限制:
- 1000ms
- 内存限制:
- 65536kB
- 描述
- 一张普通的国际象棋棋盘,它被分成 8 乘 8 (8 行 8 列) 的 64 个方格。设有形状一样的多米诺牌,每张牌恰好覆盖棋盘上相邻的两个方格,即一张多米诺牌是一张 1 行 2 列或者 2 行 1 列的牌。那么,是否能够把 32 张多米诺牌摆放到棋盘上,使得任何两张多米诺牌均不重叠,每张多米诺牌覆盖两个方格,并且棋盘上所有的方格都被覆盖住?我们把这样一种排列称为棋盘被多米诺牌完美覆盖。这是一个简单的排列问题,同学们能够很快构造出许多不同的完美覆盖。但是,计算不同的完美覆盖的总数就不是一件容易的事情了。不过,同学们 发挥自己的聪明才智,还是有可能做到的。
现在我们通过计算机编程对 3 乘 n 棋盘的不同的完美覆盖的总数进行计算。
任务
对 3 乘 n 棋盘的不同的完美覆盖的总数进行计算。 - 输入
- 一次输入可能包含多行,每一行分别给出不同的 n 值 ( 即 3 乘 n 棋盘的列数 )。当输入 -1 的时候结束。
n 的值最大不超过 30. - 输出
- 针对每一行的 n 值,输出 3 乘 n 棋盘的不同的完美覆盖的总数。
- 样例输入
-
2 8 12 -1
- 样例输出
- 3
- 153
- 2131
- 方法一:老师的思路:
- 虽然比较麻烦,但是以后再解决相同的问题中,可以参考一下这种方法,也就是二进制解题思想,在后面一个题目中也有应用,所以这里就特别整理一下:
- 分析:
- 把图中的每一列作为一个状态,0,表示没切割到了牌,1表示切割到了牌,那么这一列总共有七种情况:是000,001,100,111,110,011,101,010,(但是实际中101,010,两种情况是不存在的,这是无法铺满的)八种情况,
- 可能的状态有0~7共8种
- 定义f(i,j)表示3*i的棋盘上状态j的总数,f(n,0)即为所求。
#include<iostream> using namespace std; #include<cstdio> #define N 50 long long int dp[N][10]; int n; #include<cstring> using namespace std; int t1,t2,t3; int main() { while(scanf("%d",&n)==1&&n!=-1) { memset(dp,0,sizeof(dp)); dp[0][0]=1;/*在第0列,每行都没有被切割到得情况是1*/ for(int i=1;i<=n;++i) for(int j=0;j<8;++j)/*这里使用了j的二进制表示切刀还是没切刀*/ { t1=j%2;t2=j/2%2;t3=j/4;/*依次取到这个数二进制位,t1,t2,t3,由低到高*/ int tmp; tmp=(1-t1)*1+(1-t2)*2+(1-t3)*4;/*所有的0,1互转的情况,再找到对应的tmp*/ dp[i][tmp]+=dp[i-1][j];/*表示tmp这个状态可以由前一列的j状态推到*/ if(t1==0&&t2==0)/*相邻两个数是0,可以保持不变,只把t3或者t1转换*/ { tmp=(1-t3)*4; dp[i][tmp]+=dp[i-1][j]; } if(t2==0&&t3==0) { tmp=(1-t1); dp[i][tmp]+=dp[i-1][j]; } } cout<<dp[n][0]<<endl;/*最终要找的就是在第n列,没有不切刀情况*/ } return 0; }
- 方法二:递推公式(针对这道题的),普遍公式及其证明
- 分析规律:n==2 ,3,n==3,是11,....可以看出,当n是奇数的时候,输出0,当结果是偶数的时候符合f[n]=f[n-2]*4-f[n-4],根据这个公式就可以都求出来的了.
#include <stdio.h> int main() { unsigned long a[100] = { 3, 11 }; int i = 0; for( i = 2; i < 100; i++ ) a[i] = a[i-1] * 4 - a[i-2]; while( scanf( "%d", &i ) && i!=0) { if( i % 2 == 0 ) printf ( "%d\n", a[i/2-1] ); else printf( "0\n" ); } return 0; }
如果是2*n的地面覆盖,那就是斐波那契数列。
5.NOI 6046:数据包的调度机制
6046:数据包的调度机制
- 总时间限制:
- 1000ms
- 内存限制:
- 65536kB
- 描述
-
随着 Internet的迅猛发展,多媒体技术和电子商务应用日益广泛,Internet上的服务质量
(QoS,Qualityof Service)问题已越来越受到重视。网络中采用的数据包调度机制与网络的服务质量 QoS有着密切的关系。研究表明传统的基于队列的调度机制已不能满足网络服务质量QoS 的需求。服务质量 QoS取决于数据包的延迟。每一个数据包都有一个延迟惩罚值。由于数据包承载的数据不同,不同数据包的延迟惩罚值也可能不同。此外,数据包的延迟也和它的发送顺序有关。如果一个数据包被第K个发送,假设它的延迟惩罚值是D,则这个数据包的最终延迟是 (K - 1) * D。北京大学2012 级信息学院的同学在程序设计课堂上,设计了一种新的基于栈的数据包的调度算法。同学们通过栈的先进后出(Last in First out)的原理,改变数据包的发送顺序,以减小数据包的延迟总值。给定N 个等待调度的数据包,起始这N 个数据包排成一个队列等待发送。接着,这些数据包按序进栈,调度算法可以控制数据包的出栈顺序。因此通过栈,可以将后面的数据包先于前面的数据包发送出去。请你实现一个调度算法使N 个数据包的延迟总值最小。
- 输入
- 标准的输入包含若干组测试数据。输入第一行是整数T(1 <= T <= 1000),表明有T组测试数据。紧接着有T组连续的测试。每一组测试数据的第1行是 N(N <= 100),表述数据包的个数。接着的 N 行,每一行是一个整数,第i 行表示数据包i的延迟惩罚值( <=50 )。
- 输出
- 对于每组测试数据,输出最小的延迟总值。
- 样例输入
-
1 5 5 4 3 2 2
- 样例输出
- 24
- 问题分析:
- 枚举k是最后一个出栈,根据栈这个数据结构的特点,k如果是最后一个出栈,
k之前的一定在k之后的入栈前已经出栈了,k之后的元素之后才入栈,出栈。
根据栈的这一特点,我们可以把区间 [i--i+j-1]枚举第k个元素最后出栈来划分区间,划分为前后两个区间
先是区间[i..k-1],然后是区间[k+1..j],最后是k
具体的实现:
f[i][j]表示的是区间[i--i+j-1](j是延长的长度),最小延迟值
尝试把一个区间作为整体来看,那么这个区间的最小惩罚值是
f[i][k-i]+a[k]*(len-1)+f[k+1][i+len-1-(k+1)+1]+(sum[i+len-1]-sum[k])*(k-i),
但是我们可以看出len是表示k(i--i+len-1枚举序列中的元素)在当前序列中的位置,而不是整个n长序列的位置,那么*(len-1)会不会结果不对呢?而且f[i][k-i]在整体的序列中也不定是第一个,为什么不乘以他是第几个出发的呢?
我们来看后面这个(sum[i+len-1]-sum[k])*(k-i),sum是前缀和,因为区间[k+1][i+len-1-(k+1)+1]是当前区间f[i][j]的子区间,而且他在这个区间中的是从第k-i个开始发数据包的,所以要乘以(k-i),这就可以向大区间考虑了,更新大区间的小区间表示的仅是他自身作为第一组的情况,再加上(sum[i+len-1]-sum[k])*(k-i),就可以完整的表示出这个小区间在大区间中究竟是第几个位置了。
代码的实现:
1.考虑循环顺序:把长度作为参数,每次都用到之前的小区间,所以长度是外层循环,第二层是枚举每个数据包,第三层是枚举这个区间内可以作为最后一个出栈的元素,从第一个到最后一个都可以的。
代码:
/*f[i][j]表示的是区间[i--i+j-1](j是延长的长度),最小延迟值 */ #include<cstdio> #include<iostream> #include<cstring> #define INF 1<<30 using namespace std; #define N 111 int a[N],f[N][N],n,t; int sum[N]; void input() { scanf("%d",&n); for(int i=1;i<=n;++i) { scanf("%d",&a[i]); sum[i]=sum[i-1]+a[i]; } } void DP() { memset(f,0,sizeof(f)); for(int len=2;len<=n;++len) for(int i=1;i+len-1<=n;++i) { int tmp=INF;/*开始因为INF初值不够大,WA了几次*/ for(int k=i;k<=i+len-1;++k) { tmp=min(tmp,f[i][k-i]+a[k]*(len-1)+f[k+1][i+len-1-(k+1)+1]+(sum[i+len-1]-sum[k])*(k-i)); } f[i][len]=tmp; } } int main() { scanf("%d",&t); while(t--) { memset(sum,0,sizeof(sum)); memset(a,0,sizeof(a)); input(); DP(); printf("%d\n",f[1][n]); } return 0; }
6.NOI 1793:矩形覆盖--二进制解题思想/kruskal最小生成树算法
1793:矩形覆盖
- 总时间限制:
- 3000ms
- 内存限制:
- 65536kB
- 描述
- 在平面上给出了n个点,现在需要用一些平行于坐标轴的矩形把这些点覆盖住。每个点都需要被覆盖,而且可以被覆盖多次。每个矩形都至少要覆盖两个点,而且处于矩形边界上的点也算作被矩形覆盖。注意:矩形的长宽都必须是正整数,也就是说矩形不能退化为线段或者点。
现在的问题是:怎样选择矩形,才能够使矩形的总面积最小。 - 输入
- 输入包括多组测试数据。每组测试数据的第一行给出n (2 <= n <= 15),表示平面上的点数。后面的n行,每行上包括两个整数x, y (-1000 <= x, y <= 1000),给出一个点在平面上的x坐标和y坐标。输入数据保证:这n个点在平面上的位置各不相同。
最后一组测试数据中n = 0,表示输入的结束,这组数据不用处理。 - 输出
- 对每一组测试数据,输出一行,包括一个正整数,给出矩形的最小总面积。
- 样例输入
-
2 0 1 1 0 0
- 样例输出
-
1
- 提示
- 矩形的总面积指的是所有矩形的面积直接相加的结果
- 解法一:--二进制解题法
- 1)状态的表示:
- 我们很容易求出覆盖任意两个点的矩形,但是如何表示被若干个矩形覆盖的点,那么我们定义的状态应该清晰地表示覆盖了那几个点(因为覆盖任意几个点的结果是不同),因为题目对于矩形的个数没有限制,所以不能把矩形的个数作为参数(以后做其他DP题目的时候需要注意),对于有很多点表示他是否被覆盖多用二进制数表示1表示覆盖,0表示没有覆盖,最后一个状态就是f{(1<<n)-1}(位运算符的优先级比较低),所有二进制位都是1,那么他的前一个状态至少有一个点没有被覆盖,也可能有多个点,f(i)表示覆盖状态i所需的最小面积,那么对于状态i中的某个点p,点p必须与状态i中的另一个点q被一个矩形r覆盖,因为矩形至少覆盖两个点。
- 那么我们可以写出粗DP方程 : f(i)=min{area(p,q)+f[i']};
- 2)DP方程上的细节问题:
- 1.f(i) 与f(i')的关系
- 2.area(p,q)如果覆盖着不止两个点,那么它覆盖着几个点也会影响到更新f[i]到底是那个f[i']。
- 分析矩形:如果以两个点为矩形的内部还有一个点,那么这个矩形还存在优化的空间(前提是这个点不是边界的点),优化就是矩形内部存在两个小矩形,可以把这三个点联通,所以我们只要维护好矩形的四个角上的点就可以了,如果恰好是矩形的角上的点,恰好被覆盖,这种情况是我们的DP方程不能解决的,需要特别维护一下,而对于矩形内部有小矩形的情况,可以不管,因为他一定会被优化取代的,所以仍要分析area(p,q)与f[i']之间的对应关系,他们各覆盖几个点是相互影响的,
- 根据前面的分析,我们可以看出,被矩形r覆盖的点至少有一个,至多有4个点,t取1--1<<C;i++枚举不在状态i的点(既有个数的变化,又有编号的变化),再根据枚举的点,与f[i]按位异或运算,找出f[i']这个状态,^相当于相加但是不进位,可以1都变为0,也就是找到了前一个状态。
- 3)循环顺序的确定:因为这不是线性顺序而是二进制位关系,所以用for循环顺序较为复杂,建议使用记忆化搜索,比较方便。
- (c++:程序调试中的注意:单步执行是执行一行而不是一个语句,所以把多个语句放在一行,单步执行可能会出现跟踪变量上的误差。)
- 代码实现:
#include<iostream> using namespace std; #include<cstdio> #include<cstring> #include<cstdlib> const int N=16; const int INF=1<<30; const int zb=2010; struct Poi{ int x,y; }poi[N];/*点的坐标*/ int f[1<<N],n;/*f是状态*/ int g[zb][zb];/*记录坐标i,j所对应的点的编号,在检查矩形的四个角上是否有其他点时使用到*/ int area[N][N];/*记录矩形覆盖i,j点的面积*/ void SUm() { for(int i=0;i<n;++i) for(int j=0;j<n;++j)/*求出矩形面积的过程,注意横坐标或者纵坐标相等的情况*/ { if(i==j||area[i][j]) continue; int dx=abs(poi[i].x-poi[j].x); int dy=abs(poi[i].y-poi[j].y); if(dx&&dy) area[i][j]=area[j][i]=dx*dy; else if(!dx) area[i][j]=area[j][i]=dy; else area[i][j]=area[j][i]=dx; } for(int i=0;i<n;++i) f[1<<i]=INF;/*为什把i个点被覆盖的情况设为INF,为了防止一个点的状态在solve函数中更新其他的,因为是min取最小*/ for(int i=0;i<n;++i) for(int j=i+1;j<n;++j) { f[(1<<i)|(1<<j)]=area[i][j];/*|运算有1则1,二进制数中有两个1,表示当前状态两个点被覆盖*/ } } void input() { memset(area,0,sizeof(area)); memset(f,-1,sizeof(f));/**/ memset(g,-1,sizeof(g));/*-1,都是标志作用,要与0区别开*/ memset(poi,0,sizeof(poi)); for(int i=0;i<n;++i) { scanf("%d%d",&poi[i].x,&poi[i].y); g[poi[i].x+1000][poi[i].y+1000]=i;/*因为坐标最小到-1000,要防止数组越界,那么就都加1000*/ } SUm(); } void check(int x,int y,int *p,int &cnt,int state) { int pr=g[x+1000][y+1000];/*检查这个点不是在矩形角上*/ if(pr==-1||!((state>>pr)&1)) return ;/*如果这不是一个点或者这个点被状态state覆盖,那就返回*/ p[cnt]=pr;cnt++;/*把这个点记录到p数组中,*/ } int solve(int); int calc(int *p,int cnt,int state) { int tmp,ans=INF; for(int i=1;i<(1<<cnt);++i)/*i表示当前的p数组中cnt个点的组合情况,总共有2^cnt中,可一个点不在state,也可以多个点不在(涉及那几个点不在)*/ { tmp=state;/*为了寻找state的上一状态,所以f[1个点]的情况=INF,就是为了防止这里的组合中,被一个点更新*/ for(int j=0;j<cnt;++j)/*枚举不包含在state的情况*/ { if((i>>j)&1)/*取出i情况的覆盖的点不在state的点在p中的位置,进而用p[j]改变tmp,寻找前一状态*/ tmp^=1<<p[j]; /*在tmp中进行修改,1^1=0,就把覆盖的点改为没覆盖的点,也就是前一状态*/ } int cur=solve(tmp); ans=min(cur,ans); } return ans; } int solve(int state) { if(state==0) return 0;/*递归的边界,注意这里calc,和solve是间接相互递归调用*/ if(f[state]!=-1) return f[state]; int minn,p[4],cnt; int q; for(q=0;q<n;++q) if((state>>q)&1)/*选择state中,被覆盖的点中编号最小的点,为什么选择这个作为状态转移的点呢?其实这个点是可以随便选的,用这个点找出之前的状态,之前的状态一定没有这个点,那之前的状态再用他其中的编号最小的点更新,最有一定可以遍历所有矩形*/ break; cnt=0; p[cnt]=q; int ans=INF; for(int j=0;j<n;++j) { cnt=1;/*注意:不能是cnt++,因为每次循环j都会把p数组更新,所以cnt+的位置一定要确保每次能让p更新,而且不越界*/ if(j!=q&&((state>>j)&1)) { p[cnt]=j; cnt++; if(poi[q].x==poi[j].x)/*这就是检查当前的矩形的四个角是不是还覆盖着其他的点*/ { check(poi[q].x+1,poi[q].y,p,cnt,state);/*都加1处理,是矩形的面积最少是1*...,加了1后检查该位置是不是有点*/ check(poi[j].x+1,poi[j].y,p,cnt,state); } else if(poi[q].y==poi[j].y) { check(poi[q].x,poi[q].y+1,p,cnt,state); check(poi[j].x,poi[j].y+1,p,cnt,state); } else { check(poi[q].x,poi[j].y,p,cnt,state);/*对角互相检查*/ check(poi[j].x,poi[q].y,p,cnt,state); } int cur=calc(p,cnt,state)+area[q][j];/*核心DP方程:当前的f[state]=area(p,q)+f[state'](对应的前一状态),calc就是寻找这一状态,并且寻找最小值的过程*/ ans=min(ans,cur); } } f[state]=ans; return f[state]; } int main() { while(scanf("%d",&n)==1&&n) { input(); printf("%d\n",solve((1<<n)-1)); } return 0; }
- 解法二:最小生成树算法(未实现):
- 类似求无向连通图的最小生成树算法;更简单,不需要矩形“连通”:
1)未覆盖的点集合为A、已覆盖点的集合为B,初始时n个点全在A中。
2)n个点两两生成最小覆盖矩形,矩形集合为R,按面积排序。
3)初始化总面积S=0
4)循环直到A为空:
4.1)从R中选最小的一个矩形r
4.2)如果r覆盖的点有不在B中的:点就从A移到B,累计面积 S=S+r
4.3)并从R中移除r。
5)输出S - 没有处理好的几个地方:在矩形角上如果有多个点的情况,我用了一个空间很大flag数组,才能实现。
- 与最小生成树的不同之处也没有解决好:矩形不必连接成一片,但是最小生成树要求连成一片,这也是一个错误所在
- 在NOI上只过一半的数据:
- 代码:
#include<iostream> using namespace std; #include<cstdio> #include<cstring> #define INF 2010 #define JIA 1000 #define N 16 #include<cmath> #include<cstdlib> #include<algorithm> int sum(int x1,int y1,int x2,int y2) { if(x2-x1!=0&&y2-y1!=0) return abs((x2-x1)*(y2-y1));/*解决最起码是一个矩形,即使两个点在同一列或者同一行上*/ else { if(x2-x1==0) return abs(y2-y1); else return abs(x2-x1); } } struct Poi{ int x,y; }; Poi poi[N]; bool flag[INF+JIA][INF+JIA]; struct Edge{ int u,v,w; }; int cmp(Edge a,Edge b) { return a.w<b.w; } Edge edge[N*N]; int n,t=0,father[N]; long long int cou=0; void input() { memset(flag,false,sizeof(flag)); memset(poi,0,sizeof(poi)); memset(edge,0,sizeof(edge)); for(int i=1;i<=n;++i) scanf("%d%d",&poi[i].x,&poi[i].y); for(int i=1;i<=n;++i) for(int j=i+1;j<=n;++j) { ++t; edge[t].u=i; edge[t].v=j; edge[t].w=sum(poi[i].x,poi[i].y,poi[j].x,poi[j].y); } } int find(int x) { if(x!=father[x]) return father[x]=find(father[x]); return father[x]; } void unionn(int a,int b) { father[b]=a; } void kruskal() { cou=0; for(int i=1;i<=n;++i) father[i]=i; sort(edge+1,edge+t+1,cmp); for(int i=1;i<=t;++i) { if(flag[poi[edge[i].u].x+JIA][poi[edge[i].u].y+JIA]&&flag[poi[edge[i].v].x+JIA][poi[edge[i].v].y+JIA]) continue; int r1=find(edge[i].u); int r2=find(edge[i].v); if(r1!=r2) { unionn(r1,r2); cou+=edge[i].w; int x1=min(poi[edge[i].u].x,poi[edge[i].v].x); int x2=max(poi[edge[i].u].x,poi[edge[i].v].x); int y1=min(poi[edge[i].u].y,poi[edge[i].v].y); int y2=max(poi[edge[i].u].y,poi[edge[i].v].y); for(int i=x1;i<=x2;++i) { flag[i+JIA][y1+JIA]=flag[i+JIA][y2+JIA]=true; } for(int j=y1;j<=y2;++j) { flag[x1+JIA][j+JIA]=flag[x2+JIA][j+JIA]=true; } } } } int main() { while(scanf("%d",&n)==1&&n!=0) { input(); kruskal(); cout<<cou<<endl; } return 0; }
- 这里提供一些数据:
2
1 1
2 1
ans=1
5
0 0
0 1
0 2
0 3
0 5
ans=4//
2
1000 -1000
-1000 1000
ans=4000000
4
0 1
1 0
2 1
1 2
ans=2//
3
0 1
1 0
2 2
ans=3