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