本來想刷省賽題呢,結果一不小心刷成國賽了
真是個小迷糊〒▽〒
但,又如何( ?? ω ?? )?
記錄刷題的過程、感悟、題解。
希望能幫到,那些與我一同前行的,來自遠方的朋友😉
大綱:
一、子2023-(題解)-遞推or動態規劃
二、雙子數-(題解)-篩法、類型(unsigned long long)😥
三、班級活動-(題解)-不出所料、貪心+計數
四、合并數列-(題解)-妥妥的前綴和😥,當然雙指針也能做
五、數三角-(題解)-這個真的就是算術題了,還要用到各種優化(叉乘、用半徑分組)
六、刪邊問題-(題解)-圖(tarjan算法)割邊、割點,經典板子題
七、AB路線-(題解)-BFS廣搜,最短路徑、記憶話搜索😉
八、抓娃娃-(題解)-簡單點的差分+前綴和😊
九、十,等后續沖擊國賽時,再解決。
一、子2023
問題描述
小藍在黑板上連續寫下從?11?到?20232023?之間所有的整數,得到了一個數字序列:?S=12345678910111213...20222023S=12345678910111213...20222023。 小藍想知道?SS?中有多少種子序列恰好等于?20232023?
以下是?33?種滿足條件的子序列(用中括號標識出的數字是子序列包含的數字):
1[2]34567891[0]111[2]1[3]14151617181920212223...
1[2]34567891[0]111[2]1[3]14151617181920212223...
1[2]34567891[0]111[2]131415161718192021222[3]...
1[2]34567891[0]111[2]131415161718192021222[3]...
1[2]34567891[0]111213141516171819[2]021222[3]...
1[2]34567891[0]111213141516171819[2]021222[3]...注意以下是不滿足條件的子序列,雖然包含了?22、00、22、33?四個數字,但是順序不對:
1[2]345678910111[2]131415161718192[0]21222[3]...
1[2]345678910111[2]131415161718192[0]21222[3]...答案提交
這是一道結果填空的題,你只需要算出結果后提交即可。本題的結果為一個整數,在提交答案時只填寫這個整數,填寫多余的內容將無法得分。
動態規劃解法:
本題解法,說成是狀態規劃,可能會引起恐懼,其實它就是一道簡單的狀態推導題( ?? ω ?? )?
C++
#include <iostream>
#include <vector>
using namespace std;
// 是個簡單的動態規劃就算了
// 怎么又是一道越界題目
// 以后統一不用long long改用 unsigned long long。更大。
int main(){vector<unsigned long long> dp(4,0);string str="";for(int i=1; i<=2023; ++i) str+=to_string(i);// 本題的解法是動態規劃for(char c : str){if(c=='2'){dp[0]++;dp[2]+=dp[1];}if(c=='0') dp[1]+=dp[0];if(c=='3') dp[3]+=dp[2];}cout<<dp[3]<<endl;return 0;
}
Java
public class DpProblem {public static void main(String[] args) {// 創建一個長度為 4 的 long 類型數組 dp 并初始化為 0long[] dp = new long[4];// 拼接字符串StringBuilder str = new StringBuilder();for (int i = 1; i <= 2023; i++) {str.append(i);}// 動態規劃過程for (int i = 0; i < str.length(); i++) {char c = str.charAt(i);if (c == '2') {dp[0]++;dp[2] += dp[1];}if (c == '0') {dp[1] += dp[0];}if (c == '3') {dp[3] += dp[2];}}// 輸出結果System.out.println(dp[3]);}
}
二、雙子數
問題描述
若一個正整數?xx?可以被表示為?p2×q2p2×q2,其中?pp、qq?為質數且?p≠qp=q,則?xx?是一個雙子數。請計算區間?[2333,?23333333333333?][2333,?23333333333333?]?內有多少個雙子數?
答案提交
這是一道結果填空的題,你只需要算出結果后提交即可。本題的結果為一個整數,在提交答案時只填寫這個整數,填寫多余的內容將無法得分。
// 我本以為,本題最難的是歐拉篩,也就是線性篩
// 后來我發現,我錯了,而且錯的離譜
// 歐拉篩,能用埃氏篩代替,能用樸素也就是暴力法代替。
// 而本題最大的難點是符號位,如果你開到(long long),答案會始終多10,讓你痛不欲生。
// 本題要開到,unsigned long long
// int->1e9 , long long->1e18, unsigned long long是longlong的兩倍
// 切記,不會歐拉篩的,可以用線性篩代替,或者直接暴力(會慢一些):: 四種篩法 ::
C++
#include <iostream>
#include <vector>
#define ll long long
using namespace std;const int N = 1e7 + 10;
vector<bool> vec(N, true); // 假設都是質數
vector<ll> res;
void sieve(){ // 歐拉篩vec[0]=vec[1]= false;for(int i=2; i<vec.size(); ++i){if(vec[i]) res.push_back((ll)i);for(ll num:res){if(num*i>vec.size()) break; // 超出最大范圍vec[i*num] = false;if(i%num==0) break; // 確保每個合數,只被最小因子除去。}}
}
//天吶,你要這樣玩?還咋玩??
int main(){sieve();ll num = 0;for(ll i=0;i<res.size();i++){unsigned ll pp = res[i]*res[i];if(pp * pp >23333333333333) break;//一點小優化for(ll j=i+1;j<res.size();j++){ll qq = res[j]*res[j];if(pp*qq>23333333333333) break;if(pp*qq<2333) continue;num++;}}cout<<num<<endl;return 0;
}
Java
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;public class PrimeNumberCombination {// 定義常量 N,用于篩法范圍static final int N = 10000010;// 標記每個數是否為質數的布爾數組static boolean[] isPrime = new boolean[N];// 存儲質數的列表static List<Integer> primes = new ArrayList<>();// 歐拉篩函數,篩選出所有質數public static void sieve() {// 初始化所有數為質數for (int i = 0; i < N; i++) {isPrime[i] = true;}// 0 和 1 不是質數isPrime[0] = isPrime[1] = false;for (int i = 2; i < N; i++) {if (isPrime[i]) {primes.add(i);}for (int prime : primes) {if (prime * i >= N) {break;}isPrime[prime * i] = false;if (i % prime == 0) {break;}}}}public static void main(String[] args) {// 調用歐拉篩函數sieve();BigInteger limit1 = BigInteger.valueOf(23333333333333L);BigInteger limit2 = BigInteger.valueOf(2333L);long num = 0;for (int i = 0; i < primes.size(); i++) {BigInteger pp = BigInteger.valueOf(primes.get(i)).pow(2);if (pp.pow(2).compareTo(limit1) > 0) {break;}for (int j = i + 1; j < primes.size(); j++) {BigInteger qq = BigInteger.valueOf(primes.get(j)).pow(2);BigInteger product = pp.multiply(qq);if (product.compareTo(limit1) > 0) {break;}if (product.compareTo(limit2) < 0) {continue;}num++;}}// 輸出滿足條件的組合數量System.out.println(num);}
}
三、班級活動
問題描述
小明的老師準備組織一次班級活動。班上一共有?nn?名 (nn?為偶數) 同學,老師想把所有的同學進行分組,每兩名同學一組。為了公平,老師給每名同學隨機分配了一個?nn?以內的正整數作為?idid,第?ii?名同學的?idid?為?aiai?。
老師希望通過更改若干名同學的?idid?使得對于任意一名同學?ii,有且僅有另一名同學?jj?的?idid?與其相同 (ai=ajai?=aj?)。請問老師最少需要更改多少名同學的?idid?
輸入格式
輸入共?22?行。
第一行為一個正整數?nn。
第二行為?nn?個由空格隔開的整數?a1,a2,...,ana1?,a2?,...,an?。
輸出格式
輸出共?11?行,一個整數。
樣例輸入
4 1 2 2 3
樣例輸出
1
樣例說明
僅需要把?a1a1??改為?33?或者把?a3a3??改為?11?即可。
評測用例規模與約定
對于?20%20%?的數據,保證?n≤103n≤103。
對于?100%100%?的數據,保證?n≤105n≤105
// ?_??當我找到哪里錯了之后,淚流滿面
// 本題其實挺簡單
// 讀題:“每名同學,隨機分配n以內的正整數作為id”?
// 這說明,每個同學的id有兩種情況。
// 此時舉一個簡單的例子就OK了
// 像題目中舉得例子,1 2 2 3 一組(2,2)就能配對,僅需更改3變成1(1->3也行)就OK了,只需一次。
// 推算成公式,也就是 2/1=1 -> 散列的總數/2
// 進階一點,當id為 2 2 2 1時,此時一組(2,2)就能配對,這時僅需更改剩下的2變成1就OK了,也只需要更改一次。
// 如果是 2 2 2 2 2 1 呢,要先去掉一組(2,2)
// 此時剩下 2 2 2 1,因為不能與已經配對的(2,2)重復,
// 所以先把其中一個2改為1,需要一次。
// 此時剩下 2 2,只需將它們改成其他數就行,如改成(3,3),需要兩次。
// 一共用了3次,也就是2的總數 減去2 也就是減去(2,2)這個不需要改變的組合。
// 也就是 當已被占用的id的數量,大于未被占用id時,總數等于 重復 id的數量。
C++
#include <iostream>
const int N = 1e5+5;
using namespace std;
int arr[N]; int main()
{int n;cin>>n;for(int i=0; i<n; ++i){int num;cin>>num;arr[num]++;}int num1=0, num2=0;for(int i : arr){if(i>2) num1 += i-2; // 求取數量相同的數在減2;else if(i==1) num2++;}int sum = 0;// 當已被占用的id的數量,大于未被占用id時,那么sum = num1;if(num1>num2){sum = num1; }else{ // num2<num1的情況sum = num2 + (num1-num2)/2;}cout<<sum<<endl;return 0;
}
Java
import java.util.Scanner;public class StudentIdAdjustment {// 定義常量 N,用于數組大小static final int N = 100005;public static void main(String[] args) {// 創建 Scanner 對象用于讀取輸入Scanner scanner = new Scanner(System.in);// 定義一個數組來記錄每個 ID 出現的次數int[] arr = new int[N];// 讀取學生的數量int n = scanner.nextInt();// 循環讀取每個學生的 ID,并統計每個 ID 出現的次數for (int i = 0; i < n; i++) {int num = scanner.nextInt();arr[num]++;}// 初始化兩個變量,用于統計需要處理的不同情況的數量int num1 = 0;int num2 = 0;// 遍歷數組,統計 num1 和 num2 的值for (int i : arr) {// 如果某個 ID 出現的次數大于 2,計算超出 2 的部分并累加到 num1if (i > 2) {num1 += i - 2;} // 如果某個 ID 只出現了 1 次,將 num2 加 1else if (i == 1) {num2++;}}// 初始化最終結果變量int sum = 0;// 當 num1 大于 num2 時,說明已被占用的 ID 數量更多,sum 等于 num1if (num1 > num2) {sum = num1;} // 當 num2 大于等于 num1 時,按照相應規則計算 sumelse {sum = num2 + (num1 - num2) / 2;}// 輸出最終結果System.out.println(sum);// 關閉 Scanner 對象,釋放資源scanner.close();}
}
四、合并數列
問題描述
小明發現有很多方案可以把一個很大的正整數拆成若干正整數的和。他采取了其中兩種方案,分別將他們列為兩個數組?{a1,a2,...,an}{a1?,a2?,...,an?}?和?{b1,b2,...,bm}{b1?,b2?,...,bm?}。兩個數組的和相同。
定義一次合并操作可以將某數組內相鄰的兩個數合并為一個新數,新數的值是原來兩個數的和。小明想通過若干次合并操作將兩個數組變成一模一樣,即?n=mn=m?且對于任意下標?ii?滿足?ai=biai?=bi?。請計算至少需要多少次合并操作可以完成小明的目標。
輸入格式
輸入共?33?行。
第一行為兩個正整數?nn,?mm。
第二行為?nn?個由空格隔開的整數?a1,a2,...,ana1?,a2?,...,an?。
第三行為?mm?個由空格隔開的整數?b1,b2,...,bmb1?,b2?,...,bm?。
輸出格式
輸出共?11?行,一個整數。
樣例輸入
4 3 1 2 3 4 1 5 4
樣例輸出
1
樣例說明
只需要將?a2a2??和?a3a3??合并,數組?aa?變為?{1,5,4}{1,5,4},即和?bb?相同。
評測用例規模與約定
對于?20%20%?的數據,保證?nn,?m≤103m≤103。
對于?100%100%?的數據,保證?nn,?m≤105m≤105,0<ai0<ai?,?bi≤105bi?≤105。
// 本題原意:“兩個數列,通過不斷合并相鄰的兩個數,使兩個數列相同”
// 注意-“相鄰” “合并(相加)”,也就意味著可能要使用前綴和。
// 用反向思維來看,兩個數列最終是相同的。
// 也就意味著從倆數列,第一個數開始,就要是相同的。
// 我們只需要從頭開始計算前綴和,如果相同時,代表倆數列第i位已經相同,
// 此時更新倆前綴和的計算起始位置即可。
// 所以本題是,雙指針與前綴和的結合。
1 2 3 4
1 5 4
------------
對比兩個數列的第一位,相同,不用變
------------
3 3 4
6 4
第一位不同,合并小的前兩位
----------
6 4
6 4
....
// 本題又讓我痛哭不已,類型開小了,本題最大有1e10左右,int不太行
C++
#include <iostream>
#include <vector>
using namespace std;int main()
{int n,m;cin>>n>>m;vector<int> a(n,0);vector<int> b(m,0);for(int i=0; i<n; ++i) cin>>a[i];for(int i=0; i<m; ++i) cin>>b[i];// sum_a a數列的前綴和// sum_b b數列的前綴和// cnt_a a的位數// cnt_b b的位數// cnt_sum 總共需要的次數long long sum_a=0,sum_b=0,cnt_a=0,cnt_b=0,cnt_sum=0;while(true){if(sum_a==sum_b){sum_a+=a[cnt_a++];sum_b+=b[cnt_b++];}else if(sum_a<sum_b){ // 第一個值小,需要合并sum_a+=a[cnt_a++];cnt_sum++;}else if(sum_b<sum_a){ // 第二個值小,需要合并sum_b+=b[cnt_b++];cnt_sum++;}if(cnt_a==n&&cnt_b==m) break;}cout<<cnt_sum;return 0;
}
Java
import java.util.Scanner;public class MergeArrays {public static void main(String[] args) {Scanner scanner = new Scanner(System.in);// 讀取兩個數組的長度int n = scanner.nextInt();int m = scanner.nextInt();// 初始化兩個數組int[] a = new int[n];int[] b = new int[m];// 讀取第一個數組的元素for (int i = 0; i < n; i++) {a[i] = scanner.nextInt();}// 讀取第二個數組的元素for (int i = 0; i < m; i++) {b[i] = scanner.nextInt();}// 初始化前綴和變量和指針long sum_a = 0, sum_b = 0; // 使用long避免大數溢出int cnt_a = 0, cnt_b = 0; // 數組a和b的當前指針位置int cnt_sum = 0; // 記錄合并次數// 循環處理直到兩個數組都遍歷完畢while (true) {// 當兩個前綴和相等時,移動到下一個元素if (sum_a == sum_b) {// 注意邊界條件:避免數組越界if (cnt_a < n) {sum_a += a[cnt_a++]; // 移動a的指針并累加值}if (cnt_b < m) {sum_b += b[cnt_b++]; // 移動b的指針并累加值}} else if (sum_a < sum_b) {// a的前綴和較小,需要合并下一個元素sum_a += a[cnt_a++]; // 合并a的下一個元素cnt_sum++; // 合并次數加1} else {// b的前綴和較小,需要合并下一個元素sum_b += b[cnt_b++]; // 合并b的下一個元素cnt_sum++; // 合并次數加1}// 檢查是否已經遍歷完兩個數組的所有元素if (cnt_a == n && cnt_b == m) {break;}}// 輸出最終的合并次數System.out.println(cnt_sum);scanner.close();}
}
五、數三角
問題描述
小明在二維坐標系中放置了?nn?個點,他想在其中選出一個包含三個點的子集,這三個點能組成三角形。然而這樣的方案太多了,他決定只選擇那些可以組成等腰三角形的方案。請幫他計算出一共有多少種選法可以組成等腰三角形?
輸入格式
輸入共?n+1n+1?行。
第一行為一個正整數?nn。
后面?nn?行,每行兩個整數?xixi?,?yiyi??表示第?ii?個點的坐標。
輸出格式
輸出共?11?行,一個整數。
樣例輸入
5 1 1 4 1 1 0 2 1 1 2
樣例輸出
4
樣例說明
一共有?44?種選法:?{3,4,5}{3,4,5}、{1,3,4}{1,3,4}、{5,2,3}{5,2,3}、{1,4,5}{1,4,5}。
評測用例規模與約定
對于?20%20%?的數據,保證?n≤200n≤200。
對于?100%100%?的數據,保證?n≤2000n≤2000,0≤xi,yi≤1090≤xi?,yi?≤109。
/*
? 本題要是直接3層暴力,肯定對,但是只能獲取60%的分!
? 所以要用上很多優化方式
? 如:根據半徑求解,叉乘(在本文下方有講解,也可上網搜)判是否三點在一條直線上
? 通過vector<map<ll,vector<int>>> 預處理分組。
? 最后用unordered_map存儲,O(1)
? 其實最開始,我也是有疑問的,這是怎么將O(n^3)優化到接近O(n^2)
? 畢竟預處理分組后下方仍有4層循環呢
? 其實畫個圖就好了。
? 沒優化之前,每個節點都要判斷(n^2)次,優化之后,每個節點僅需判斷分組過后的就行(哪怕是4層,其實有效的點不多,可近似成線性)。
*/
?C++
#include <iostream>
#include <vector>
#include <unordered_map>
using namespace std;#define ll long long // 因為不可能出現負數,直接開到最大(unsigned long long)
const ll N = 2e3+5;struct point{int x;int y;
}p[N]; // 定義節點-預處理ll get_radius(int i, int j){ // 半徑的平方return (p[i].x-p[j].x)*(p[i].x-p[j].x) + (p[i].y-p[j].y)*(p[i].y-p[j].y);
}bool is_parallel(int i, int j, int k){ // 用叉乘的方法,快速判斷,這一步不會的,可以上網查詢叉乘的作用,以及用法。ll v = (p[j].x-p[i].x)*(p[k].y-p[i].y)-(p[j].y-p[i].y)*(p[k].x-p[i].x); // i-j(p[j].x-p[i].x),(p[j].y-p[i].y) 與 i-k(p[k].x-p[i].x),(p[k].y-p[i].y)if(v==0) return true;else return false;
}int main(){int n;cin>>n;for(int i=0; i<n; ++i) cin>>p[i].x>>p[i].y;vector<unordered_map<ll,vector<int>>> vec(n); // 存// 預處理,只需要耗時O(n^2),即可分堆,避免3層暴力的大量重復計算for(int i=0; i<n; ++i){unordered_map<ll,vector<int>> m; // 用半徑的平方儲存,這樣就不用開方了for(int j=0; j<n; ++j){if(i==j) continue;ll len = get_radius(i,j);m[len].push_back(j); // 把這個點存進去}vec[i] = m; // 把map存入}// 最大的優點就是避免了許多無意義的重復計算int sum = 0;for(int i=0; i<n; ++i){ // 找到這個點,判斷其內部的存儲auto m = vec[i]; // 這個的出來的知識map結合,里面幼嫩多for(auto it=m.begin(); it!=m.end(); ++it){vector<int> p_m = it->second;if(p_m.size()<=1) continue; // 這種情況下,跳過for(int j=0; j<p_m.size(); ++j){for(int k=j+1; k<p_m.size(); ++k){if(is_parallel(i,p_m[j],p_m[k])) continue; // 共線sum++;}}}}cout<<sum<<endl;return 0;
}
Java
import java.util.*;
public class Main {public static void main(String[] args) {//輸出部分//讀取輸入的點,并統計每個點的出現次數Scanner sc = new Scanner(System.in);int n = sc.nextInt();//輸入點的數量int[] x = new int [n + 10];//存儲每個點的x坐標int[] y = new int [n + 10];//存儲每個點的y坐標HashMap<String, Integer> s = new HashMap<>();//統計每個點的出現次數//鍵是點的坐標,如("1 2")for(int i = 0; i < n; i++){x[i] = sc.nextInt();//輸入第i個點的x坐標y[i] = sc.nextInt();//輸入第i個點的y坐標String t = x[i] + " " + y[i];//將坐標拼接成字符串作為鍵s.put(t, s.getOrDefault(t , 0) + 1);//統計讀點的出現次數}//計算核心部分//對于每個點i,計算它與其他所有點j的距離,并將距離相同的點分組long res = 0;//最終結果for(int i = 0; i < n; i++){HashMap< Long,List<Integer> > map = new HashMap<>();//存儲距離相同的點的索引for(int j = 0; j < n; j++){if(i == j) continue;//跳過自己long d = (long)(Math.pow(x[i] - x[j], 2) + Math.pow(y[i] - y[j] , 2));//計算距離的平方List<Integer> list = map.getOrDefault(d, new ArrayList<>());//初始化列表list.add(j);//將點j加入對應的列表map.put(d,list);//更新map}//map是一個哈希表,鍵是距離的平方(d)值是一個列表,存儲所有與點i距離為d的點的索引//d是點i和點j之間的距離的平方(為了節省計算量,沒有開平方)for(long b : map.keySet()){List<Integer>list = map.get(b);//獲取距離為b的所有點res += (list.size() * (list.size() - 1)) /2;//統計點的數量long c = 0;for(int j : list){long x3 = 2 * x[i] - x[j], y3 = 2 * y[i] - y[j];//計算對稱帶點x與y的坐標if(s.containsKey(x3 + " " + y3)){//拼接成字符串c += s.get(x3 + " " + y3);//統計對稱點的出現次數}}res -= c/2;//減去重復統計的點對}}System.out.println(res);}
}
六、刪邊問題
問題描述
給定一個包含?NN?個結點?MM?條邊的無向圖?GG,結點編號?1...N1...N。其中每個結點都有一個點權?WiWi?。
你可以從?MM?條邊中任選恰好一條邊刪除,如果剩下的圖恰好包含?22?個連通分量,就稱這是一種合法的刪除方案。
對于一種合法的刪除方案,我們假設?22?個連通分量包含的點的權值之和分別為?XX?和?YY,請你找出一種使得?XX?與?YY?的差值最小的方案。輸出?XX?與?YY?的差值。
輸入格式
第一行包含兩個整數?NN?和?MM。
第二行包含?NN?個整數,W1,W2,...WNW1?,W2?,...WN?。
以下?MM?行每行包含?22?個整數?UU?和?VV,代表結點?UU?和?VV?之間有一條邊。
輸出格式
一個整數代表最小的差值。如果不存在合法的刪除方案,輸出??1?1。
樣例輸入
4 4 10 20 30 40 1 2 2 1 2 3 3 4
樣例輸出
20
樣例說明
由于?11?和?22?之間實際有?22?條邊,所以合法的刪除方案有?22?種,分別是刪除?(2,3)(2,3)?之間的邊和刪除?(3,4)(3,4)?之間的邊。
刪除?(2,3)(2,3)?之間的邊,剩下的圖包含?22?個連通分量:?{1,2}{1,2}?和?{3,4}{3,4},點權和分別是?3030、7070,差為?4040。
刪除?(3,4)(3,4)?之間的邊,剩下的圖包含?22?個連通分量:?{1,2,3}{1,2,3}?和?{4}{4},點權和分別是?6060、4040,差為?2020。
評測用例規模與約定
對于?20%20%?的數據,1≤N,M≤100001≤N,M≤10000。
對于另外?20%20%?的數據,每個結點的度數不超過?22。
對于?100%100%?的數據,1≤N,M≤2000001≤N,M≤200000,0≤Wi≤1090≤Wi?≤109,1≤U,V≤N1≤U,V≤N。
// 本題為tarjan算法的變種,不懂的,可以先搜一下基本用法(涉及圖的知識),本文最底部,也有優質視頻的鏈接
/*
? 其實本題不難,只要有圖的基礎就行--(能看懂答案的前提)
? 連通分量:圖的一個子圖,這個子圖中,任意兩點之間,都存在可達路徑
? 然后就是 tarjan 算法(懂得可以不用看,建議先看視頻,知道tarjan是啥)
? *
? ? 用dfn(發現時間戳)與low(最低可達祖先的發現時間戳)。確切的說,他倆都是個編號。
? ? 然后用cnt(count)這個設置時間戳。每次++。
? ? --以上是tarjan算法模版--
? ? 建立一個函數tarjan(n,fa) // n是現節點,fa是父節點,避免重復用的
? ? 然后,遞歸調用每個現階段子節點(大致會先將所有)
? ? 此時有三種情況
? ? ? 1、是未被遍歷過的新節點
? ? ? ? (這時可以繼續向下搜索,等回退到這里時,
? ? ? ? (更新一下low值,若果現節點的dfn小于子節點的low值(dfn<low),代表連通兩節點的邊能被割去
? ? ? ? (為了計數,可以在維護一個w集合,用于儲存以本節點為結尾的總和
? ? ? 2、遍歷到了父節點
? ? ? ? (可以毫不猶豫的退回了)
? ? ? 3、遍歷到了非父節點的舊節點
? ? ? ? (這個可是更新low值的好時候
? ? ? ? (是為了給,回溯時,判斷是否能構成聯通圖 ? ?做準備
? *
? // 拓展-從割邊問題 -> 割點問題
? (割點問題時,需要將low小于等于dfn(low<=dfn)
? (為啥<=中,多了個等于?因為一旦刪掉這個點,整個鏈子就會斷開,Java解析下方有圖解
*/
C++
#include <iostream>
#include <cmath>
#include <vector>
#define ll long long
using namespace std;
const ll N = 1e6+5;
const ll maxn = 0x3f3f3f3f3f3f3f3f; // 定義“偽最大值”
ll n,m,sum_value=0,cnt=0,ans=maxn; // sum_value總和,cnt計數器vector<ll> dfn(N,0),low(N,0);
vector<int> vec[N]; // 定義鄰接表
vector<ll> value(N,0); // 每個節點的權值
vector<ll> node_sum(N,0); // 回退回來的節點總和void tarjan(int u, int fa){ // 現節點u、父節點fadfn[u]=low[u]=++cnt;for(int v:vec[u]){if(dfn[v]==0){ // 沒遍歷過tarjan(v,u);low[u] = min(low[v],low[u]);if(dfn[u]<low[v]) ans=min(ans,abs(sum_value-2*value[v]));value[u] += value[v];}else if(v!=fa){low[u]=min(low[u],low[v]);}}
}void slove(){ // 作為中轉站cin>>n>>m;for(int i=1; i<=n; ++i) cin>>value[i],sum_value+=value[i];for(int i=0; i<m; ++i){ // 將無向圖存入int r1,r2;cin>>r1>>r2;vec[r1].push_back(r2);vec[r2].push_back(r1);}// 現在啥都有了tarjan(1,0);if(value[1]!=sum_value) cout<<abs(sum_value-2*value[1])<<endl;else if(ans!=maxn) cout<<ans<<endl;else cout<<-1<<endl;
}int main(){slove();return 0;
}
Java
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;public class Main {// 定義常量 N 作為數組大小static final long N = (long) (1e6 + 5);// 定義“偽最大值”static final long maxn = 0x3f3f3f3f3f3f3f3fL;// 節點數量 n,邊的數量 mstatic long n, m;// 所有節點權值總和static long sum_value = 0;// 時間戳計數器static long cnt = 0;// 存儲最終結果static long ans = maxn;// 存儲每個節點的發現時間戳static List<Long> dfn = new ArrayList<>();// 存儲每個節點能回溯到的最早祖先的發現時間戳static List<Long> low = new ArrayList<>();// 鄰接表,存儲圖的結構static List<List<Integer>> vec = new ArrayList<>();// 存儲每個節點的權值static List<Long> value = new ArrayList<>();// Tarjan 算法核心函數static void tarjan(int u, int fa) {// 初始化當前節點的發現時間戳和最早祖先時間戳dfn.set(u, ++cnt);low.set(u, cnt);// 遍歷當前節點的所有鄰接節點for (int v : vec.get(u)) {if (dfn.get(v) == 0) { // 如果鄰接節點未被訪問過// 遞歸調用 Tarjan 算法處理鄰接節點tarjan(v, u);// 更新當前節點的最早祖先時間戳low.set(u, Math.min(low.get(u), low.get(v)));// 如果當前節點的發現時間戳小于鄰接節點的最早祖先時間戳,說明該邊是割邊if (dfn.get(u) < low.get(v)) {ans = Math.min(ans, Math.abs(sum_value - 2 * value.get(v)));}// 將鄰接節點的權值累加到當前節點value.set(u, value.get(u) + value.get(v));} else if (v != fa) { // 如果鄰接節點已被訪問且不是父節點// 更新當前節點的最早祖先時間戳low.set(u, Math.min(low.get(u), low.get(v)));}}}// 主處理函數static void solve() {Scanner scanner = new Scanner(System.in);// 讀取節點數量和邊的數量n = scanner.nextLong();m = scanner.nextLong();// 初始化列表for (int i = 0; i <= n; i++) {dfn.add(0L);low.add(0L);value.add(0L);vec.add(new ArrayList<>());}// 讀取每個節點的權值并計算總和for (int i = 1; i <= n; i++) {value.set(i, scanner.nextLong());sum_value += value.get(i);}// 讀取邊的信息并構建鄰接表for (int i = 0; i < m; i++) {int r1 = scanner.nextInt();int r2 = scanner.nextInt();vec.get(r1).add(r2);vec.get(r2).add(r1);}// 從節點 1 開始進行 Tarjan 算法tarjan(1, 0);// 如果節點 1 的權值總和不等于所有節點的權值總和if (value.get(1) != sum_value) {System.out.println(Math.abs(sum_value - 2 * value.get(1)));} else if (ans != maxn) { // 如果找到了割邊System.out.println(ans);} else { // 沒有找到符合條件的割邊System.out.println(-1);}}public static void main(String[] args) {// 調用主處理函數solve();}
}
?拓展(割點):
?
七、AB路線
問題描述
小明拿了?nn?條線段練習抓娃娃。他將所有線段鋪在數軸上,第?ii?條線段的左端點在?lili?,右端點在?riri?。小明用?mm?個區間去框這些線段,第?ii?個區間的范圍是 [LiLi?,?RiRi?]。如果一個線段有?至少一半?的長度被包含在某個區間內,則將其視為被這個區間框住。請計算出每個區間框住了多少個線段?
輸入格式
輸入共?n+m+1n+m+1?行。
第一行為兩個正整數?nn,?mm。
后面?nn?行,每行兩個整數?lili?,?riri?。
后面?mm?行,每行兩個整數?LiLi?,?RiRi?。
輸出格式
輸出共?mm?行,每行一個整數。
樣例輸入
3 2 1 2 1 3 3 4 1 4 2 4
樣例輸出
3 2
評測用例規模與約定
對于?20%20%?的數據,保證?n,m≤103n,m≤103。
對于?100%100%?的數據,保證?n,m≤105n,m≤105,li<rili?<ri?,0<li,ri,Li,Ri≤1060<li?,ri?,Li?,Ri?≤106,max?{ri?li}≤min?{Ri?Li}max{ri??li?}≤min{Ri??Li?}.
/*
? ?審題 “小藍最少要走多少步?”,“最少”
? ?BFS就是解決圖-最短路徑的特效藥,
? ?DFS深搜也能搜到,但不一定是最短的,深搜更傾向于排列組合、解數獨、八皇后,這種嘗試性的算法。
? ?好了,確定本題的基調,就是廣搜
? ?在開始之前,還需考慮一個問題,就是暴力搜索必然會超時,因此,枝減操作,必不可少。也就是要引入記憶化搜索。
? ?這個時候,就要思考,用什么存儲記憶化搜索
? ?注意本題 "格子數,要是K的倍數,所以這里涉及到了k"
? ?-used[i][j][k]; 其中k來儲存走到這個格式,連續走的步數。
? ?步數相同時,代表該地址已經被走過。相同的符號,相同的步數,所以可以直接跳過。
? ?(注意,這里不用三維數組標記,是會超時的)。
? ?所以本題用queue廣搜、used[i][j][k]記憶化搜索、res[i][j][k]標記走的這個位置,用了多少步。
*/
C++?
#include <iostream>
#include <queue>
using namespace std;const int N = 1e3+5;
const int step = 1e1+5;
struct node{int x;int y;int step;
};
int n,m,k;
queue<node> q; // 用隊列,實現廣搜
bool used[N][N][step]; // 預處理
int res[N][N][step]; // 表示,每個節點,都在那個狀態走過
char grid[N][N];int xf[]={1,-1,0,0}; // 用來記錄方向的改變
int yf[]={0,0,1,-1};int BFS(){q.push({0,0,0}); // 將第一個節點存入while(!q.empty()){node u = q.front(); q.pop(); // 取出該節點if(u.x==n-1 && u.y==m-1) return res[u.x][u.y][u.step];for(int i=0; i<4; ++i){int X = u.x+xf[i], Y = u.y+yf[i], st = u.step+1;if(X<0||X>=n||Y<0||Y>=m) continue; // 出邊界if(st<k&&grid[X][Y]!=grid[u.x][u.y]) continue;if(st==k&&grid[X][Y]==grid[u.x][u.y]) continue;st = st%k; // 時刻更新著if(used[X][Y][st]) continue; // 都是更新之后,在遍歷的used[X][Y][st]=true; // 表示遍歷過res[X][Y][st]=res[u.x][u.y][u.step]+1; // 多走了一步q.push({X,Y,st});}}return -1; // 此時還沒有解釋,只能說明一件事,遍歷到頭了,沒有結果。
}int main(){// 基本處理cin>>n>>m>>k;string str;for(int i=0; i<n; ++i){cin>>str;for(int j=0; j<m; ++j) grid[i][j]=str[j];}cout<<BFS();return 0;
}
Java
import java.math.BigInteger;
import java.util.*;public class Main {static long INF = (long) 1e18;public static void main(String[] args) {Scanner sc = new Scanner(System.in); int n = sc.nextInt();int m = sc.nextInt();int k = sc.nextInt();sc.nextLine();char[][] mg = new char[n][m];for (int i = 0; i < n; i++) {mg[i] = sc.nextLine().toCharArray();}LinkedList<Pair> qu = new LinkedList<Pair>();qu.add(new Pair(0, 0, 1));int[][] d = new int[][] {{0,1},{1,0},{0,-1},{-1,0}};boolean[][][] visited = new boolean[n][m][2 * k];for (int step = 0; !qu.isEmpty(); step++) {int num = qu.size();for (int i = 0; i < num; i++) {Pair pair = qu.pollFirst();int px = pair.x, py = pair.y, pf = pair.flag;if (visited[px][py][pf]) {continue;}visited[px][py][pf] = true;if (pair.x == n - 1 && pair.y == m - 1) {System.out.print(step);return;}for (int j = 0; j < 4; j++) {int x = px + d[j][0];int y = py + d[j][1];int f = (pf + 1) % (2 * k);if (x >= 0 && x < n && y >= 0 && y < m) {if (visited[x][y][f]) {continue;}if (pf < k && mg[x][y] == 'A' || pf >= k && mg[x][y] == 'B') {qu.addLast(new Pair(x, y, f));}}}}}System.out.print(-1);}
}class Pair {int x, y, flag;public Pair(int x, int y, int flag) {super();this.x = x;this.y = y;this.flag = flag;}
}
八、抓娃娃
問題描述
小明拿了?nn?條線段練習抓娃娃。他將所有線段鋪在數軸上,第?ii?條線段的左端點在?lili?,右端點在?riri?。小明用?mm?個區間去框這些線段,第?ii?個區間的范圍是 [LiLi?,?RiRi?]。如果一個線段有?至少一半?的長度被包含在某個區間內,則將其視為被這個區間框住。請計算出每個區間框住了多少個線段?
輸入格式
輸入共?n+m+1n+m+1?行。
第一行為兩個正整數?nn,?mm。
后面?nn?行,每行兩個整數?lili?,?riri?。
后面?mm?行,每行兩個整數?LiLi?,?RiRi?。
輸出格式
輸出共?mm?行,每行一個整數。
樣例輸入
3 2 1 2 1 3 3 4 1 4 2 4
樣例輸出
3 2
評測用例規模與約定
對于?20%20%?的數據,保證?n,m≤103n,m≤103。
對于?100%100%?的數據,保證?n,m≤105n,m≤105,li<rili?<ri?,0<li,ri,Li,Ri≤1060<li?,ri?,Li?,Ri?≤106,max?{ri?li}≤min?{Ri?Li}max{ri??li?}≤min{Ri??Li?}.
// 聰明的你,一定用的暴力,聰明的你,一定超時o(* ̄▽ ̄*)ブ?
// 本題,用差分+前綴和,就能非常完美的解決問題
// 此外,本題預處理化的時候,一定要看清楚!
// 不要處理成2e5+5了,要開r、l、R、L。而不是n,m;
C++
#include <iostream>
#include <vector>
const int N = 2e6+5;
using namespace std;int main()
{int n,m;cin>>n>>m;vector<int> res(N,0);for(int i=0,r,l; i<n; ++i) cin>>l>>r,res[r+l]++; for(int i=1; i<N; ++i) res[i]+=res[i-1];for(int i=1,L,R; i<=m; ++i){cin>>L>>R;L*=2,R*=2;if(L==0) cout<<res[R]<<endl;else cout<<res[R]-res[L-1]<<endl;}return 0;
}
Java
import java.util.Scanner;public class SegmentQuery {public static void main(String[] args) {// 定義常量 N,用于數組大小final int N = 2000005;Scanner scanner = new Scanner(System.in);// 讀取 n 和 m 的值int n = scanner.nextInt();int m = scanner.nextInt();// 創建長度為 N 的數組 res 并初始化為 0int[] res = new int[N];// 讀取 n 條線段的左右端點信息for (int i = 0; i < n; i++) {int l = scanner.nextInt();int r = scanner.nextInt();// 對線段中點對應的數組元素加 1res[l + r]++;}// 構建前綴和數組for (int i = 1; i < N; i++) {res[i] += res[i - 1];}// 處理 m 個查詢區間for (int i = 1; i <= m; i++) {int L = scanner.nextInt();int R = scanner.nextInt();// 將查詢區間的左右端點乘以 2L *= 2;R *= 2;int result;// 處理左端點為 0 的邊界情況if (L == 0) {result = res[R];} else {result = res[R] - res[L - 1];}// 輸出結果System.out.println(result);}scanner.close();}
}
后面的兩道題,咱時間有限,就先跳過啦(~ ̄▽ ̄)~
等后續打國賽時,在拐回來寫寫。
當然大家有好的代碼、解析,也可以發給我,讓我瞅瞅。( ?? ω ?? )?,我貼上去。
知識點:
一、向量、點乘、叉乘
(AI整理)
1、向量:
由來:
早在古希臘時期,數學家與物理學家已經意識到某些量(如力、速度)兼具大小和方向。此時
已經蘊含了向量,但此時并未形成明確的概念與理論。
17世紀笛卡爾創建解析幾何后,點的位置可用坐標來表示,線段的長度和方向可通過坐標差量化。
這為向量的代數表達,奠定了基礎。
后來經過負數、四元數的啟發、線性擴張論...等幾代人努力與知識的迭代,向量才最終別確定下來。
定義:
?
基本屬性:
?
常見運算:
?
2、點乘:
不要問,為啥叫點乘,這是一個非常可愛的問題 --> 因為兩向量之間用 “點號” 相乘。
定義:
兩向量之間相乘,結果為標量。
?
應用:
- 判斷兩條邊之間,是鈍角(負數)、直角(0)、還是銳角(正數)
- 一條邊在另一條邊上映射的長度。
- 計算力的做功。
3、叉乘:
也不要問,為啥叫叉乘,這是一個非常可愛的問題 --> 因為兩向量之間用 “叉號” 相乘。
二維:
?
三維:
?應用:
切記,判斷點時,叉乘邊的方向很重要。點的那條邊,放在第二個。
求平行四邊形與三角形的面積:
二維 兩向量叉乘,求的是平行四邊形的面積。除于2,求的是三角形。
點與直線的關系:
?
線段相交的檢測:
?
點與直線關系,詳解!!?
二、浮點數比較
在編程中,通常不用==號,來進行浮點數比較。因為會出現精度誤差。即便在數學中相等,在計算機中也不一定相等。
abs(a-b)<1e-6
通常用小于1e-6來測試差值。
1e-6在通常情況下是夠用的,
它既不是那么嚴格(把本應該相同的數,判為不同)
它也不是那么寬松(把本不同的數,判為相同)
三、map與unordered_map
map底層為紅黑樹,unordered_map底層為哈希表
存or取,map(O(logN))的效率皆低于unordered_map(O(1))。
四、極大值(32位、64位、16進制)
INT32_MAX
?是 32 位有符號整數的最大值,為?2,147,483,647。(2.15 × 10?)0x3f3f3f3f3f3f3f3f:
轉換為十進制為:1,082,367,756,170,655,743。(約 1.08 × 101?)- INT64_MAX:約 9.22 × 101?。
INT32_MAX是32位,標準最大值。
INT64_MAX是64位下,標準最大值。
0x3f3f3f3f3f3f3f3f,常常被歸納于“偽最大值”,它即起到最大值的作用,又能適當的相加。在圖、樹中,常被賦予權值,表示不可抵達的點。
五、廣搜(BFS)與深搜(DFS)
廣搜:
(隊列)
- 最短路徑問題,常用于判斷最短路徑問題。
- 圖或樹的層序遍歷問題。
- 判斷連通性。
深搜:
(遞歸、棧)
- 路徑問題,尋找某一個點到另一個點的路徑,可能不是最短路徑。
- 回溯問題,可用于某些需要試錯的算法(解數獨、八皇后)
- 求解組合問題(全排列、組合等問題)DFS可以遍歷所有有解空間。
- 拓撲排序,暫時還不熟悉
六、vector的比較方案-map
在C++中,兩個vector比較,是通過operate==,進行比較的。
先比較數組大小,在依次、逐個比較具體內容。
map可以以vector為鍵
std::map 基于紅黑樹(一種自平衡的二叉搜索樹)。會通過排序確定位置。(operate<)
且vector已經重載過(operate <) 了,比較時,自動按照字典序比較。
unordered_map不可以以vector為鍵
鍵的要求是
1、能滿足 哈希函數 轉化為 哈希值。
2、能通過operate== 進行比較
雖然vector中定義了operate==,但是沒有定義,哈希函數。
(總結:map都具備,unordered_map不能將vector轉化為哈希函數)
借鑒視頻、博客:
1、[算法]輕松掌握tarjan強連通分量?- 圖