算法 {算法经验技巧}
@LOC: 1
汇总
#枚舉不同字符個數為K
的子字符串#
@MARK: @LOC_0
;
字符串S, 從[l]
開始 找一個S[l...r]
使得其不同字符個數是K
且盡可能的長;
.
比如S = "abbabc", l=0, K=2
, 那麼答案是"abbab"
;
看一個超時做法:
令Rig[i]: 最長的S[i...r]都是相同的 則Rig[i]=r
; 然後從l
開始 用一個set<char>
來維護 通過Rig[i]
跳到下一個不同字符位置; 然後你可能認為 她的時間複雜度是K
即最多跳K次;
這是錯誤的, 比如S = "abababababab..."
, 其實他是O(N)
的, 你用這個Rig
其實沒有用;
正確做法是:
令vector<int> Pos[26]; Pos[i]=[....] 表示i這個字符所有出現的位置
, 對於l
這個位置 遍歷所有26個字符 求他的lower_bound( l)
位置 放到inds
裡面(inds
最長是26), 然後對inds
排序 找第K
個元素x
那麼[l...x)
就是答案;
.
比如S = "abbabc", l=0, K=2
, 那麼inds = [0,1,5]
所以inds[K]=5
因此[0...4]
是答案;
@DELI;
#給定兩個同長度的字符串AB, 每次問A[l...r] == B[l...r]
#
用字符串哈希可以, 但因為我們每次比較的 都是相同的下標 即不會比較A[1...3] == B[4...8]
而是對應位置 即A[1...3] == B[1...3]
, 所以有一個更簡單的做法; 我們令Same[i] = (A[i] == B[i])
, 然後利用前綴和算法 求出[l...r]
裡面 是否有Same[i]==0
的下標;
@DELI;
#BFS判重的時機#;
while( !que.empty()){
auto cur = que.front(); que.pop();
//>< @MARK: @LOC_0
for( nex){
if( !Vis[nex]){
que.push( nex);
//>< @MARK: @LOC_1
}
}
}
我們設置一個Vis
, 表示所有BFS訪問過的點(即進入過que
的點);
那麼對於Vis[] = 1
這個操作, 你是放在@LOC_0
? 還是放到@LOC_1
?
必須放到@LOC_1
, 否則你的程序會超時;
.
比如, a,b
都會訪問到c
, 那麼會導致 c
多次入隊列, 也就是 隊列裡 會有很多重複的點; 再比如, a->b
有很多重邊 這也會導致b
會多次入隊列;
也就是 在進行que.push(x)
的時候, 設置Vis[x] = 1
;
@DELI;
5e8 = 1s
, 即FOR_( i, 1, 5e8){ ++c;}
这个代码的耗时是1s
;
{全局數組,構造,析構函數} 都是佔運行時間的
@LINK: (https://editor.csdn.net/md/?articleId=133793114)-(@LOC_0)
;
而且, 全局数组 和 动态new
, 差不多; 即全局数组T A[100005]
和 你程序里进行1e5次的 new T
, 效率差不多;
用const char *
取代string
来传参
你函數參數接收一個字符串 不要寫成string
而是要寫成const char * _s, int _len
; (比如字符串哈希模板裡的函數 這是這麼設計的);
這能極大的優化程序, 因為對於一個字符串S, 你取他的子串[l,r]
, 如果用substr()
傳參 這是深拷貝 效率非常低, 最好就是用S.data() + l(指針), r-l+1(長度)
這個效率非常高;
連續量轉換為離散量
由於計算機存儲 一定是離散量, 比如說(1,2)
這個區間 計算機是不可能給存儲下來的; 因此就需要把他做離散化的處理;
比如說 你操作的元素 是[-1,0] [0,1] [1,2] ...
這些區間 即[l,l+1]
, 那麼 你可以將一個區間[l,l+1]
哈希為l
這個點, 也就是x
這個哈希點 就對應[x, x+1]
這個區間;
對於二維單位正方形 可以把(x,y)-(x+1,y+1)
這個正方形 哈希為(x,y)
這個點;
再比如用一個矩陣去覆蓋若干個點, 如果說 點都是整數點(即坐標都是整數), 那麼 答案矩陣的左下角 一定可以在整數點位置上, 否則的話 你要枚舉的方案就太多了 因為涉及到整個浮點數域;
這在動態掃描線算法 經過會用到這種映射方法;
比如有若干個點Ai
(可以是浮點數), 然後對於[A0, A1, A2, A3, ..., An]
這些點(遞增的), 假如你操作的最小單元 就是一個[Ai, A(i+1)]
這個區間, 即你操作的最小單元是[A0,A1] [A1,A2] [A2,A3] ...
這些區間, 那麼由於Ai
是浮點數 你可以做以下的離散化處理:
.
做離散化哈希Ai->i
, 即此時是[0, 1, 2, ...., n]
, 那麼原來的 你的最小操作單元 [Ai, A(i+1)]
, 此時就對應為[i, i+1]
這個區間; 此時 還要繼續進行哈希 因為[i,i+1]
這個區間 我們無法進行維護, 把這個區間 再哈希為[i]
這個點, 也就是原來是A0...,An
這些點, 他有n
個區間 每個區間哈希為0,1,2,...,n-1
這些最終點, 即對於最終點 比如n-1
這個點 他對應[A(n-1), An]
這個區間;
.
這種哈希 他是等距的; 比如說 A0 = -3.4, An = 13.5
, 那麼比如你要獲取所有區間的長度 即[A0,A1] ... [A(n-1), An]
這些區間長度之和, 因為這些區間就對應 0..., n-1
這些點, 他的區間之和為 A[n-1 + 1] - A[0] = 13.5 - (-3.4)
, 即你要獲得 第l,...,r
個區間的長度之和 他等於A[r + 1] - A[l]
(比如l==r==0
即你要獲得第0個區間[A0, A1]
這個區間的長度 他等於A1 - A0
);
N*M <= 1e6
的矩陣T[N][M]
由於N,M
的值 是不固定的, 只是知道N*M
的最大值, 此時N
可能為1e6
, M
也可能是1e6
;
那麼 你如果寫成T A[1e6][1e6]
這肯定是爆內存的;
如果用vector( N, vector(M))
, 那麼會消耗額外的內存(即存儲vector
) 而且效率也沒有樸素的數組下標好(因為vector
的下標 是調用了重載運算符函數);
正確做法是: T A[ 1e6]
然後對於(x,y)
轉換為一維x * M + y
;
vector
的硬性超時問題
即便你不涉及到內存動態增長, 他也可能會超時 而使用樸素的數組 就不會超時, 原因有2個:
1(取下標): 普通數組進行A[i]
操作 他效率是非常高的 他就是直接去取內存地址, 而容器的下標操作 他是T & operator[](int ind)
他是在調用一個函數 這自然相比之下 效率要低很多很多;
2(push_back
): 即便你已經提前reserve
了, 但是 如果用數組方式 他是A[ size ++] = val
, 可是用容器 他是void push_back( T & val){ if( size == maxSize){ ...} else{ A[ size ++] = val}}
他不是直接的賦值 而是一個函數 會產生臨時對象 而且內部有邏輯判斷 會常數消耗;
遇到vector
超時 基本就是以上這2個原因, 要優化 很簡單 就是把他替換成一個T A[?];
的靜態數組;
栈空间大小
力扣的栈空间为100MB
, 正规的CF/AcWing等都是256MB
;
哈希set一定比普通set要快的多
普通版本比哈希版本 只多了一个lower_bound
操作 (因为普通版本是有序的)
假如你的需求 不涉及lower_bound
只涉及(增删改查) 即两个版本都可以满足你的需求, 那么: 哈希版本 一定比 普通版本 优秀 (至少快1倍);
不管是set, multiset, map, multimap
, 他的哈希版本 一定比 其普通版本 快;
当题目的单组测试数据, 手动改为多组测试数据
比如题目是单组测试数据录入, 然后样例给出了4个测试点 d1, d2, d3, d4
;
那么, 你的程序 肯定也必须是单组数据录入;
但是 这就很麻烦, 你把d1
复制到data.in
里运行, 然后d2, d3, ...
, 如果你代码错误 你去修改代码了 然后再回来, 又得一个个的复制和运行;
一个便利方式是, 你把你的程序 改成多组数据录入, 然后把data.in
改为
4
d1
d2
d3
d4
(即使有多余的{空格,回车} 也没关系, 因为cin
的录入会给忽略掉)
这样多方便… 但必须要注意, 提交代码时 再把程序改成单组数据录入;
LeetCode
的多组数据 只是函数的多次调用
比如有多组数据[a, b, c]
, OI平台的计时 有以下兩種方式:
1(非力扣): 通过程序的录入cin
数据(大多数都是采用这种方式), 那么 你的程序exe
会执行3次;
2(力扣): 通过入口函数的传参, 那么 你的程序 只有执行1次, 只是说 那个入口函数 会执行3次;
.
这点非常重要, 比如对于DFS的记忆化 如果你采用static bool is_first = true
这种方式 (当他为true
, 那么就执行memset
初始化), 你的本意 是想让每组数据 都执行初始化, 可是 由于力扣里 你的程序只会执行1次 因此你的初始化只会进行1次 这就出错了; 你需要将这个is_first
改为全局变量 然后在力扣的入口函数里 每次将他置为true
, 这样那个memset
初始化 才会执行3次;
所以, 對於力扣 你的一些全局變量(比如篩質數 預處理組合數…), 不需要每次都初始化!(這可能會超時的!) 你應該只初始化一次!
根据数据大小, 断定时间复杂度
读懂题意后, 根据数据的大小, 来断定 时间复杂度, 这是非常重要的做题技巧;
比如DP类题目, 数据大小为 1 e 6 1e6 1e6, 那么 一般就是 N ∗ l o g N N * logN N∗logN (比如LIS,LCS 通过优化DP定义, 都可以做到 从 N 2 N^2 N2 优化为 N ∗ l o g N N*logN N∗logN, l o g N logN logN为使用二分 来查找DP的前驱节点);
memset
的耗时 是常数
有1e4
条a->b
的边 (a,b = [0,4e3]
), 需求是 给定一条边a->b
询问是否存在这条边;
方式1:
bool Exist[ 4003][ 4003];
memset( Exist, false, sizeof( Exist));
Exist[ a][ b] = true;
方式2:
unordered_set< int> Exist;
Exist.insert( a * 4003 + b);
暂不讨论 查询的效率, 先看此时 即插入的效率;
方式2: 1e4 * log(1e4)
, 不大的 无关紧要;
方式1: 2e7 + 1e4(插入)
, 重点看memset
的耗时 其实他也是常数… 你非常容易认为, 这个时间太大了 毕竟2e7
的级别, 而把这个做法给摒弃掉;
.
其实, memset
的效率非常高 虽然这个数组大小是2e7
, 但其实 对于memset
, 他的耗时 大概也就是2e6
(甚至更少)的时间, 因此不用担心他会超时;
因此, 不讨论查询的效率 此时这两种方式 时间差不多;
然后关键是查询的时间, 假如你算法会调用查询x
次, 那么方式2 就是x * log(1e4)
, 而方式1 是x
;
.
假如x
很大 那么方式2一定会超时, 而方式1不会 因为他是O(1)
的;
因此, 综合而看 方式1 一定是优于 方式2的;
例题: https://www.acwing.com/problem/content/description/4982/
.
用方式1可以AC, 用方式2会超时;
可以尝试下, 使用counter
计数 来碰个巧
如果你的算法超时了 (正解又想不出), 比赛也要快结束了 (即万不得已的情况下), 考虑下你的算法;
如果你的算法, 是求最值/最优解等问题, 里面用到一个循环while(){ ...}
, 很可能是这里超时了;
那么, 比如你循环里 遍历的序列是[a1,a2,a3,...]
(可以去碰巧猜一下, 即最优解 一定来自a[<=K]
, 当然不一定正确), 那么把你的代码改为:
int __counter = 0;
while(?){
if( ++ __counter >= K){
break;
}
}
K可能是常数, 也可能是个因变量, 视情况而定;
一开始去猜这个K
时, 要尽量的大一点, 因为即使你AC了 比赛结束后 还会重新有后台数据再评测, 防止你K太小 到时候出现 时间够用 但由于你K小 没找到答案 也就是WA
错误;
当然 如果K太大了 那到时候的后台数据 还是会超时…
只能说, 这毕竟是投机取巧嘛, 你即使比赛时AC了, 比赛后还有2次评测: 你K小了 可能会WA
, K大了 可能会LTE
;
@DELI;
比如:
SPFA的判断负环 LINK: (https://editor.csdn.net/md/?articleId=130535265)-(@LOC_0)
;
2023的LC战队赛的第3题 LINK: https://editor.csdn.net/md/?not_checkout=1&articleId=130651187
;
一组录入数据, 与一个测试点的区别
For example, the input is T n1 m1 n2 m2 ... nT mT
where ni mi
denotes the size of a matrix;
A Data (一组数据) denotes
n
i
,
m
i
ni, mi
ni,mi, that is a matrix;
While a Test (一个测试点) denotes
T
T
T, that is these
T
T
T matrices;
If the problem says, the Sum of all ni * mi
in one Test satisfies
<
1
e
5
< 1e5
<1e5;
.
That is,
∑
i
=
1
T
n
i
∗
m
i
<
1
e
5
\sum_{i=1}^T n_i*m_i < 1e5
∑i=1Tni∗mi<1e5, do not perceive that
n
i
∗
m
i
<
1
e
5
n_i * m_i < 1e5
ni∗mi<1e5 of a Data; These two notions are absolutely different;
LeetCode
全局对象的位置
Do not define Global-Objects like this:
//>< @Location_0
class Solution{
int Dp[ 4005][ 4005];
int Work(){ ...}
};
This array would be in Stack-Space causing overstack
error; you should put it in @Location_0
;
当数据范围接近INT_MAX
, 全体使用long long
When the Data-Range is very large that close to INT_MAX
(e.g., the problem-content occurs
n
≤
2
31
−
1
n \leq 2^{31}−1
n≤231−1), don’t be hesitant, just use long long
for all variables;
数论中遇到大数
Given m = 1 e 6 m = 1e6 m=1e6 numbers x = 1 e 12 x = 1e12 x=1e12, you need to checking whether x x x is a Prime-Number or get its Prime-Factors, or other demands; in a word, directly performing the Brute-Algorithm on x x x is Infeasible (cuz m ∗ x = 1 e 12 m * \sqrt{ x} = 1e12 m∗x=1e12)
There is an important property (a common and useful Trick) for an integer
x
x
x:
.
If
x
x
x is not a Prime-Number, then
m
i
n
(
p
i
)
≤
x
min(p_i) \leq \sqrt{ x}
min(pi)≤x (in other words, there must exist a Prime-Factors
p
p
p of
x
x
x such that
p
∣
x
∧
p
∈
[
1
,
x
]
p | x \ \ \land \ \ p \in [1, \sqrt{x}]
p∣x ∧ p∈[1,x])
As a result, we can stand in the converse view-point, focusing on all Primes
p
=
[
1
,
1
e
6
]
p = [1, 1e6]
p=[1,1e6] instead of
x
x
x, then any Multiple of
p
p
p must be not a Prime-Number;
.
Note that, this method would only sieved all Non-Prime-Numbers
a
a
a which contains a Prime-Number, that is,
a
=
0
/
1
a = 0/1
a=0/1 would not be sieved although they are also Non-Primes;
As an example: Link
Finding out all Prime-Numbers in the range
[
l
,
r
]
[l, r]
[l,r] where
0
≤
l
≤
r
≤
1
e
12
∧
(
r
−
l
)
≤
1
e
6
0 \leq l \leq r \leq 1e12 \quad \land (r-l) \leq 1e6
0≤l≤r≤1e12∧(r−l)≤1e6; the algorithm is:
int l, r;
bool Is_prime[ r - l + 1] = true;
for( p : $(all Primes `<= \sqrt{r}`){ //< any Non-Prime in [l,r] must has a Prime which is `<= \sqrt{r}`;
for( a : [$(all number in [l,r])]_AND_[$([k*p]_AND_[k>=2] (the multiple of `p`))]){
//< `a` is a multiple of `p`, and also `a != p` (very important);
Is_prime[ a - l] = false;
}
}
//>< note that, only a Non-Prime which has a Prime-Factor (>=2) would be sieved (set `false`); in other words, although `0,1` are also Non-Primes, they would not be sieved (you need check them specially);
vector<> primes;
for( i : [l,r]){
if( (i >= 2) && Is_prime[ i]){
primes.push_back( i);
}
}