快速存儲和查找字符串集合 字符類型統一,將單詞在最後一個字母結束的位置上打上標記java
練習題:Trie字符串統計算法
import java.util.*; public class Main{ static int N = 100010; static int[][] son = new int[N][26]; static int[] con = new int[N]; static int idx =0; static char[] str = new char[N]; // 插入操做 public static void insert(char[] str){ // 初始化根節點 int p = 0; // 遍歷字符串的每一個字符 for(int i = 0; i < str.length; i++){ //將字母映射爲數組 'a'-->97; int u = str[i] -'a'; //若是子節點沒有 及:son[p][u] ==0; //那麼 添加節點 if(son[p][u] == 0) son[p][u] = ++idx; //更新節點位置 p = son[p][u]; } //統計最後以str[p]這個結尾的字母 con[p]++; } // 查找操做 public static int select(char[] str){ //初始化根節點 int p = 0; for(int i = 0; i < str.length; i++){ int u = str[i] - 'a'; //若是子節點==0;表明沒有所查找的字母,及沒有該單詞,返回0次 if(son[p][u] == 0) return 0; p = son[p][u]; } //最後遍歷完成後p就是以str[p]的字母 //返回單詞的次數 return con[p]; } public static void main(String[] args){ Scanner sc = new Scanner(System.in); int n = sc.nextInt(); while(n-- != 0){ String common = sc.next(); String str = sc.next(); if(common.equals("I")){ //toCharArray() -->將字符串轉爲字符數組 insert(str.toCharArray()); }else if(common.equals("Q")){ System.out.println(select(str.toCharArray())); } } } }
最大異或對:數組
暴力方法:markdown
public class Main{ static int res = 0; public static void main(String[] args){ for(int i = 0; i < n; i++){ //枚舉第一個數 for(int j = 0; j < i; j++){ //枚舉第二個數 res = Math.max(res,a[i] ^ a[j]); } } System.out.println(res); } }
使用Trie樹來作:數據結構
import java.util.*; public class Main{ static int N = 100010,M = 3000000; static int[] a = new int[N]; //樹的長度最多不超過N*31,節點爲0,1; static int[][] son = new int[M][2]; static int idx; // 建立Trie public static void insert(int a){ int p = 0; for(int i = 30; i >= 0; i--){ // 判斷a的二進制位的第i個數是0仍是1; int s = a >> i & 1; if(son[p][s] == 0) son[p][s] = ++idx; p = son[p][s]; //把當前節點移動到下一節點; } } // 查詢 public static int query(int a){ int p = 0,res = 0; for(int i = 30; i >= 0; i--){ //判斷數字a在第i位的二進制是0仍是1; int s = a >> i&1; //要想使得a^x最大,那麼x要最小,也就是x得二進制與a二進制相反才行 //判斷子節點與當前a中二進制相反得分支存不存在(不存在 son[p][1-s] ==0); if(son[p][1-s] != 0){ //二進制轉十進制操做 //原:3-->110,查:001; index:二、一、0; //查到一位轉換成十進制相加即等於最後與原數異或爲最大值 res = res + (1<< i); //更新p位置,即往下走下一節點(相反得一條路) p = son[p][1-s]; //查找得分支點沒有得話,只能走已存在得分支; }else p = son[p][s]; } //返回結果 return res; } public static void main(String[] args){ Scanner sc = new Scanner(System.in); int n = sc.nextInt(); int res = 0; for(int i = 0; i < n; i++){ //初始化數組 a[i] = sc.nextInt(); insert(a[i]); } //遍歷數組中全部得元素,找到數組中異或得最大得數; for(int i = 0; i < n; i++){ //query返回得使a[i]^x最大得值 res = Math.max(res,query(a[i])); } System.out.println(res); } }
問題彙總:函數
開的son中,第一個取得M是什麼含義。 Trie樹得深度不是31嗎,那隻開31個空間不久好了嗎?
答:優化
son的第一維度存的是trie數一共有多少節點,若是是存儲一個數的話,確實開31個空間就行了,可是存儲的是N = 100000個數,每一個數循環31次,那就是31*100000 = 310w,由於會有複用的節點,用不上這麼多,300w就能夠了
問:指針
int 是32位的,請問在對一個數進行遍歷,判斷該位是否爲1時,爲啥是從30~0;不用關心第31位嗎?
答:code
題目中規定了 0 ≤ Ai <2^31,因此循環到30就夠了
問:排序
什麼是從i=30開始而不是0
答:
由於是求最大值,因此從最高位開始比較,要有限保證最高位爲1
一、 將兩個集合合併
二、 詢問兩個元素是否再一個集合當中
基本原理:每一個集合用一顆樹來表示,樹根的編號就是整個集合的編號,每一個節點存儲它的父節點,p[x]表示x的父節點;
問題一:如何判斷樹根:if(p[x] == x) x就是樹根
問題二:如何求x的集合編號:while(p[x] != x) x = p[x]---->包含路徑壓縮算法(優化)
問題三:如何合併兩個集合:px是x的集合編號,py是y的集合編號,p[x] = y
核心操做:
public static void find(int x){ // 返回x的祖宗節點 + 路徑壓縮 if(p[x] != x) p[x] = find(p[x]); return p[x]; }
import java.util.*; public class Main{ // 每一個集合用樹來存儲 static int N = 100010; // 創建父節點數組 static int[] p = new int[N]; public static void main(String[] args){ Scanner sc = new Scanner(System.in); int n = sc.nextInt(); int m = sc.nextInt(); // 初始化父節點數組 for(int i = 1; i <= n; i++) p[i] = i; while(m-- != 0){ char s = sc.next().charAt(0); int a = sc.nextInt(); int b = sc.nextInt(); if(s == 'M'){ //將a根節點的父節點指向b的根節點--》實現兩個集合的合併 p[find(a)] = find(b); }else{ // 若是a的根節點等於b的根節點-->在同一個集合中 if(find(a) == find(b)) System.out.println("Yes"); else System.out.println("No"); } } } // 返回x的根節點 public static int find(int x){ // 若是父節點不等於根節點,則遞歸尋找 if(p[x] != x) p[x] = find(p[x]); //返回x的所在的根節點 return p[x]; } }
import java.util.*; public class Main{ static int N = 100010; // 每一個集合 static int[] p = new int[N]; // 每一個集合的大小 static int[] size = new int[N]; // 返回集合(x)的跟節點 public static int find(int x){ if(p[x] != x) p[x] = find(p[x]); return p[x]; } public static void main(String[] args){ Scanner sc = new Scanner(System.in); int n = sc.nextInt(); int m = sc.nextInt(); // 對每一個集合以及集合大小進行初始化 for(int i = 0; i < n; i++){ p[i] = i; size[i] = 1; } while(m -- != 0){ String s = sc.next(); if(s.equals("C")){ int a = sc.nextInt(); int b = sc.nextInt(); //不加判斷,當a與b集合相同時, 執行了本身加本身,不符合題意,須要特判 if(find(a) != find(b)){ // 合併後的連通塊數量(有多少個點) size[find(b)] += size[find(a)]; // a集合的父節點執行b集合 p[find(a)] = find(b); } } else if(s.equals("Q1")){ int a = sc.nextInt(); int b = sc.nextInt(); if(find(a) == find(b)) System.out.println("Yes"); else System.out.println("No"); } else{ int a = sc.nextInt(); // 查詢a所在集合的連通塊大小,找到a的根,並統計根的數量 System.out.println(size[find(a)]); } } } }
堆
:就是一個用一維數組來表示一個徹底二叉樹的這麼一個數據結構。所謂二叉樹就是一種樹,每個父節點,有最多兩個子節點,通常叫作左右子樹
完美二叉樹:是一個二叉樹層數爲k的時候,它的元素數量等於2k-1
而一個徹底二叉樹能夠理解爲是一個完美二叉樹缺乏一部分或者不缺乏一部分的二叉樹,可是內容必定是從上到下,從左到右的填充,也就是缺乏的部分總在右邊;
小根堆: 即根節點小於等於它的左孩子,也小於等於它的右孩子,且每一個點都小於左右子節點;左孩子是左邊集合的最小值,右孩子是右邊的最小值
根節點是左右孩子的最小值--->推論出:根節點爲堆的最小值
如何手寫一個堆:
size-->表示堆的大小;
插入一個數:
heap[++size] = x; up(size);
求集合當中的最小值
heap[1];
刪除最小值:
heap[1] = heap[size]; size--; down(1);
刪除任意一個元素:
heap[k] = heap[size]; size--; down(k); up(k);
修改任意一個元素:
heap[k] = x; down(k); up(k);
步驟:(輸出前m個的最小值)
其中down操做的實現過程:
比較三個點的最小值,若是不符合堆的定義那麼就交換、遞歸執行down操做
import java.util.*; import java.io.*; public class Main{ static int N = 100010; // 定義堆 static int[] h = new int[N]; // 肯定堆的大小 static int size; // 當數在三個數中大時,使數往下沉 public static void down(int u){ // 設三個數的最小值爲t; int t = u; // u*2爲 u的左兒子; u*2+1 爲u的右兒子; // 若是左兒子的下標小於堆的大小,則表示存在這個點; // 而且左兒子值比最小t的值小,則將t指向左兒子 if(u*2 <= size && h[u*2] < h[t]) t = u * 2; // 若是右兒子的下標小於堆的大小,則表示存在這個點; // 而且右兒子值比最小t的值小,則將t指向右兒子 if(u*2+1 <= size && h[u*2+1] < h[t]) t = u*2+1; // 若是最後t的最小值不是本身(u); // 那麼交換兩個下標所在的值;交換完在down一下,防止破壞堆結構; if(t != u){ int temp = h[u]; h[u] = h[t]; h[t] = temp; down(t); } } public static void main(String[] args) throws IOException{ BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out)); String[] s = br.readLine().split(" "); int n = Integer.parseInt(s[0]); int m = Integer.parseInt(s[1]); String[] str = br.readLine().split(" "); // 初始化堆 for(int i = 1; i <= n; i++){ h[i] = Integer.parseInt(str[i-1]); } // 設置堆的大小 size = n; //經過遞推可獲得時間複雜度:建堆-->時間複雜度爲O(n) for(int i = n/2;i != 0;i--){ down(i); } while(m-- != 0){ // 輸出最當前最小值,也就是堆頂 bw.write(h[1]+" "); // 將最後的值覆蓋掉堆頂--就是刪掉堆頂 h[1] =h[size]; // 堆大小-- size--; // 在把堆頂down一下 找出最小值; down(1); } bw.flush(); br.close(); bw.close(); } }
實現up操做:
u/2爲u的父節點,h[u] < h[u/2]-->子節點小於父節點;交換完後,up(u父節點的父節點);
首先堆是徹底二叉樹:(如下是編號)
1 2 3 4 5 6 7
2 / 2 = 1, 3 / 2 = 1.
4 / 2 = 2, 5 / 2 = 2, 6 / 2 = 3, 7 / 2 = 3
經過上面操做就能找到父節點;
public static void up(int u){ if(u / 2 > 0 && h[u] < h[u / 2]){ heapSwap(u, u / 2); up(u/2); } }
import java.util.*; import java.io.*; public class Main{ static int N = 100010; static int[] h = new int[N]; static int[] ph = new int[N]; //存放第k個點的值的下標 static int[] hp = new int[N]; //存放隊中點的值是第幾個插入的 static int size; //size 記錄的是堆當前的數據多少 public static void down(int u){ int t = u; if(u*2 <=size && h[u*2] < h[t]) t = u*2; if(u*2+1 <= size && h[u*2+1] < h[t]) t = u*2+1; if(t != u){ heap_swap(u,t); down(t); } } public static void up(int u){ if(u / 2 > 0 && h[u] < h[u / 2]){ heap_swap(u,u/2); up(u/2); } } public static void heap_swap(int u,int v) { // 相對照 // swap(h[u],h[v]); //值交換 // swap(hp[u],hp[v]); //堆中點的插入順序(編號)交換 // swap(ph[hp[u]],ph[hp[v]]); //對編號第h[u] h[v]的值交換 swap(h,u,v); swap(hp, u, v); swap(ph, hp[u], hp[v]); } public static void swap(int[] a, int u, int v){ int tmp = a[u]; a[u] = a[v]; a[v] = tmp; } public static void main(String[] args) throws Exception{ BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out)); int n = Integer.parseInt(br.readLine()); size = 0; int m = 0; while(n-- != 0){ String[] s = br.readLine().split(" "); String op = s[0]; if("I".equals(op)){ int x = Integer.valueOf(s[1]); m++; h[++size]=x; // ph[m]=size; // hp[size]=m; // 創建映射關係即:第m個插入的數的編號是size; ph[m] = size; //第size編號下的是第m個插入的數; hp[size] = m; // 將插入的數向上調整 up(size); }else if("PM".equals(op)) bw.write(h[1]+"\n"); else if("DM".equals(op)){ heap_swap(1,size); size--; down(1); }else if("D".equals(op)){ int k = Integer.parseInt(s[1]); int u=ph[k]; //這裏必定要用u=ph[k]保存第k個插入點的下標 heap_swap(u,size); //由於在此處heapSwap操做後ph[k]的值已經發生 size--; //若是在up,down操做中仍然使用ph[k]做爲參數就會發生錯誤 up(u); down(u); }else if("C".equals(op)){ int k = Integer.parseInt(s[1]); int x = Integer.parseInt(s[2]); h[ph[k]]=x; //此處因爲未涉及heapSwap操做且下面的up、down操做只會發生一個因此 down(ph[k]); //因此可直接傳入ph[k]做爲參數 up(ph[k]); } } bw.flush(); br.close(); bw.close(); } }
求質數:
import java.util.Scanner; public class 求質數 { public static void main(String[] args) { for(int i = 100000;;i++){ boolean flag = true; for(int j = 2; j* j <= i; j++){ if(i % j == 0){ flag = false; break; } } if(flag){ System.out.println(i); break; } } } }
import java.util.*; import java.io.*; public class Main{ //h[]是哈希函數的一維數組 //N爲數據範圍外的最小質數 //模N這個數通常要取成質數且離2的整次冪儘量的遠---減小哈希衝突的機率 static int N = 100003; static int[] h = new int[N]; //e[]是鏈表中存的值 static int[] e = new int[N]; //ne[]是指針存的指向的地址 static int[] next = new int[N]; //idx是當前指針 static int idx; // 插入操做 public static void insert(int x){ //對負數的處理,k是哈希值 //若是x%N的餘數爲零,+N則必定爲正數,而後再取模 int k = (x % N + N) % N; // 單鏈表的實現 //頭插法 e[idx] = x; next[idx] = h[k]; h[k] = idx++; } // 查找元素是否存在 public static boolean find(int x){ int k = (x% N + N) % N; for(int i = h[k]; i != -1; i=next[i]){ //找到返回true if(e[i] == x) return true; } return false; } public static void main(String[] args) throws IOException{ BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); int n = Integer.parseInt(br.readLine()); //初始化h[] for(int i=0;i<N;i++){ h[i]=-1; } while(n-->0){ String[] s = br.readLine().split(" "); int x = Integer.parseInt(s[1]); if(s[0].equals("I")){ insert(x); }else{ if(find(x))System.out.println("Yes"); else System.out.println("No"); } } } }
好處:只開一個數組便可;
package ACWing.數據結構與算法; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; public class 開放尋址法_哈希表 { //通常開到範圍的兩到三倍 //N的值也須要取模判斷一下 static int N= 200003; static int[] h = new int[N]; //設當前值不在題目給出的範圍中,表示當前無數據--空數據 static int bound = (int)(1e9+1); public static int find(int x){ int k = ( x % N + N) % N; //若是當前空間有數據,且該空間的數據不等於咱們要查找的數據 //那麼繼續往下尋找 while(h[k] != bound && h[k] != x){ k++; if(k== N) k = 0; } //返回的狀況有兩種 /* 一、k指代的是當前的空間沒有人 二、k指代的是當前的空間有人且就是咱們要查找的元素 */ return k; } public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); int n = Integer.parseInt(br.readLine()); for(int i = 0; i < N; i++) h[i] = bound; while(n-- != 0){ String[] s = br.readLine().split(" "); String op = s[0]; int x = Integer.parseInt(s[1]); int k = find(x); if(op.equals("I")){ h[k] = x; }else{ //若是當前元素不爲空 if(h[k] != bound) System.out.println("Yes"); //爲空 else System.out.println("No"); } } br.close(); } }
怎麼求前綴和的哈希值:
將字符串當作P進制的數:
例:求ABCD的哈希值
當作P進制的話,該字符串有四位:
若是數據量太大的話,咱們須要mod上一個Q,經過取模能夠映射到(0-Q-1)上的數
注意:
不能映射爲0,由於0與任何數進行運算都爲零,這樣會形成數據重複;
當映射的時候一定會出現兩個不一樣的數取模成相同的數
解決的方法:假定人品足夠好,不會出現衝突,且經驗取值爲:P=131或者13331,Q=2^64次方的時候,能夠避免99.99%的衝突;
求哈希值:
h[]數組表示前綴和的哈希值;
h[R] : 1-R的前綴和hash值
h[L-1] : 1-(L-1)的前綴和hash值
在h[R]中:
在h[L-1]中:
咱們的目標時求出L-R的前綴和哈希值,如下看圖說話
對於區間和公式的理解:h[l,r]=h[r]−h[l−1]×P^(r−l+1)
求L-R 也就是求D-E,也就是求4-5
123*100= 12300;12345-12300 = 45;
ABC*100 = ABC00; ABCDE-ABC=DE;
h[R]-h[L-1]*P^(R-1-(L-2));
h[R] - h[L-1]*P^(R-L+1)
解題步驟:
字符串前綴哈希法:
import java.util.*; import java.io.*; public class Main{ static int N = 100010; static int h[] = new int[N]; static int p[] = new int[N]; static int P = 131; public static void main(String[] args) throws IOException{ BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out)); String[] s = br.readLine().split(" "); //輸入長度爲n的字符串 int n = Integer.parseInt(s[0]); //m次詢問 int m = Integer.parseInt(s[1]); String str = br.readLine(); p[0] =1; //必定不要忘記設置爲1; for(int i = 1; i <= n; i++){ //預處理保存每位的權值 p[i] = p[i-1]*P; //獲取前綴和哈希值 h[i] = h[i-1]*P+str.charAt(i-1); } while(m-- != 0){ String[] s1 = br.readLine().split(" "); int l1 = Integer.parseInt(s1[0]); int r1 = Integer.parseInt(s1[1]); int l2 = Integer.parseInt(s1[2]); int r2 = Integer.parseInt(s1[3]); if(getHash(l1,r1) == getHash(l2,r2)) bw.write("Yes"+"\n"); else bw.write("No"+"\n"); } bw.flush(); br.close(); bw.close(); } public static long getHash(int l,int r){ return h[r] - h[l-1]*p[r-l+1]; } }