Problem Description
You are required to put n straight lines on a plane, guaranteeing no three lines share a common point and no lines are coincident. They will form some intersections, please output all possible numbers of intersections.
Input
The first line, an integer T(1 ≤ T ≤ 5) - the number of test cases.
Following T lines, an positive integer n(1 ≤ n ≤ 700) - the number of lines.
Output
Several lines, each line several numbers separated by a blank space - all possible numbers of intersections.
Sample Input
2
3
5
Sample Output
0 2 3
0 4 6 7 8 9 10
這個題題意非常短,問的是給定n條直線,問在沒有三線共點等情況下能產生多少種不同的交點數。
這個題可以看做HDU1466的加強版。原題給的數據范圍是n <= 20,那么就可以用普通的dp求解。如果手玩幾個樣例,例如n = 5,會發現一開始如果五條線平行的話交點為0,四條線平行一條線與這四條平行線相交的話交點為4,三條線平行,另外兩條線互相平行或相交的話交點數為6或者7...可以看出來平行線的存在能夠將問題規模縮小。因為如果x條線相互平行,另外y條線任意(但不與這x條線平行)的話,總的交點數就是y條線自己產生的交點數 + x * y,原因就是y條線中的每條線都與x條平行線產生x個交點。
這樣就可以進行dp了,設\(dp[i, j]\)為用i條直線產生j個交點是否可行,k為i條線中相互平行的線的數量,則轉移方程為\(dp[i, j] \ |= \ dp[i - k, j - (i - k) \times k]\)。其中i - k就是任意的線的數量,(i - k) * k就是任意的線與平行線產生的交點數。這樣就可以輕松通過HDU1466這道題了。
#include <bits/stdc++.h>
using namespace std;
const int N = 25;
const int NN = N * N / 2;//交點數最多有這么多
bool dp[N + 5][NN + 5];//dp[i][j]表示用i條直線湊出來j個點是否可行
void init() {
for(int i = 0; i < N; i++) dp[i][0] = 1;
for(int i = 1; i <= N; i++) {
for(int j = 1; j <= NN; j++) {
for(int k = 0; k < i; k++) {//k條直線平行
if(j - (i - k) * k >= 0) dp[i][j] = dp[i][j] | dp[i - k][j - (i - k) * k];
}
}
}
}
void solve(int n) {
vector<int> ans;
for(int i = 0; i < NN; i++) {
if(dp[n][i]) ans.push_back(i);
}
for(int i = 0; i < ans.size(); i++) {
if(i != ans.size() - 1) cout << ans[i] << " ";
else cout << ans[i] << endl;//注意行末空格
}
}
int main () {
cin.tie(0);
ios::sync_with_stdio(false);
init();
int n;
while(cin >> n) {
solve(n);
}
}
然而對於比賽的這道題,n的范圍陡然增加到了700,上述代碼的時間復雜度為\(O(n^4)\),顯然時間上無法通過,而且有爆內存的風險。那么就要考慮進行優化,一種可行的方案是使用bitset進行優化。觀察上面代碼,第二重循環實際上直接貢獻了\(n^2\)的復雜度,那么借助bitset,我們可以把它優化成\(O(\frac{n^2}{w})\)。具體來說,對於每一個i維護一個bitset,bitset的大小為\(\frac{n\times (n - 1)}{2}\),在循環里直接省略掉原先的第二重循環,同時把轉移方程的寫法改為dp[i] = dp[i] | dp[i - k] << ((i - k) * k);這里相當於整體進行或操作而不是單獨進行遍歷,借助bitset的玄學操作可以帶來不小的提升。因為左移會在低位補0,因此也不用像上面代碼一樣擔心越界的問題。
#include <bits/stdc++.h>
using namespace std;
const int N = 700;
const int NN = N * N / 2;
bitset<NN + 5> dp[N + 5];
void init() {
for(int i = 0; i < N; i++) dp[i][0] = 1;
for(int i = 1; i <= N; i++) {
for(int k = 0; k < i; k++) {
dp[i] = dp[i] | dp[i - k] << ((i - k) * k);
}
}
}
void solve(int n) {
vector<int> ans;
for(int i = 0; i < NN; i++) {
if(dp[n][i]) ans.push_back(i);
}
for(int i = 0; i < ans.size(); i++) {
if(i != ans.size() - 1) cout << ans[i] << " ";
else cout << ans[i] << endl;
}
}
int main () {
cin.tie(0);
ios::sync_with_stdio(false);
init();
int n;
while(cin >> n) {
solve(n);
}
}
然而本地跑一下會發現init()函數執行的時間還是非常長(需要好幾秒才能進行查詢),瓶頸在於bitset開的實際上非常大。這時候就需要繼續進行優化。比賽的時候發現當輸出了若干個數以后,后面的數都是連續的,通過對拍程序測試一下n = 20的時候100以后基本都是連續的,猜想可能是大約5倍以上后面就都是連續的了(然而是錯的),題解給的界限是31500,這樣的話bitset實際上只需要開到這個上界就足矣,i大於上屆的話直接輸出即可,這樣就能通過本題了。
#include <bits/stdc++.h>
using namespace std;
const int N = 700;
const int NN = N * N / 2;
#define MX 35000
bitset<MX> dp[N + 5];//dp[i][j]表示用i條直線湊出來j個點是否可行
void init() {
for(int i = 0; i <= N; i++) dp[i][0] = 1;
for(int i = 1; i <= N; i++) {
for(int k = 0; k <= i; k++) {
dp[i] = dp[i] | (dp[i - k] << ((i - k) * k));
}
}
}
void solve(int n) {
vector<int> ans;
int lim = min(MX - 1, n * (n - 1) / 2);
for(int i = 0; i <= lim; i++) {
if(dp[n][i]) ans.push_back(i);
}
for(int i = lim + 1; i <= n * (n - 1) / 2; i++) ans.push_back(i);
for(int i = 0; i < ans.size(); i++) {
if(i != ans.size() - 1) cout << ans[i] << " ";
else cout << ans[i] << endl;
}
}
int main () {
cin.tie(0);
ios::sync_with_stdio(false);
init();
int t;
cin >> t;
while(t--) {
int n;
cin >> n;
solve(n);
}
}
