算法 {算法经验技巧}

算法 {算法经验技巧}

@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 NlogN (比如LIS,LCS 通过优化DP定义, 都可以做到 从 N 2 N^2 N2 优化为 N ∗ l o g N N*logN NlogN, l o g N logN logN为使用二分 来查找DP的前驱节点);

memset的耗时 是常数

1e4a->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=1Tnimi<1e5, do not perceive that n i ∗ m i < 1 e 5 n_i * m_i < 1e5 nimi<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

Link

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 n2311), 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 mx =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}] px    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 0lr1e12(rl)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);
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值