算法整理(更新中)

数论

lcm 和 gcd

gcd 是使用了辗转相除法

int gcd(int a, int b)
{
	return gcd(b, a % b);
}

辗转相减法的版本, 在一些特定的场合需要用到辗转相减法

辗转相减法中数字的顺序十分重要, 一般保证第一个数大于等于第二个数, 保证a > 0, b > 0

int gcd(int a, int b)
{
    if( a == b ) return a;
    if(a < b) swap(a, b);
    return gcd(b, a - b);
}
使用辗转相减法求指数的公约数

n k 1 , n k 2 , . . . , n k n n^{k_1}, n^{k_2}, ... , n^{k_n} nk1,nk2,...,nkn ,我们需要求k1, k2, …, kn的公约数s, 最后输出 n s n^{s} ns

int gcd(int a, int b)
{
    if( b == 1) return a;
    if(a < b) swap(a, b);
    return gcd(b, a / b); // 因为次幂的除法就是减法, 其实就是间接使用了辗转相减法
}

lcm(x, y) = x / gcd(x, y) * y

为了避免x 和 y相乘导致乘法溢出,所以先除后乘

int lcm(x, y)
{
    return x / gcd(x, y) * y;
}

质数

试除法判断质数

这里i的停止条件十分重要
不能是sqrt(num) 因为sqrt这个函数本身有点慢
也不能是i * i <= num 因为i * i 会溢出

bool is_prime(int num)
{
    for(int i = 2;i <= num / i; ++i)
    {
        if(num % i == 0)
            return false;
    }
    return true;
}
线性筛
int primes[N];
int st[N];

void get_prime(int n)
{
    for(int i = 2; i <= n; ++i)
    {
		if(!st[i]) primes[++primes[0]] = i;
        for(int j = 1; j <= primes[0] && i * primes[j] <= n; ++j)
        {
			st[i * primes[j]] = true;
             if(i % primes[j] == 0) break;
        }
    }
}

分解质因数

使用了试除法思想

void fun(int num)
{
    for(int i = 2; i <= num / i; ++i)
    {
        if(num % i == 0)
        {
            int cnt = 0;
            while(num % i == 0)
            {
                cnt++;
				num /= i;
            }
            printf("%d %d\n", i, cnt);
        }
	}
    if(num != 1) printf("%d %d\n", num, 1);
}

约数

使用试除法求一个数的所有约数
#include <iostream>
#include <algrithm>
#include <vector>
using namespace std;
int main()
{
    int n;
    cin >> n;
    while( n -- )
    {
		int num;
        cin >> num;
        vector<int> arr;
        for(int i = 1; i <= num / i; ++i)
        {
			if(num % i == 0) 
            {
                arr.push_back(i);
                if(num / i != i) //这个if一定要套在上一个if里面,首先要是因数,然后才能num / i
                    arr.push_back(num / i);
            }
        }
        sort(arr.begin(), arr.end());
        for(auto it = arr.begin(); it != arr.end(); ++it)
            cout << *it << " ";
        cout << endl;
	}
	return 0;
}

求一个数的所有因数(优化版) – 使用质因数分解去优化整体的算法复杂度,可以优化10 - 100倍

大致思想就是先对这个数质因数分解,这个过程是sqrt(n) / log(n) 的然后使用dfs枚举所有的约数(最多不超过1600个)可以忽略不计

#include <iostream>
#include <algorithm>
using namespace std;
//因为一个数可以被sqrt(n) 以内的质因数分解,所以primes数组只需要开sqrt(INT_MAX) 就可以了
const int N = 1e5 + 10;
int primes[N];
bool st[N];
void init(int n)
{
    for(int i = 2; i <= n; ++i)
    {
        if(!st[i]) primes[++primes[0]] = i;
    	for(int j = 1; j <= primes[0] && i * primes[j] <= n; ++j)
        {
            st[i *  primes[j]] = true;
            if( i % primes[j] == 0 ) break;
        }
    }
}

typedef long long LL;
typedef pair<int, int> PII;
//一个int范围内的数字最多可以被9个质因数分解,非常小,
const int M = 10;
PII factor[M];
int cntf;

int cntd;
//一个int范围内的数最多有1600个约数
int divider[1601];

void dfs(int u, int p)
{
	if( u > cntf)
    {
        divider[++cntd] = p;
        return;
    }
    for(int i = 0; i < factor[u].second; ++i)
    {
		dfs(u + 1, p);
        p *= factor[u].first;
    }
}

int main()
{
    int n;
    cin >> n;
    init(n);
    int d = n;
    for(int i = 1; primes[i] <= d / primes[i]; ++i)
    {
		int p = primes[i];
        if( d % p == 0)
        {
            int s = 0;
            while( d % p == 0)
                d /= p, ++s;
        	factor[++cntf] = {p, s};
        }
    }
    if(d != 1) factor[++cntf] = {d, 1};
    
    dfs(1, 1);
	
    for(int i = 1; i <= cntd; ++i)
        cout << divider[i] << " ";
	return 0;
}
求一个数的所有约数个数
LL get_sum(num)
{
    //求num的约数个数
    LL res = 1;
    unordered_map<int, int> primes;
   	for(int i = 2; i <= num / i; ++i)
    {
		while(num % i == 0)
        {
			primes[i] ++;
            num /= i;
        }
    }
    if(num != 1) primes[num] ++;
	for(auto j : primes) res = res * (j.second + 1) % mod;
    return res;
}
求一段范围内所有数的约数个数

求1 - n范围内所有数的约数个数之和

这道题可以看成1的倍数的的个数 2的倍数的个数,一次 类推

#include <iostream>
#include <algorithm>
using namespace std;
int main()
{
	int n, ans = 0;
    cin >> n;
    for(int i = 1; i <= n; ++i)
        ans += n / i;
    cout << ans;
	return 0;
}
求一个数的所有约数之和

sum(n)=(a01+a11+…+aα11)∗(a02+a12+…+aα22)∗…∗(a0k+a1k+…+aαkk)

#include<iostream>
#include<unordered_map>
using namespace std;
const long long mod=1e9+7;
int main()
{
    int n;
    long long  res=1;
    unordered_map<int,int> f;
    cin>>n;
    while(n--)
    {
        int  x;
        cin>>x;
        for(int i=2;i<=x/i;i++)
        {
            while(x%i==0)
            {
                f[i]++;
                x/=i;
            }
        }
        if(x>1) f[x]++;
    }
    for(auto t : f)
    {
        long long tmp=1;
        while(t.second--)
        {
            tmp=(tmp*t.first+1)%mod;  // 这一步十分关键
        }
        res=res*tmp%mod;
    }
    cout<<res;
    return 0;
}

质数分解一个阶乘数

题目意思是给一个阶乘数nn<= 1e6) ,要质因数分解n

ps: 质数分解定理和求解约束个数是绑定在一起的,如果可以质因数分解,那么就可以得到这个数的约数个数

#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 1e6 + 10;

int primes[N];
bool st[N];

int main()
{
    int n;
    cin >> n;
    //首先使用质数筛晒出根号n范围内的质数
    for(int i = 2; i <= n; ++i)
    {
        if(!st[i]) primes[++primes[0]] = i;
        for(int j = 1; j <= primes[0] && i * primes[j] <= n; ++i)
        {
			st[i * primes[j]] = true;
            if(i % primes[j] == 0) break;
        }
    }
    for(int i = 1; i <= primes[0]; ++i)
    {
        int p = primes[i];
        int s = 0;
        for(int j = n; j; j /= p) s += j / p;
        cout << p << " " << s << endl;
    }
	return 0;
}
反素数(一个考约数知识点较多的题目)

