数论
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;
}
质数分解一个阶乘数
题目意思是给一个阶乘数n
(n
<= 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})
如果一个数x的质因数分解:x=p1c1p2c2...pncn那么φ(x)=x(1−c11)(1−c21)...(1−cn1)
但是如果要使用很多数字的的欧拉函数,那么就需要使用到线性筛法求欧拉函数
#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(k∈Z)
这个问题可以使用扩展欧几里得算法求解:
证明:
欧
几
里
得
算
法
:
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′+(a−⌊ba⌋∗b)y′=gcd(b,a%b)对这个等式进行整理:ax+by=ay′+b(x′−⌊ba⌋∗y′){x=y′y=x′−⌊ba⌋∗y′因为使用欧几里得算法作为递归的条件,所以递归结束的时候是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=x0−db∗ky=y0+da∗k(k∈Z)
所以如果要求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(x0−b1t)+b(y0−a1t)=ax0+by0−t(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(x−x0)+b(y−y0)=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(x−x0)+gcdb(y−y0)=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} (x−x0)∣gcdb 和 ( y − y 0 ) ∣ a g c d (y - y_0) | \frac{a}{gcd} (y−y0)∣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)2→ak=a21c∗a22c...∗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二进制分解\\ 那么同样可以使用相同的思路,只不过乘法换成了加法\\ a∗k=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 两两互质)
⎩⎪⎨⎪⎧x≡a1(mod m1)x≡0(mod m2)x≡0(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 两两互质)
⎩⎪⎨⎪⎧x≡0(mod m1)x≡a2(mod m2)x≡0(mod m3) (m1,m2,m3两两互质)⎩⎪⎨⎪⎧x≡0(mod m1)x≡0(mod m2)x≡a3(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′+k∗∏1nmn (k∈Z)
这样我们就可以从特解到通解
同余的乘性(左右两边同时乘以一个常数等式依然成立):
$ 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)倍的放缩即可
⎩⎪⎨⎪⎧a′≡1(mod m1)a′≡0(mod m2)a′≡0(mod m3) ⎩⎪⎨⎪⎧b′≡0(mod m1)b′≡1(mod m2)b′≡0(mod m3) ⎩⎪⎨⎪⎧c′≡0(mod m1)c′≡0(mod m2)c′≡1(mod m3)∴x=a1∗a′+a2∗b′+ac∗c′这样的式子就满足上面一开始的同余方程组(使用到了同余的加性和乘性)再对x做1∏nmn (k∈Z)倍的放缩即可
扩展中国剩余定理
原先要满足 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;
}
一些数论中作题目知道的神奇定理
-
如果
a b
两个数互质,那么定义一个集合,集合中包含 a ∗ k % b ( k ∈ [ 0 , a − 1 ] ) a * k \% b\ (k \in [0, a - 1]) a∗k%b (k∈[0,a−1]) (当a > b 时会有重复), 但这个集合中包含了0 - (b - 1)
的所有数字。a ∗ x + b ∗ y a * x + b * y a∗x+b∗y 在数轴上表示的所有数字可以被拆分成a份, 如果即记 a ∗ x + b ∗ y = c a * x + b * y = c a∗x+b∗y=c 那么 c % a c \% a c%a 一定是落在a范围内的, 而 b ∗ y b * y b∗y决定了c落在a范围内的具体位置(就是决定了偏移)所以 b ∗ y % a b * y \% a b∗y%a就是c落在a范围内的数值。
这个时候我们利用少那个面介绍的一个小定理就可以很快地知道, { k ∗ b ∗ y % a } ( k ∈ [ 0 , a ] ) \{ k * b *y \% a\}\ (k \in [0, a]) {k∗b∗y%a} (k∈[0,a]) 表示的集合中的所有数恰好是0 - a 中的数字,一个不落。
-
如果 ( a − b ) % k = = 0 (a-b) \% k == 0 (a−b)%k==0 说明 a, b同余(虽然好像说的是废话,但是在做一些题目的时候, 这句话还是蛮有用的)
#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问题考虑的几个步骤
- 确定集合, 就是
f[i][j]
的意义 - 确定集合表达式的属性(可以是最大值,最小值或者数量)
- 确定转移方程,这一步是非常关键的,一般根据具体题目意思而定。
- 确定边界(一般是
i
或者j
为0
的情况)
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]);
-
第一次优化之后的版本
核心思想:
所以核心的更新方程变成了
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表示已经填充, 为了让接下来填充的竖着的方块合法, 那么就需要让连续的空着的方格的数量为偶数。以及相邻两列的填充情况不冲突(如果第
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 疑难杂题
波动数列
这道题难在两个地方, 一个是公式的推导,另一个是想到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
剪枝
- 优化搜索顺序
大部分情况下, 我们应该优化搜索分支较少的节点。 - 排除等效冗余
采用组合型枚举进行搜索 - 可行性剪枝
- 最优性剪枝
- 记忆化搜索(DP)
优化算法
DFS主要使用IDA* 算法进行优化,其实就是 迭代加深+剪枝**(用BFS的思想写DFS)**
迭代加深会在递归函数中限定深度,如果迭代深度超过那个深度就会返回;由于深度是由小到大递增的, 可以求一些最值问题, 可以在某些场景下替代BFS, 因为BFS需要较大的空间, 而DFS节省空间, 在剪枝得当的情况下, DFS并不比BFS慢多少
迭代加深算法的重点在于剪枝(乐观估计), 而且迭代加深比较方便剪枝。
当遇到最值问题,而且比较适合DFS做的情况下, 用迭代加深+乐观估计(可行性剪枝)
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(n2)的(因为平方只需要枚举到 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;
}
一些爆搜问题的解题思路
-
这道题的思路比较新颖, 这是一个分组枚举问题, 问题是最少可以分成多少组, 我的第一思路是枚举最少的组数,迭代加深, 但是这样的思路时间复杂度较高, 而且不一定好做;
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; }
-
可以自定义顺序搜索,排除等效冗余
将整数 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);
}
- 连续区间有一个很重要的性质,起点和长度,枚举的话可以浓缩成这两个性质。
线段树
零碎的知识点
-
对一个数字四舍五入
cout << (int)(num + 0.5) << endl;