藍橋杯-數三角(ac程式碼時間複雜度分析)

小程xy發表於2024-05-24

問題描述

小明在二維座標系中放置了 ( n ) 個點,他想在其中選出一個包含三個點的子集,這三個點能組成三角形。然而這樣的方案太多了,他決定只選擇那些可以組成等腰三角形的方案。請幫他計算出一共有多少種選法可以組成等腰三角形?

輸入格式

輸入共 ( n+1 ) 行。

第一行為一個正整數 ( n )。

後面 ( n ) 行,每行兩個整數 ( x_i ) 和 ( y_i ) 表示第 ( i ) 個點的座標。

輸出格式

輸出共 1 行,一個整數。

樣例輸入

5
1 1
4 1
1 0
2 1
1 2

樣例輸出

4

評測用例規模與約定

  • 對於 20% 的資料,保證 ( n <= 200)。
  • 對於 100% 的資料,保證 ( n <= 2000),( 0 <= xi, yi <= 1e9)。

題解:

正常的暴力程式碼👇 時間複雜度O(n^3) 會超時, 只過了不到一半資料

#include <bits/stdc++.h>
using namespace std;
#define int double

const signed N = 1e4;

int a[N], b[N];

signed main()
{
	signed n; cin >> n;
	for (signed i = 0; i < n; i ++) cin >> a[i] >> b[i];
	
	int cnt = 0;
	for (signed i = 0; i < n; i ++)
		for (signed j = i + 1; j < n;  j ++)
			for (signed k = j + 1; k < n; k ++)
			{
				int x1 = sqrt((a[i] - a[j]) * (a[i] - a[j]) + (b[i] - b[j]) * (b[i] - b[j]));
				int x2 = sqrt((a[j] - a[k]) * (a[j] - a[k]) + (b[j] - b[k]) * (b[j] - b[k]));
				int x3 = sqrt((a[i] - a[k]) * (a[i] - a[k]) + (b[i] - b[k]) * (b[i] - b[k]));
				if (x1 + x2 > x3 && x1 + x3 > x2 && x2 + x3 > x1)
				{
					if (abs(x1 - x2) < 1e-8 && abs(x2 - x3) < 1e-8 && abs(x1 - x3) < 1e-8) continue;  // 等邊三角形不算, 也可以不寫, 因為題中要求橫縱座標都是整數, 那麼不可能構成等邊三角形
					if (abs(x1 - x2) < 1e-8 || abs(x2 - x3) < 1e-8 || abs(x1 - x3) < 1e-8)  // double 型別判斷是否相同要用差來判斷, double型別的變數在計算機中儲存的值會丟失精度, 不能直接用==
						cnt ++;
				}
			}
			
	cout << cnt << endl;
	return 0;
}

ac程式碼

這題的思路是儘可能最佳化時間複雜度,

  • 我們列舉每個點, 然後對該點生成一個hash表, 把到該點距離相同的點放到一個陣列中, 然後遍歷這個hash表中的所有陣列,任選陣列中的兩個點, 判斷這三個點是否滿足條件

ac程式碼👇 這個的時間複雜度是在O(n^2) 和 O(n^3)之間的

#include <bits/stdc++.h>
using namespace std;
#define int long long
#define x first
#define y second
typedef pair<int, int> PII;
const double cha = 1e-8;
vector<PII> v;

double dist(int x1, int y1, int x2, int y2)
{
	return sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
}

bool check(PII a, PII b, PII c)
{
	double x1 = dist(a.x, a.y, b.x, b.y);
	double x2 = dist(a.x, a.y, c.x, c.y);
	double x3 = dist(b.x, b.y, c.x, c.y);
	
	if (x1 + x2 <= x3 || x1 + x3 <= x2 || x2 + x3 <= x1) return false;  // 不能構成三角形
	if (abs(x1 - x2) < cha && abs(x1 - x3) < cha && abs(x2 - x3) < cha) return false;  // 是等邊三角形。也可以不寫, 因為題中要求橫縱座標都是整數, 那麼不可能構成等邊三角形
	
	return true;
}

signed main()
{
	int n; cin >> n; v.resize(n);
	
	for (int i = 0; i < n; i ++) cin >> v[i].x >> v[i].y;
	
	int cnt = 0;
	
	for (int i = 0; i < n; i ++)
	{
		unordered_map<double, vector<int>> mp;  // 選用i為一個點的前提下, 其他點到i距離相同的點存放到一個 vector中
		for (int j = 0; j < n; j ++)
		{
			if (i == j) continue;
			double dis = dist(v[i].x, v[i].y, v[j].x, v[j].y);  // 計算距離
			mp[dis].emplace_back(j);  
		}
		
		for (auto it : mp)
		{
			vector<int> vv;
			vv = it.second;  // vv 是到i距離相同的點的 集合, 也就是說vv中的元素都是到i的距離相同的點
			for (int j = 0; j < vv.size(); j ++)
				for (int k = j + 1; k < vv.size(); k ++)
				{
					if (check(v[i], v[vv[j]], v[vv[k]])) cnt ++;  // 因為vv裡面存的是下標, 所以是 v[vv[j]]
				}
		}
		
	}
	cout << cnt << endl;
	return 0;
}

下面是筆者自己理解的ac程式碼的時間複雜度的分析

hash時間複雜度分析:

中間的點在執行hash時是最耗時的, 而且圖中這種方式的點分佈也是讓所有點的時間複雜度儘可能多的情況。

下面分別是vector的個數和map個數分析

  • 因為座標的橫縱座標都只能是整數, 所有一個 map對映的vector中最多有4個數, 也就是說距離到i座標相同的點最多有4個, 上下左右距離相同 4個, 4個對焦的點距離相同.

  • 而map對映的個數是 中間的點的最外圍每增加一圈, map對映的個數加2, 因為每增加一圈 橫縱的距離+1, 斜線的距離+根號2, 一共是兩種

hash執行總次數 m 和總個數 n 之間的大小關係分析

當點的個數增加到2000的時候, 實際hash執行的次數不到2000 (map的對映個數不到 100,(這裡按50個外圈, 50 * 50是2500個點的個數, 比2000個點大), vecotr中最多是4個點, 4 * 4 = 16) 所以hash執行的次數是100 * 16 == 1600, 這還是中間點的情況, 而且是按2500個點算, 其他點的遍歷次數更少 ( m < n )

還有就是, 從上面的分析可以看到, 點的個數越小, 遍歷hash的總次數有可能反而比 n 次還要多, 但是當點的個數n邊大的時候, 遍歷hash的總次數會比(n - 1)小很多。------> 比如一共9個點, 3*3, 中間的點正常應該遍歷(n - 1) = 8次, 但是用hash的話應該遍歷了 2 * (4 * (4 - 1) / 2) = 12次, 2是hash的兩個對映 距離為1和距離為根號2, 4是距離為1和根號2的vector中各有4各元素 。(ps:程式碼中的for ( int j = 0; j < vv.size(); j ++);for (int k = j + 1; k < vv.size(); k ++) 時間複雜度是(n * (n - 1) / 2)

總的時間複雜度

當點的個數比較大的時候, O(n * (n + m)), m < n, 時間複雜度很OK
即使當點的個數n比較小的時候m可能比n大, 但無傷大雅, 因為它不會比n大特別多,而且也說了 n 比較小這個前提, 這個ac程式碼的時候複雜度可以看成是O(n^2)

覺得寫的不錯的話, 點個贊吧~

相關文章