反素数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7tbtPg7n-1645509840000)(C:\Users\Administrator\Desktop\ACM模板整理\images\image-20210729163959213.png)]

#include<iostream>
#include <algorithm>
using namespace std;
int primes[] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29};
int maxd, num;
int n;
typedef long long LL;


//s表示当前约数个数
//last是上一个素数的次数,因为要保证次数严格递减,所以当做下一次遍历的最大次数
//这里质数的次数要严格递减
void dfs(int u, int last, int p, int s)
{
    if(s > maxd || (s == maxd && p < num))
    {
        maxd = s;
        num = p;
    }
    if(u > 9) return ;
    
    for(int i = 1; i <= last; ++i)
    {
        if((LL) p * primes[u] > n) break;
        p *= primes[u];
        dfs(u + 1, i, p, s * (i + 1));
    }
}

int main()
{
    cin >> n;
    
    dfs(0, 30, 1, 1);
    
    cout << num << endl;
    return 0;
}

从这道题最后可以总结出int范围内一个数的约数个数最多是1600个,不会超过这个数,1600是精确的数字

而且int范围内的质因数分解,得到的质因数最大是23,到29就会爆int

线性筛法求欧拉函数

欧拉函数 φ ( x ) \varphi(x) φ(x)​​​​​ 表示小于x的与x互质的数的个数,在求单个数的欧拉函数的时候,我们可以使用质因数分解的方式快速求解一个数的欧拉函数:
如 果 一 个 数 x 的 质 因 数 分 解 : x = p 1 c 1 p 2 c 2 . . . p n c n 那 么 φ ( x ) = x ( 1 − 1 c 1 ) ( 1 − 1 c 2 ) . . . ( 1 − 1 c n ) 如果一个数x的质因数分解:x=p_1^{c1}p_2^{c2}...p_n^{c_n}\\ 那么\varphi(x)=x (1-\frac{1}{c1})(1-\frac{1}{c2})...(1-\frac{1}{cn}) xx=p1c1p2c2...pncnφ(x)=x(1c11)(1c21)...(1cn1)
但是如果要使用很多数字的的欧拉函数,那么就需要使用到线性筛法求欧拉函数

#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e7 + 10;
int primes[N];
bool st[N];
int st[N];

void init(int n)
{
	for(int i = 2; i <= n; ++i)
    {
        if(!st[i]) primes[++primes[0]] = i, phi[i] = i - 1;
    	for(int j = 1; j <= primes[0] && i * primes[j] <= n; ++j)
        {
            st[i * primes[j]] = true;
            if(i % primes[j] == 0) 
            {
                phi[i * primes[j]] = phi[i] * primes[j];
				break;
            }
            phi[i * primes[j]] = phi[i] * (primes[j] - 1);
        }
    }
}
int main()
{
    int n;
    cin >> n;
    init(n);
    //这样就筛完了, phi中保存的数字就是我们需要的数
	return 0;
}

扩展欧几里得算法

对于二元一次方程 a x + b y = c ax + by = c ax+by=c 的求解问题, 首先这个问题有解的前提是 c = g c d ( a , b ) ∗ k ( k ∈ Z ) c=gcd(a, b) * k (k \in Z) c=gcd(a,b)k(kZ)​ ​

这个问题可以使用扩展欧几里得算法求解:

证明:
欧 几 里 得 算 法 : g c d ( a , b ) = g c d ( b , a % b ) 所 以 一 个 方 程 可 以 表 示 成 : a x + b y = g c d ( a , b ) → a x + b y = g c d ( b , a % b ) 将 相 关 的 变 量 进 行 替 换 b x ′ + ( a % b ) y ′ = g c d ( b , a % b ) → b x ′ + ( a − ⌊ a b ⌋ ∗ b ) y ′ = g c d ( b , a % b ) 对 这 个 等 式 进 行 整 理 : a x + b y = a y ′ + b ( x ′ − ⌊ a b ⌋ ∗ y ′ ) { x = y ′ y = x ′ − ⌊ a b ⌋ ∗ y ′ 因 为 使 用 欧 几 里 得 算 法 作 为 递 归 的 条 件 , 所 以 递 归 结 束 的 时 候 是 g c d ( a , 0 ) = a , 所 以 x = 1 , y = 0 欧几里得算法:\\ gcd(a, b) = gcd(b, a \% b)\\ 所以一个方程可以表示成: ax + by = gcd(a, b) \rightarrow \\ ax + by = gcd(b, a \% b)\\ 将相关的变量进行替换\\ bx' + (a \% b)y' = gcd(b, a \% b) \rightarrow bx' + (a - \lfloor{\frac{a}{b}}\rfloor * b)y' = gcd(b, a \% b)\\ 对这个等式进行整理:\\ ax + by = ay' + b(x' - \lfloor{\frac{a}{b}}\rfloor * y')\\ \begin{cases} x = y'\\ y = x' - \lfloor\frac{a}{b}\rfloor * y' \end{cases}\\ 因为使用欧几里得算法作为递归的条件,所以递归结束的时候是gcd(a, 0) = a, 所以x = 1, y = 0 :gcd(a,b)=gcd(b,a%b):ax+by=gcd(a,b)ax+by=gcd(b,a%b)bx+(a%b)y=gcd(b,a%b)bx+(abab)y=gcd(b,a%b)ax+by=ay+b(xbay){x=yy=xbay使gcd(a,0)=a,x=1,y=0
代码:

int exgcd(int a, int b, int& x, int &y)
{
	if(b == 0)
    {
        x = 1, y = 0;
        return a;
    }
    int d = exgcd(b, a % b, y, x); //这里我们交换一下x' 和 y' 的位置, 方便运算
    y -= a / b * x;
    return d;
}
  • 通过特殊解求的一般解
    对 于 a x + b y = c d = g c d ( a , b ) { x = x 0 − b d ∗ k y = y 0 + a d ∗ k ( k ∈ Z ) 对于 ax + by = c\\ d = gcd(a, b)\\ \begin{cases} x = x_0 - \frac{b}{d} * k\\ y = y_0 + \frac{a}{d} * k\\ \end{cases} (k \in Z) ax+by=cd=gcd(a,b){x=x0dbky=y0+dak(kZ)
    所以如果要求x的最小正整数解

    x = (x0 % (b / gcd) + (b / gcd)) % (g / gcd)

    证明:

    充分性: 已知 a x 0 + b y 0 = c ax_0 + by_0 = c ax0+by0=c​,可得 a ( x 0 − b 1 t ) + b ( y 0 − a 1 t ) = a x 0 + b y 0 − t ( a b 1 + b a 1 ) = c a(x_0 - b_1t) + b(y_0 - a_1t) = ax_0 + by_0 - t(ab_1 + ba_1) = c a(x0b1t)+b(y0a1t)=ax0+by0t(ab1+ba1)=c​​
    所以 a b 1 + b a 1 = 0 ab_1 + ba_1 = 0 ab1+ba1=0

    必要性: 设x’, y’是 a x + b y = c ax + by = c ax+by=c的任意一解,则 a x ′ + b y ′ = c ax' + by' = c ax+by=c ,与 a x + b y = c ax + by = c ax+by=c 两式联立可得

    a ( x − x 0 ) + b ( y − y 0 ) = 0 a(x - x_0) + b(y - y_0) = 0 a(xx0)+b(yy0)=0​ , 等式两边同除gcd(a, b), a g c d ( x − x 0 ) + b g c d ( y − y 0 ) = 0 \frac{a}{gcd}(x - x_0) + \frac{b}{gcd}(y - y_0) = 0 gcda(xx0)+gcdb(yy0)=0

    因为 a g c d \frac{a}{gcd} gcda b g c d \frac{b}{gcd} gcdb互质所以 ( x − x 0 ) ∣ b g c d (x - x_0) | \frac{b}{gcd} (xx0)gcdb ( y − y 0 ) ∣ a g c d (y - y_0) | \frac{a}{gcd} (yy0)gcda

    代码:

    //求解二元一次等式的多组解
    // ax + by = c
    int exgcd(int a,int b, int& x, int& y)
    {
        if( b == 0 )
        {
            x = 1; y = 0;
            return a;
        }
        int d = exgcd(b, a % b, y, x);
        y -= a / b * x;
        return d;
    }
    
    int main()
    {
        int a, b, c;
        cin >> a >> b >> c;
        int x, y;
        int d = exgcd(a, b, x, y);
        // 首先判断这个式子成不成立
        if(c % d != 0) puts("impossible");
        else 
        {
    		x *= (c / d);
            int t1 = b / d, t2 = a / d;
            int minx = (x % t1 + t1) % t1;
           	int miny = (c - a * minx) / b;
            //枚举10组解
            for(int i = 0; i <= 10; ++i)
            	cout << minx + i * t1 << " " << miny - i * t2;
        }
        return 0;
    }
    

快速幂和龟速乘

这两个算法都用到了二进制分解的概念
a k % p ( k = 2 c 1 2 c 2 . . . 2 c n ) → k = ( 1...1...1 ) 2 → a k = a 2 1 c ∗ a 2 2 c . . . ∗ a 2 n c 思 路 就 是 停 地 & 1 , 如 果 非 零 , 那 么 就 说 明 a 2 n c 存 在 , 就 需 要 在 结 果 中 乘 上 这 一 项 a^k \% p(k = 2^{c_1}2^{c_2}...2^{c_n})\\ \rightarrow k = (1...1...1)_2\\ \rightarrow a^k=a^{2^c_1}*a^{2^c_2}...*a^{2^c_n}\\ 思路就是停地 \& 1, 如果非零,那么就说明a^{2^c_n}存在,就需要在结果中乘上这一项 ak%p(k=2c12c2...2cn)k=(1...1...1)2ak=a21ca22c...a2nc&1,a2nc

a ∗ k = a ∗ ( 2 c 1 + 2 c 2 + . . . 2 c n ) p s : 将 k 二 进 制 分 解 那 么 同 样 可 以 使 用 相 同 的 思 路 , 只 不 过 乘 法 换 成 了 加 法 a * k = a * (2^{c_1} + 2^{c_2} +...2^{c_n}) \\ ps: 将k二进制分解\\ 那么同样可以使用相同的思路,只不过乘法换成了加法\\ ak=a(2c1+2c2+...2cn)ps:k使

//龟速乘的代码
int qmul(int a, int b, int p)
{
	int res = 0;//因为加法的零元是0
    while(b)
    {
		if( b & 1 ) res = (LL)(res + a) % p; //如果数据大的话,这里都有可能爆int
        b >>= 1;
        a = (LL) (2 * a) % p;
    }
}

中国剩余定理

Youtube 讲解视频 讲得非常好

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rAHAqUE8-1645509840001)(C:\Users\Administrator\Desktop\ACM模板整理\images\image-20210731160427696.png)]

对于上面的同于方程组问题,我们要得到x的一个通项表达式。

这里要介绍一个非常重要的概念(同余的加性)

比如:
{ x ≡ a 1 ( m o d   m 1 ) x ≡ 0 ( m o d   m 2 ) x ≡ 0 ( m o d   m 3 )   ( m 1 , m 2 , m 3 两 两 互 质 ) \left\{ \begin{aligned} x \equiv a1(mod\ m_1)\\ x \equiv 0(mod\ m_2)\\ x \equiv 0(mod\ m_3) \end{aligned} \right.\ (m_1, m_2, m_3 两两互质) xa1(mod m1)x0(mod m2)x0(mod m3) (m1,m2,m3)
通过上式我们知道x1是m2和m3的倍数,但是mod m1余a1

同样我们可以得到下面两组相同的方程组
{ x ≡ 0 ( m o d   m 1 ) x ≡ a 2 ( m o d   m 2 ) x ≡ 0 ( m o d   m 3 )   ( m 1 , m 2 , m 3 两 两 互 质 ) { x ≡ 0 ( m o d   m 1 ) x ≡ 0 ( m o d   m 2 ) x ≡ a 3 ( m o d   m 3 )   ( m 1 , m 2 , m 3 两 两 互 质 ) \left\{ \begin{aligned} x \equiv 0(mod\ m_1)\\ x \equiv a_2(mod\ m_2)\\ x \equiv 0(mod\ m_3) \end{aligned} \right.\ (m_1, m_2, m_3 两两互质)\\ \\ \left\{ \begin{aligned} x \equiv 0(mod\ m_1)\\ x \equiv 0(mod\ m_2)\\ x \equiv a_3(mod\ m_3) \end{aligned} \right.\ (m_1, m_2, m_3 两两互质) x0(mod m1)xa2(mod m2)x0(mod m3) (m1,m2,m3)x0(mod m1)x0(mod m2)xa3(mod m3) (m1,m2,m3)
我们知道x2是m1和m3的倍数,但是mod m2余a2
我们知道x2是m1和m2的倍数,但是mod m3余a3

我们将这三个x相加 x = x 1 + x 2 + x 3 x = x_1 + x_2 + x_3 x=x1+x2+x3, 得到一个特解
易得这个x是满足第一个同余方程的, 而且x的解有很多个,每个 x = x ′ + k ∗ ∏ 1 n m n   ( k ∈ Z ) x = x' + k * \prod_{1}^{n}m_n\ (k \in Z) x=x+k1nmn (kZ)

这样我们就可以从特解到通解

同余的乘性(左右两边同时乘以一个常数等式依然成立):

$ a \equiv b \ (mod\ c) \rightarrow a * k \equiv b * k\ (mod\ c)$​

所以我们只需要计算
{ a ′ ≡ 1 ( m o d   m 1 ) a ′ ≡ 0 ( m o d   m 2 ) a ′ ≡ 0 ( m o d   m 3 )   { b ′ ≡ 0 ( m o d   m 1 ) b ′ ≡ 1 ( m o d   m 2 ) b ′ ≡ 0 ( m o d   m 3 )   { c ′ ≡ 0 ( m o d   m 1 ) c ′ ≡ 0 ( m o d   m 2 ) c ′ ≡ 1 ( m o d   m 3 ) ∴ x = a 1 ∗ a ′ + a 2 ∗ b ′ + a c ∗ c ′ 这 样 的 式 子 就 满 足 上 面 一 开 始 的 同 余 方 程 组 ( 使 用 到 了 同 余 的 加 性 和 乘 性 ) 再 对 x 做 ∏ 1 n m n   ( k ∈ Z ) 倍 的 放 缩 即 可 \left\{ \begin{aligned} a' \equiv 1(mod\ m_1)\\ a' \equiv 0(mod\ m_2)\\ a' \equiv 0(mod\ m_3) \end{aligned} \right.\ \left\{ \begin{aligned} b' \equiv 0(mod\ m_1)\\ b' \equiv 1(mod\ m_2)\\ b' \equiv 0(mod\ m_3) \end{aligned} \right.\ \left\{ \begin{aligned} c' \equiv 0(mod\ m_1)\\ c' \equiv 0(mod\ m_2)\\ c' \equiv 1(mod\ m_3) \end{aligned} \right.\\ \therefore x = a_1 * a' + a_2 * b' + a_c * c'\\ 这样的式子就满足上面一开始的同余方程组(使用到了同余的加性和乘性)\\ 再对x做\prod_{1}^{n}m_n\ (k \in Z)倍的放缩即可 a1(mod m1)a0(mod m2)a0(mod m3) b0(mod m1)b1(mod m2)b0(mod m3) c0(mod m1)c0(mod m2)c1(mod m3)x=a1a+a2b+acc(使)x1nmn (kZ)

扩展中国剩余定理

原先要满足 m 1   m 2   m 3 . . . m n m_1\ m_2\ m_3 ... m_n m1 m2 m3...mn​都要两两互质才行,不然在求同余方程的时候会不满足条件

但是如果不满足这个条件,那么就需要用到扩展中国剩余定理去求解(扩展欧几里得算法)

具体的证明推导的过程

代码

#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e5 + 10;
typedef long long LL;
LL C[N], M[N];

LL exgcd(LL a, LL b, LL& x, LL& y)
{
    if(b == 0)
    {
        x = 1, y = 0;
        return a;
    }
    LL d = exgcd(b, a % b, y, x);
    y -= a / b * x;
    return d;
}
LL gcd(LL a, LL b)
{
    return b ? gcd(b, a % b) : a;
}
LL qmul(LL a, LL b, LL mod)
{
    LL res = 0;
    while(b)
    {
        if(b & 1) res = (res + a) % mod;
        b >>= 1;
        a = 2 * a % mod;
    }
    return res;
}


int main()
{
    freopen("data.in", "r", stdin);
    int n;
    cin >> n;
    for(int i = 1; i <= n; ++i)
        cin >> M[i] >> C[i];
    bool flag = true;
    for(int i = 2; i <= n; ++i)
    {
        LL m1 = M[i - 1], m2 = M[i], c1 = C[i - 1], c2 = C[i], T = gcd(m1, m2);
        if((c2 - c1) % T != 0) {flag = false; break;}
        LL x, y;
        exgcd(m1 / T, m2 / T, x, y);
        // C[i] = (x * (c2 - c1) / T) % (m2 / T) * m1 + c1;
        while(x < 0) x += m2 / T;
        C[i] = qmul((c2 - c1) / T, x, m2 / T) * m1 + c1; // 防止爆LongLong 这里要用龟速乘,在使用龟速乘的时候要注意第二个数字一定不能是负数,不然就是死循环,所以我们把x映射到正数范围内,使用龟速乘。
        M[i] = m1 / T * m2;
        C[i] = (C[i] % M[i] + M[i]) % M[i];
    }
    cout << (flag ? C[n] : -1) << endl;
    return 0;
}

斐波那契数列的求解问题

普通求解(直接使用递归求解):

//这里一定是LL, 因为菲波那切数列到59左右就会爆int
LL fun(int num)
{
	if(num == 1 || num == 0)
        return 1;
    return fun(num - 1) + fun(num - 2);
}

快速幂 + 矩阵求法(这种求法十分高效)
令 F n = [ f ( n ) , f ( n + 1 ) ]   ( 这 是 一 个 向 量 ) ∴ F n + 1 = [ f ( n ) , f ( n + 1 ) ] [ 0 1 1 1 ]   = [ f ( n + 1 ) , f ( n ) + f ( n + 1 ) ] 由 于 矩 阵 是 有 结 合 律 的 , 所 以 可 以 用 指 数 次 幂 的 表 达 式 来 表 示 这 个 结 果 F n = [ 0 , 1 ] [ 0 1 1 1 ] n 涉 及 到 次 幂 的 表 达 式 就 可 以 将 这 个 式 子 用 二 进 制 分 解 , 然 后 使 用 快 速 幂 的 思 路 去 解 决 就 可 以 了 . 令F_{n} = [f(n), f(n + 1)]\ (这是一个向量)\\ \therefore F_{n + 1} = [f(n), f(n + 1)] \begin{bmatrix} 0 & 1 \\ 1 & 1 \\ \end{bmatrix}\ = [f(n + 1), f(n) + f(n + 1)]\\ 由于矩阵是有结合律的,所以可以用指数次幂的表达式来表示这个结果\\ F_{n} = [0, 1] \begin{bmatrix} 0 & 1 \\ 1 & 1 \\ \end{bmatrix}^{n}\\ 涉及到次幂的表达式就可以将这个式子用二进制分解,然后使用快速幂的思路去解决就可以了. Fn=[f(n),f(n+1)] ()Fn+1=[f(n),f(n+1)][0111] =[f(n+1),f(n)+f(n+1)]Fn=[0,1][0111]n使.

#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;
const int mod = 10000;
typedef long long LL;
const int N = 2;
//向量和矩阵之间的乘法
void mul(int a[N], int b[][N])
{
   int temp[N] = {0};
   for(int i = 0; i < N; ++i)
   {
       for(int j = 0; j < N; ++j)
       {
           temp[i] += a[j] * b[j][i] % mod;
       }
   }
   memcpy(a, temp, sizeof temp);
}

void mul(int a[][N], int b[][N])
{
   int temp[N][N] = {0};
   for(int i = 0; i < N; ++i)
       for(int j = 0; j < N; ++j)
           for (int k = 0; k < N; ++k)
               temp[i][j] += a[i][k] * b[k][j] % mod;
   memcpy(a, temp, sizeof temp);
}
int main()
{
   int n;
   while(cin >> n, n != -1)
   {
       int A[N][N] = {
           {0, 1},
           {1, 1}
       };
       int f[N] = {0,1};
       
       while(n) 
       {
           if(n & 1) mul(f, A);
           n >>= 1;
           mul(A, A);
       }
       cout << f[0] % mod << endl;
   }
   return 0;
}

一些数论中作题目知道的神奇定理

  1. 如果 a b两个数互质,那么定义一个集合,集合中包含 a ∗ k % b   ( k ∈ [ 0 , a − 1 ] ) a * k \% b\ (k \in [0, a - 1]) ak%b (k[0,a1])​​ (当a > b 时会有重复), 但这个集合中包含了​0 - (b - 1)的所有数字。

    a ∗ x + b ∗ y a * x + b * y ax+by 在数轴上表示的所有数字可以被拆分成a份, 如果即记 a ∗ x + b ∗ y = c a * x + b * y = c ax+by=c 那么 c % a c \% a c%a​ 一定是落在a范围内的, 而 b ∗ y b * y by决定了c落在a范围内的具体位置(就是决定了偏移)所以 b ∗ y % a b * y \% a by%a就是c落在a范围内的数值。

    这个时候我们利用少那个面介绍的一个小定理就可以很快地知道, { k ∗ b ∗ y % a }   ( k ∈ [ 0 , a ] ) \{ k * b *y \% a\}\ (k \in [0, a]) {kby%a} (k[0,a])​​ 表示的集合中的所有数恰好是0 - a 中的数字,一个不落。

    例题

  2. 如果 ( a − b ) % k = = 0 (a-b) \% k == 0 (ab)%k==0 说明 a, b同余(虽然好像说的是废话,但是在做一些题目的时候, 这句话还是蛮有用的)

    K倍区间

    #include <iostream>
    using namespace std;
    const int N = 1e5 +10;
    typedef long long LL;
    LL s[N];
    int cnt[N];
    
    int main()
    {
        int n, k;
        cin >> n >> k;
        
        for(int i = 1; i <= n; ++i)
        {
            scanf("%lld", &s[i]);
            s[i] += s[i - 1];
            cnt[s[i] % k ] ++;
        }
        LL res = 0;
        for(int i = 1; i <= n; ++i)
            if(s[i] % k == 0)
                res += cnt[0] + 1;
            else
                res += cnt[s[i] % k] - 1;
        cout << res / 2 << endl;
        
        return 0;
    }
    

动态规划(dp问题)

在考虑dp问题的时候要先把dp状态方程写出来,然后再考虑通过等价变形的方式优化原来的dp问题。

f[i][j] 其实是一个集合,这个集合包含了所有的选法,i是从前i个物品中选,j是总体积小于j
f[i][j]数组的值表示的某个属性,(最大值、最小值、数量)

dp问题考虑的几个步骤

  1. 确定集合, 就是f[i][j]的意义
  2. 确定集合表达式的属性(可以是最大值,最小值或者数量)
  3. 确定转移方程,这一步是非常关键的,一般根据具体题目意思而定。
  4. 确定边界(一般是i或者j0的情况)

0 - 1 背包问题

  • 普通方法

    int v[N], w[N];
    int f[N][N];
    
    int main()
    {
        // freopen("date.in", "r", stdin);
        int n, m;
        cin >> n >> m;
        for(int i = 1; i <= n; ++i)
            cin >> v[i] >> w[i];
    
        //第i件物品 
        for(int i = 1; i <= n; ++i)
        {
            //第j个容量 
            for(int j = 0; j <= m; ++j)
            {
                f[i][j] = f[i - 1][j];
                if(v[i] < j) f[i][j] = max(f[i - 1][j - v[i]]+ w[i] , f[i][j]);
            }
        }
    
        cout << f[n][m] << endl;
    
        return 0;
    }
    
  • 一维数组优化之后的终极版本

    // 只定义了一维数组
    int f[N];
    
    for(int i = 1; i <= n; ++i)
        for(int j = m; j >= v[i]; --j) // 从后向前遍历
            f[j] = max(f[j], f[j - v[i]] + w[i])
    
  • 背包问题的另一种写法(个人感觉更加易于理解)

    int f[N];
    
    memset(f, 0, sizeof f);
    int n, m;
    cin >> n >> m;
    for(int i = 1; i <= n; ++i)
    {
        int v, w;
        cin >> v >> w;
        for(int j = m; j >= v; --j)
            f[j] = max(f[j], f[j - v] + w);
    
    }
    cout << f[m] << endl;
    

完全背包问题

  • 完全背包问题最原始(本质的)思想

    for(int i = 1; i <= n; ++i)
        for(int j = 0; j <= m; ++j)
            for(int k = 0; k * v[i] <= j; ++k) //因为可以放任意多个,所以要多加一层遍历
                f[i][j] = max(f[i - 1][j - k * v[i]] + k * w[i], f[i][j]);			
    
    
  • 第一次优化之后的版本

    核心思想:

    image-20210722154740449

    所以核心的更新方程变成了 f[i][j] = max(f[i - 1][j], f[i][j - v] + w)

    //更新之后的迭代方程
    for (int i = 1; i <= n; ++i)
        for(int j = 0; j <= m; ++j)
        {
            f[i][j] = f[i - 1][j];
            if(j >= v[i]) f[i][j] = max(f[i][j], f[i][j - v[i]] + w[i]);
        }
    
  • 第二次优化

    将二维的数组转变成一维的数组

    int f[N];
    for(int i = 1; i <= n; ++i)
        for(int j = v[i]; j <= m; ++j)
           f[j] = max(f[j], f[j - v[i]] + w[i]);
    

多重背包问题

  • 多重背包问题一

    for(int i = 1; i <= n; ++i)
    {
        for(int j = 0; j <= m; ++j)
        {
            for(int k = 0; k <= s[i] && k * v[i] <= j; ++k) // 这里加入一个条件s[i] 表示这里最多放s[i]个商品
            {
                f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);
            }
        }
    }
    
  • 多重背包问题二

    将一个物品的变成2的幂次倍(包括体积和价值)放到我们要考虑的物品栏中,优化完之后再用0 - 1背包问题的思路去写就可以了for()

    将s物品通过二进制分解, 分解成log(s)个可以表达0 - s中每一个数字的的物品数量。

    这就是二进制优化的概念发,非常巧妙

    背包大小 n * log(n)

    #include <iostream>
    #include <algorithm>
    using namespace std;
    const int N = 25000;
    int v[N];
    int w[N];
    int f[N];
    
    int main()
    {
        int n, m;
        cin >> n >> m;
        int cnt = 0;
        for(int i = 1; i <= n; ++i)
        {
            int a, b, s;
            cin >> a >> b >> s;
            int k = 1;
            while(k <= s)
            {
                cnt++;
                v[cnt] = a * k;
                w[cnt] = b * k;
                s -= k;
                k = 2 * k;
            }
            //如果还有剩余 最后能表达的范围就是0 - k + s, 其中k是2的次幂
            if(s > 0)
            {
                cnt ++;
                v[cnt] = s * a;
                w[cnt] = s * b;
            }
        }
        n = cnt;
        
        //变成0 - 1 问题解决
        for(int i = 1; i <= n; ++i)
            for(int j = m;j >= v[i]; --j)
                f[j] = max(f[j], f[j - v[i]] + w[i]);
        cout << f[m] << endl;
        return 0;
    }
    
  • 分组背包问题

    给若干个组, 每个组中只能挑选一个物品,使最后的价值最大,做法也是很简单的

    int n, m;
    cin >> n >> m;
    for(int i = 1; i <= n; ++i)
    {
        cin >> s[i];
        for(int j = 1; j <= s[i]; ++j)
            cin >> v[i][j] >> w[i][j];
    }
    
    for(int i = 1; i <= n; ++i)
        for(int j = m; j >= 0; --j)
            for(int k = 1; k <= s[i]; ++k)
                if(v[i][k] <= j)
                    f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);
    cout << f[m];
    
    

数字三角形模型(这个就是杨辉三角形)

状态dp方程就是:f[i][j] = max(f[i - 1][j - 1], f[i - 1][j]) + a[i][j]

状态数组中记录的是最大值。

最长上升子序列模型

状态转移方程 f[i] = max(f[j] + 1, f[i])

状态数组中记录的是以index为i结尾的最长严格上升子序列的长度,所以最后在取结果的时候不能直接用f[n];

int n;
cin >> n;
for(int i = 1; i <= n; ++i) cin >> a[i];
for(int i = 1; i <= n; ++i)
{
    f[i] = 1;
    for(int j = 1; j < i; ++j)
        if(a[j] < a[i]) 
            f[i] = max(f[i], f[j] + 1);
}

int res = 0;
for(int i = 1; i <= n; ++i) res = max(res, f[i]);
  • 拓展 - 将最长上升子序列记录下来

    精髓就是将转移的过程记录下来,使用转移数组记录,这样记录打印出来的结果是逆序的,在使用的时候需要转置一下再用。

    int n;
    cin >> n;
    for(int i = 1; i <= n; ++i) cin >> a[i];
    for(int i = 1; i <= n; ++i)
    {
        f[i] = 1;
        g[i] = 0;
    
        for(int j = 1; j < i; ++j)
            if(a[j] < a[i]) 
                if(f[i] < f[j] + 1)
                {
                    f[i] = f[j] + 1;
                    g[i] = j;
                }
    }
    
    int k = 1;
    
    for(int i = 2; i <= n; ++i)
        if(f[i] > f[k])
            k = i;
    cout << " res: " << f[k] << endl;
    
    while(k != 0)
    {
        cout << a[k] << " ";
        k = g[k];
    }
    
    

最长公共子序列

有两个字符串,输出两个字符串的最长公共子串

cin >> n >> m;
scanf("%s%s", a + 1, b + 1);
for(int i = 1;i <= n; ++i)
    for(int j = 1; j <= m; ++j)
    {
        f[i][j] = max(f[i - 1][j], f[i][j - 1]);
        if(a[i] == b[j])
            f[i][j] = max(f[i][j], f[i - 1][j - 1] + 1);
    }

cout << f[n][m] << endl;

区间DP问题

石子合并问题,将左右两个区间合并到一起,然后计算最小和

状态转移方程:

状态数组的意义:f[i][j]表示的是一个区间,表示 i - j这个区间, 所有第i堆石子到第j堆石子合并成一堆石子的合并方式的最小值

石子合并

#include <iostream>
#include <algorithm>
using namespace std;
const int N = 310;

int s[N];
int f[N][N];
int n;

int main()
{
   cin >> n;
   //前缀和
   for(int i = 1; i <= n; ++i)
       cin >> s[i], s[i] += s[i - 1];
   
   //遍历长度i - j
   for(int len = 2; len <= n; ++len)
   {
       //遍历起点
       for(int i = 1; i + len - 1 <= n; ++i)
       {
           int j = i + len - 1;//分界线
           f[i][j] = 1e8;
            //遍历分界线
           for(int k = i; k < j; ++k)
               f[i][j] = min(f[i][j], f[i][k] +f[k + 1][j] + s[j] - s[i - 1]);
       }
   }
   cout << f[1][n] << endl;
   
   return 0;
}

多维DP问题

有这样一类题目:变量非常多, 一般有至少三个(暗指如果使用DP,那么需要开多维数组),但是变量的范围十分小:

比如这道例题: 地宫取宝

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6FoCd0bS-1645509840003)(C:\Users\Administrator\Desktop\ACM模板整理\images\image-20220211161050803.png)]

有四个变量,但是每一个变量的范围都十分小, 就可以考虑爆搜去做(个人认为爆搜的优化方式就是DP,DP也属于爆搜的范畴),DP本质上就是用空间换时间,一般需要开很大的数组(也有状态压缩的方法),然后找到前后状态的关系(递推关系式 或者是 y总的集合论),用前一个状态更新当前状态。

经过合理分析,可以知道上面的题目采用的四维的状态搜索,也就是四维DP;

在DP中状态表示也是十分重要的,确定一个比较好的状态表示,那么题目就已经解决一半了;

因为数据范围十分小,可以直接将值变成数组的下标进行表示,这个也是这道题解法的核心之一;

#include <iostream>
using namespace std;
const int N = 51, M = 15;
int d[N][N][M][M];
int a[N][N];
const int MOD = 1000000007;

int main()
{
    int n, m, k;
    cin >> n >> m >> k;
    for(int i = 1; i <= n; ++i)
        for(int j = 1; j <= m; ++j)
            cin >> a[i][j], a[i][j] ++;
    //初始化边界情况
    //选第一个的情况
    d[1][1][1][a[1][1]] = 1;
    // 不选第一个的情况
    d[1][1][0][0] = 1;
    
    for(int i = 1; i <= n; ++i)
        for(int j = 1; j <= m; ++j)
        {
            if(i == 1 && j == 1)continue;
            
            for(int cnt = 0; cnt <= k; ++cnt)
            {
                //遍历最大值
                for(int c = 0; c < M; ++c)
                {
                    //不取的情况
                    int& val = d[i][j][cnt][c];
                    val = (val + d[i - 1][j][cnt][c]) % MOD;
                    val = (val + d[i][j - 1][cnt][c]) % MOD;
                    
                    //如果可以取
                    if(cnt > 0 && c == a[i][j])
                    {
                        for(int s = 0; s < a[i][j]; ++s)
                        {
                            val = (val + d[i - 1][j][cnt - 1][s]) % MOD;
                            val = (val + d[i][j - 1][cnt - 1][s]) % MOD;
                        }
                    }
                    
                }
            }
        }
    int res = 0;
    for(int i = 0; i < M; ++i)
        res = ( res + d[n][m][k][i]) % MOD;
    cout << res << endl;
    
    return 0;
}

状态压缩DP

经典例题:蒙德里安的梦想

这道题首先状态更新的方式很难想, 在想到这个状态更新的方式之后才要使用状态压缩(二进制表示可能性)去优化。

最后为了防止超时, 使用打表的方式预处理一些合法的状态。(这些都是这道题十分精妙的地方)。

思路:

  1. 首先,只要考虑所有横着的方格填充的情况, 只要保证横着的方格填充合法,竖着的方格只要顺次填充进去就可以了。也就是说只要考虑横着填充的方案数,其实就是总方案数。

  2. 这道题的重点也是如何判断两个相邻列之间填充的合法性, 以及如何枚举所有的方案(不重不漏) 。

    这里使用到了一个非常精妙的方法, 就是使用二进制表示每一行的表示的情况, 同样是使用二进制的1表示已经填充, 为了让接下来填充的竖着的方块合法, 那么就需要让连续的空着的方格的数量为偶数。以及相邻两列的填充情况不冲突(如果第i - 1列第j行填充了, 那么第i列第j行就不可以填充横向的方块)

#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>

using namespace std;
typedef long long LL;

int n, m;
const int N = 12, M = 1 << N;
LL f[N][M];
int st[M];
vector<int> state[M];

int main()
{
    
    //预处理一些合法情况
    //首先预处理单个情况填充的合法性
    
    while( cin >> n >> m, n || m ) 
    {
        for(int i = 0; i < 1 << n; ++i)
        {
            int cnt = 0;
            bool valid = true;
            
            for(int j = 0; j < n; ++j)
            {
                if( i >> j & 1)
                {
                    if( cnt & 1) {valid = false; break;} //1 **重点1
                    cnt = 0;
                }
                else cnt ++;
            }
            if(cnt & 1) valid = false;
            st[i] = valid;
        }
        
        
        //处理前后关系
        for(int i = 0 ; i < 1 << n; ++i)
        {
            state[i].clear();
            for(int j = 0; j < 1 << n; ++j)
            {
                if( ((i & j) == 0) && st[i | j]) state[i].push_back(j); //2 **重点2
            }
        }
        memset(f,0,sizeof f);
        f[0][0] = 1;
        // dp 开始
        for(int i = 1; i <= m; ++i)
            for(int j = 0; j < 1 << n; ++j)
                for(auto t : state[j])
                    f[i][j] += f[i - 1][t];
        cout << f[m][0] << endl;
    } 
    return 0;
}

例题2: 最短Hamilton路径

这道题需要求的是最短路径问题, 要求经过每一个点(每一个点都需要不重不漏的路过),求最短路径

我们发现状态压缩方法都有一个大前提, 就是数据范围一般 <= 二十多, 首先是因为2的二十多次方就已经快逼近空间限制了, 而且这种数据范围很少的一般都是使用状态压缩DP进行求解。

这道题也是DP思路的一个很经典的体现。f[i][j]i表示的是一个1~n位的一个二进制数, 表示一个路径, 二进制为1的点表示路过。j表示到达的点。比如f[i][j]表示的是最终到达j,并且路径是i。数组表示的数是最小距离

那么在将集合分类讨论的时候, 将f[i][j]这个集合分成从0 到其他n - 1个点k的最短距离加上 w[k][当前的点] 。这道题的DP思路很经典, 但是在写代码的时候处理二进制的时候还是有很多细节需要注意。

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;
const int N = 20, M = 1 << N;
int n;
int w[N][N];
int f[M][N];

int main()
{
    cin >>n;
    for(int i = 0; i < n; ++i)
    {
        for(int j = 0; j < n; ++j )
            cin >> w[i][j];
    }
    
    memset(f, 0x3f, sizeof f); //求最小值, 就需要将dp数组初始化成正无穷。相对应的求最大值, 那么初始化为0,或者负无穷。
    
    f[1][0] = 0;
    for(int i = 0;  i < 1 << n; ++i )
        for(int j = 0; j < n; ++ j)
        {
            if( i >> j & 1) // 确保这个路径经过j
            {
                //枚举倒数第二个点
                for(int k = 0; k < n; ++k )
                    if( (i - (1 << j)) >> k & 1) // 如果经过了k, 那么就使用经过k的最短路径更新
                        f[i][j] = min(f[i][j], f[i - (1 << j)][k] + w[k][j]);
            }
        }
    cout << f[(1 << n) - 1][n - 1] << endl;
    return 0;
}

树形DP

树形DP题目一般都是十分典型的, 一般都是有十分明显的提示告诉你这是一棵树, 有父节点和子节点, 然后父子节点之间有很明显的关系,树形DP一般想通了就很好写, 因为一般递推公式不是很难;

例题:没有上司的舞会

这道题每一个节点主要有两个状态,一个是选一个是不选,分别是f[u][1]f[u][0]

如果当前节点选的话那么f[u][1] += f[子节点][0]

如果当前节点不选 f[u][0] += max(f[子节点][0], f[子节点][1];

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;
const int N = 6e3 + 10;

int n, ne[N], e[N], h[N], idx;
int w[N];
int st[N]; // 表示一个点是否有父节点
int f[N][2];

void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}

void dfs(int u)
{
    f[u][1] = w[u];
    for(int i = h[u]; i != -1; i = ne[i])
    {
        int j = e[i];
        dfs(j);
        f[u][1] += f[j][0];
        f[u][0] += max(f[j][0], f[j][1]);
    }
}
int main()
{
    cin >> n;
    for(int i = 1; i <= n; ++i)
        cin >> w[i];
    memset(h, -1, sizeof h);
    
    for(int i = 1; i <= n -1; ++i )
    {
        int a, b;
        cin >> a >> b;
        add(b, a);
        st[a] = true; //表示有父节点
    }
    int root = 1;
    while(st[root]) root ++;
    
    dfs(root);
    
    cout << max(f[root][1], f[root][0]) << endl;
    
    return 0;
}

DP 疑难杂题

波动数列

1. 波动数列

这道题难在两个地方, 一个是公式的推导,另一个是想到DP并找到对应的状态表示;

公式的推导,首先需要将其转换成这种形式:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gIt3a63d-1645509840004)(C:\Users\Administrator\Desktop\ACM模板整理\images\image-20220211171614625.png)]

很容易看出x是可以任取的,所以优先把它提取到等式的一边;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qkAdE1eN-1645509840005)(C:\Users\Administrator\Desktop\ACM模板整理\images\image-20220211171750539.png)]

易得 s与[(n - 1) * d1 + (n - 2) * d2 + ... + dn - 1] 模 n同余(因为x是任取的)

得到上面的结论,第一阶段结束,开始第二阶段关于DP的分析;

问题变成 要找到这样一组d序列,满足 s与[(n - 1) * d1 + (n - 2) * d2 + ... + dn - 1] 模 n同余;

因为s与n是定量,所以余数是确定的;第一想法是爆搜,爆搜n个d,但是算了一下时间复杂度,最多要爆搜 2 1000 2^{1000} 21000个状态(最多有1000个d),显然不合理。使用DP优化。余数最大到n - 1, 可以爆搜余数(将余数变成一个状态);也就是选了x个a或者b之后,[(n - 1) * d1 + (n - 2) * d2 + ... + (n - x)dx] 这个式子与n的余数;

当前选择只有两个,选a或者b:选a则使用dp[n - 1][get_mod(j - a * (n - i), s)]去更新当前状态

选b则 同理;

最后我们得到的结果就是dp[n][get_mod(n, s)]


2. 求回文串的最大长度

有两种方法, 一种可以被严谨地证明出来,一种是网上比较多的方法(但未被严谨证明)。

写代码的时候, 我个人认为这两种方法的近似程度是非常高的,都是用了两个子集的并集去表示一个部分的状态。

  • 区间dp

    #include <iostream>
    #include <string.h>
    
    using namespace std;
    const int N = 1010;
    char s[N];
    int f[N][N]; //第二位表示的长度, 第一位表示线段的左端点
    
    int main()
    {
        cin >> s;
        int len = strlen(s);
        //区间dp最常见的技巧, 先遍历长度, 长度必然是从小到大进行搜索的。
        for(int i = 1; i <= len; ++i) 
        {
            for(int l = 0; l + i - 1 < len; ++l)
            {
                int r = l + i - 1;
                if(i == 1) f[l][r] = 1;
                else
                {
                    f[l][r] = max(f[l + 1][r], f[l][r - 1]);
                    if(s[l] == s[r]) f[l][r] = max(f[l][r], f[l + 1][r - 1] + 2);
                }
            }
        }
            
    	cout << f[0][len - 1] << endl; //最长回文串长度
        return 0;
    }
    
  • 求一个串与其翻转串的最长公共子序列

    #include <iostream>
    #include <cstring>
    #include <algorithm>
    using namespace std;
    
    const int N = 1e3 + 10;
    char s1[N], s2[N];
    int f[N][N];
    
    int main()
    {
        scanf("%s", s1 + 1);
        memcpy(s2, s1, sizeof s1);
        int len = strlen(s1 + 1);
        reverse(s2 + 1, s2 + 1 + len);
        for(int i = 1; i <= len; ++i)
        {
            for(int j = 1; j <= len; ++j)
            {
                if(s1[i] == s2[j]) f[i][j] = f[i - 1][j - 1] + 1;
                else f[i][j] = max(f[i - 1][j], f[i][j - 1]);
            }
        }
        cout << f[len][len] << endl;
        return 0;
    }
    

前缀和问题

求三角形前缀和需要注意的地方

  • 求三角形前缀和因为不是正方形,有一个角的值是缺失掉的,所以要特殊考虑

    //求三角形的前缀和
    #include <iostream>
    using namespace std;
    const int N = 100;
    
    int s[N][N];
    int num[N][N];
    
    int main()
    {
        for(int i = 1; i <= N - 1; ++i)
            for(int j = 1; j <= i; ++j)
                num[i][j] = 1;
        
        for(int i = 1; i <= N - 1; ++i)
        {
            for(int j = 1; j <= i; ++j)
                s[i][j] = s[i][j - 1] + s[i - 1][j] - s[i - 1][j - 1] + 1;
            s[i][i + 1] = s[i][i];
        }
        
        for(int i = 1; i <= 5; ++i)
        {
         for(int j = 1; j <= i; ++j)
            cout << s[i][j] << " " ;
        cout << endl;
        }
           
        return 0;
    }
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FGO7ZuJr-1645509840005)(C:\Users\Administrator\Desktop\ACM模板整理\images\image-20210725205236557.png)]

差分

爆搜

DFS

剪枝
  1. 优化搜索顺序
    大部分情况下, 我们应该优化搜索分支较少的节点。
  2. 排除等效冗余
    采用组合型枚举进行搜索
  3. 可行性剪枝
  4. 最优性剪枝
  5. 记忆化搜索(DP)
优化算法

DFS主要使用IDA* 算法进行优化,其实就是 迭代加深+剪枝**(用BFS的思想写DFS)**

迭代加深会在递归函数中限定深度,如果迭代深度超过那个深度就会返回;由于深度是由小到大递增的, 可以求一些最值问题, 可以在某些场景下替代BFS, 因为BFS需要较大的空间, 而DFS节省空间, 在剪枝得当的情况下, DFS并不比BFS慢多少

迭代加深算法的重点在于剪枝(乐观估计), 而且迭代加深比较方便剪枝。

当遇到最值问题,而且比较适合DFS做的情况下, 用迭代加深+乐观估计(可行性剪枝)

Addition Chains

tips: 这道题的核心在于剪枝, 后一项等于前面两项的加和, 最快的方式就是一直都是a[i] = a[i - 1] * 2;

max_d = log2(n) (在这道题里面) (不加这个剪枝就会变得非常慢)

#include <iostream>
using namespace std;
int n;
int max_d;
const int N = 110;

int a[N];

bool dfs(int depth, int now_d)
{
	if( now_d > depth && a[depth] == n) return true;
	for(int i = now_d - 1; i; --i)
	{
		for(int j = i; j; --j)
		{
			if(a[i] + a[j] > a[now_d - 1] && a[i] + a[j] <= n)
			{
				a[now_d] = a[i] + a[j];
				int s = a[now_d];
				s <<= (depth - now_d);
				if( s < n) continue; // 乐观估计, 比如其中一种是估计当前要找到最优解最少需要多少步,
				//如果当前深度加上这个步数大于了限定的下界,那么只好强制退出——等再一次扩宽下界时再搜这里。
				if(dfs(depth, now_d + 1)) return true;
			}
		}
	}
	return false;
}
int main()
{
	a[1] = 1;
	while(cin >> n && n) 
	{
		max_d = 1;		
		while((1 << max_d) < n) max_d ++;
		int depth = max_d;
		while(depth <= n && !dfs(depth, 2)) depth ++;
		for(int i = 1; i <= depth; ++i)
			cout << a[i] << " ";
		cout << endl;
	}
	return 0;
}

BFS

用空间换时间

四平方和

这道题需要枚举四个数字, 经过简单的思考转换成枚举三个数字, 每一层都最多需要枚举2300个数字, 最多 n 2 l o g ( n ) n^2log(n) n2log(n), 肯定是不行的。

先枚举两个数, 将结果存储到一个数组中;然后再重新枚举两个数, 剩下的两个数字通过二分去已经预处理的数组中查就可以了,预处理是 O ( n 2 ) O(\sqrt{n}^2) O(n 2)的(因为平方只需要枚举到 n \sqrt{n} n ),算法的瓶颈是排序 n l o g ( n ) nlog(n) nlog(n)

#include <iostream>
#include <cstring>
#include <algorithm>
const int N = 5e6 + 10;

using namespace std;
int n;

struct Node
{
    int s, a, b;
    bool operator < (const Node& t) const
    {
        if( s != t.s) return s < t.s;
        if( a != t.a) return a < t.a;
        return b < t.b;
    }
}nodes[N];

int main()
{
    cin >> n;
    int cnt = 0;
    //爆搜(用空间换时间,进行优化)
    for(int i = 0; i * i <= n; ++i)
    {
        for(int j = i; j * j + i * i <= n; ++j)
        {
            nodes[cnt] = {i * i + j * j, i, j};
            cnt ++;
        }
    }
    
    sort(nodes, nodes + cnt);
    
    for(int i = 0; i * i <= n; ++i)
    {    for(int j = i; j * j + i * i <= n; ++j)
        {
            int s = n - i * i - j * j;
            int l = 0, r = cnt - 1; 
            while( l < r)
            {
                int mid = l + r >> 1;
                if(nodes[mid].s >= s) r = mid;
                else l = mid + 1;
            }
            if(nodes[l].s == s)
            {
                cout << i << " " << j << " " << nodes[l].a << " "<< nodes[l].b << endl;
                return 0;
                
            }
        }
    }
    return 0;
}

一些爆搜问题的解题思路

  1. 分成互质组

    这道题的思路比较新颖, 这是一个分组枚举问题, 问题是最少可以分成多少组, 我的第一思路是枚举最少的组数,迭代加深, 但是这样的思路时间复杂度较高, 而且不一定好做;

    y总的思路就是每一次都将元素放在最后一个组, 可以有两个分支(选择):

    • 放在这个组(如果允许的话)
    • 新开一个组
    #include <iostream>
    using namespace std;
    const int N = 10;
    int a[N];
    int n;
    int ans = N;
    
    int g[N][N];
    int st[N];
    
    int gcd(int a, int b)
    {
    	return b ? gcd(b, a % b) : a;
    }
    
    bool check(int g[], int num, int start)
    {
    	for(int i = 0; i < start; ++i)
    	{
    		if(gcd(g[i], num) > 1) return false;
    	}
    	return true;
    }
    void dfs(int gr, int gc, int start, int cnt)
    {
    	if( gr >= ans) return;
    	if( cnt == n) ans = gr;
    	bool flag = true;
    	for(int i = start; i < n ; ++i)
    	{
    		if( !st[i] && check(g[gr], a[i], gc))
    		{
    			st[i] = true;
    			g[gr][gc] = a[i];
    			dfs(gr, gc + 1, start + 1, cnt + 1);
    			flag = false;
    			st[i] = false;
    		}
    	}
    	
    	if(flag) dfs(gr + 1, 0, 0, cnt);
    }
    int main()
    {
    	cin >> n;	
    	for(int i = 0; i < n; ++i)
    	{
    		cin >> a[i];	
    	}
    	dfs(1, 0, 0, 0);
    	cout << ans << endl;
    	return 0;
     } 
    
  2. 可以自定义顺序搜索,排除等效冗余

    数的划分

    将整数 nn 分成 kk 份,且每份不能为空,任意两个方案不相同(不考虑顺序)

    例如:n=7n=7,k=3k=3,下面三种分法被认为是相同的。

    1,1,51,1,5;
    1,5,11,5,1;
    5,1,15,1,1.

    问有多少种不同的分法。

​ 这道题的主要冗余部分(也是我之前写的时候没有想到的部分)是 任意两个相同方案(不考虑顺序)只算一 次,我原本的思想是用st数组判重, 但是超时了。可以从搜索这个根源上解决问题,在搜索的时候就采用升序 搜索,下一个搜索的数字大于等于当前搜索的数字,这样就可以直接去重了,而且搜索量非常少。

#include<cstdio>

int n,k,cnt;

void dfs(int last,int sum,int cur)
{
    if(cur==k)
    {
        if(sum==n) cnt++;
        return;
    }
    //这个剪枝非常关键, 认为定义一个顺序,这样就可以去除冗余搜索
    for(int i=last;sum+i*(k-cur)<=n;i++)//剪枝,只用枚举到sum+i*(k-cur)<=n为止
        dfs(i,sum+i,cur+1);
}

int main()
{
    scanf("%d%d",&n,&k);
    dfs(1,0,0);
    printf("%d",cnt);
}
  1. 连续区间有一个很重要的性质,起点和长度,枚举的话可以浓缩成这两个性质。

线段树

零碎的知识点

  1. 对一个数字四舍五入

    cout << (int)(num + 0.5) << endl;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值