Algorithm
数学基础运算快速冥/乘高精度加 减 乘 除 模 幂 python高精数学运算sqrtlog除法取整分式运算BigInt初等数论约数公约数最大公约数扩展欧几里得最小公倍数分解只因数筛法求质因数Pollard_Rho约数个数约数之和素数Miller-Rabin素数筛威尔逊定理欧拉函数欧拉筛欧拉定理拓展欧拉定理 (欧拉降幂)模数同余定理线性同余高次同余乘法逆元中国剩余定理数论分块其它组合数学排列组合 杨辉三角逆元求组合数卢卡斯定理高精度排列组合错位排列容斥原理数列等差/等比数列递推斐波那契广义斐波那契数列皮萨诺周期卡特兰数贝尔数 (集合划分)康托展开 (全排列排名)线性代数矩阵矩阵快速幂矩阵封装行列式线性基概率论期望方差数值算法数值积分高斯消元插值博弈论Nim游戏SG函数计算几何距离扫描线面积并周长并二维数点平面几何点线封装其它中位数对顶堆主席树均值不等式约瑟夫环吉姆拉尔森日期公式二维和一维坐标转化基础算法前缀和&差分一维前缀和二维前缀和一维差分二维差分二分二分查找二分答案实数域二分分数规划子段和问题三分双指针排序sort快速排序第k小的数归并排序逆序对分治二进制lowbit状态压缩位运算^ 异或& 与>> 移位离散化搜索DFS剪枝迭代加深BFS双端队列BFS优先队列BFS双向搜索A*IDA*Dancing Links精确覆盖重复覆盖倍增ST表根号分治数据结构基本数据结构链表单链表双链表栈表达式计算单调栈队列单调队列滑动窗口最大子段和堆二叉堆左偏树配对堆哈希开放寻址法拉链法并查集边带权并查集拓展域并查集分块树状数组一维树状数组权值树状数组静态区间颜色数线段树单点修改,区间查询区间修改,区间查询,lazy标记权值线段树霍夫曼树字符串字符串哈希双哈希KMP前缀统计循环节最长公共子串exKMPTrie字典树字符串统计前缀统计最大异或对AC自动机回文串Manacher最小表示法后缀数组排序+哈希+二分倍增实现height数组其它图论树和图的存储邻接矩阵邻接表树和图的遍历DFSDFS序列连通块划分BFS拓扑排序最短路单源最短路仅正权边Dijkstra朴素Dijkstra堆优化含负权边Bellman-FordSPFA多源最短路Floyd传递闭包Johnson(LCA)树上问题树的重心树的中心树的直径最近公共祖先树上差分树哈希生成树最小生成树KruskalPrim朴素Boruvka唯一性次小生成树生成树计数最小树形图二分图二分图判定最大匹配匈牙利算法HK 算法转为网络流模型常见问题模型最大权匹配KM算法转为费用流模型最小点覆盖最大独立集最小路径覆盖一般图最大匹配一般图最大权匹配基环树负环SPFA判负环bellman-ford判负环差分约束连通性相关割点和割边双连通分量边双连通分量点双连通分量强连通分量2-SAT网络流最大流Edmondsd-KarpDinic最小割最大权闭合图费用流欧拉图动态规划背包问题01背包完全背包多重背包分组背包二维费用背包求具体方案求方案数有依赖的背包问题线性DP数字三角形最长上升子序列最长公共子序列编辑距离区间DP一维二维计数DP数位DP记忆化搜索试填法状压DPbitset优化重复覆盖问题树形DP树上背包换根DP记忆化搜索DP优化单调队列优化数据结构优化斜率优化其他环形处理滚动数组状态机DP贪心区间问题区间选点区间合并不相交区间区间覆盖区间分组区间包含绝对值不等式排序不等式后悔解法其它字符与数字转换进制转换精度/溢出问题输入输出优化交互题一些语法/标准重载运算符Lambda表达式预编译头文件gcc标准__int128__cplusplus文件读写时间复杂度握手定理分块打表随机数生成.vimrcOI
待填坑: 数学:莫比乌斯反演、多项式与生成函数、快速傅里叶变换、快速数论变换、计算几何、原根、Pick定理、线性基、母函数、 基础算法:莫队、倍增、 字符串:exKMP、AC自动机、后缀自动机、可持久化Trie、回文树、 图论:树链剖分、基环树、树分治、圆方树、 数据结构:主席树、平衡树、笛卡尔树、动态树、可持久化数据结构、 动态规划:计数DP、插头DP、期望/概率DP、
数学
基础运算
快速冥/乘
1//求a^b对p取模的值2long long qmi(long long a, long long b,long long p) {3 long long ans = 1;4 while (b) {5 if (b&1) {//如果指数为奇数6 ans = ans*a%p;//收集好指数为奇数时分离出来的一次方,(不可写为ans*=a%p)7 }8 b >>= 1; //指数折半9 a = a*a%p; //底数变平分10 }11 return ans%p;12}x1//龟速乘2//求a*b对p取模的值3long long qmx(long long a, long long b, long long p) {4 long long ans = 0;5 while (b) {6 if (b & 1) ans = (ans + a) % p;7 b >>= 1;8 a = (a + a) % p; 9 }10 return ans;11}12
13//__int12814long long qmx(long long a, long long b, long long p) {15 return __int128(a) * b % p;16}17
18//转为浮点运算(模数p不超过int)19long long qmx(long long a, long long b, long long p) {20 a %= p; b %= p;21 long long r = a * b - p*(long long)(1.0L / p * a * b);22 return r - p * (r >= p) + p * (r < 0);23}
求
xxxxxxxxxx21//https://vjudge.net/problem/LightOJ-12822int p = pow(10,k*log10(n) - floor(k*log10(n))) * 100;
高精度
加 减 乘 除 模 幂
//按住ctrl点击跳转
加
xxxxxxxxxx331// A+B 234using namespace std;5string a, b;6vector<int> A, B;7vector<int> add(vector<int>&A, vector<int>&B) {8 vector<int>C;9 int t = 0;10 for (int i = 0; i < A.size() || i < B.size(); i++){11 if (i < A.size()) t += A[i];12 if (i < B.size()) t += B[i];13 C.push_back(t % 10); //无论是否有进位,都取 %10的余数14 t /= 10; //判断是否有进位15 }16 if (t) C.push_back(1); //如果有最高位还有进位则在C数组最后加元素117 return C;18}19int main() {20 cin >> a >> b; //a = 12345621 for (int i = a.size() - 1; i >= 0; i--) {22 A.push_back(a[i] - '0'); //A = {6,5,4,3,2,1}23 }24 for (int i = b.size() - 1; i >= 0; i--) {25 B.push_back(b[i] - '0');26 }27
28 A = add(A, B); //auto 进行类型自动转换 在此相当于vector<int>;29
30 for (int i = A.size() - 1; i >= 0; i--) {31 printf("%d", A[i]);32 }33}
减
xxxxxxxxxx421// A-B234using namespace std;5string a, b;6vector<int>A, B, C; //判断a与b的大小7bool cmp(vector<int>& A, vector<int>& B) {8 if (A.size() != B.size()) return A.size() > B.size();//先比较位数9 for (int i = A.size() - 1; i >= 0; i--) { //位数相同则从高位依次比下来10 if (A[i] != B[i]) return A[i] > B[i];11 }12 return 1;13}14
15vector<int>sub(vector<int>& A, vector<int>& B) {//A >= B16 vector<int>C;17 int t = 0;18 for (int i = 0;i < A.size();i++){19 t+=A[i];20 if (i < B.size()) t -= B[i];21 C.push_back((t + 10) % 10); //保证相减后取正数22 if (t < 0) t = -1; //判断是否要借位23 else t = 0;24 }25 while (C.size() > 1 && C.back() == 0) C.pop_back();//去除前导0,pop_back删除容器中最后一个元素26 return C;27}28
29int main() {30 cin >> a >> b;31 for (int i = a.size() - 1; i >= 0; i--) {32 A.push_back(a[i] - '0');33 }34 for (int i = b.size() - 1;i >= 0;i--){35 B.push_back(b[i] - '0');36 }37
38 if (cmp(A, B)) A = sub(A, B);39 else A = sub(B, A);40 41 for (int i = A.size() - 1; i >= 0; i--) printf("%d", A[i]);42}
乘
xxxxxxxxxx111//A*b O(N)2vector<int>mul(vector<int>& A, int b) {3 int t = 0;4 vector<int>C;5 for (int i = 0; i < A.size() || t; i++) { //注意加上||t;6 if (i < A.size()) t += A[i] * b; //将b当做一个整体分别与a的每一位相乘,再加上进位7 C.push_back(t % 10); //C的每一位取其%108 t /= 10; //计算进位9 }10 return C;11}
xxxxxxxxxx141//A*B O(N*M)2vector<int> mul(vector<int> &A,vector<int> &B)3 vector<int> C(A.size()+B.size());4 for(int i=0;i<A.size();i++)5 for(int j=0;j<B.size();j++)6 C[i+j]+=A[i]*B[j];7 for(int i=0,t=0;i<res.size();i++){8 t+=C[i];9 C[i]=t%10;10 t/=10;11 }12 while(C.size() >= 2 && C.back()==0) C.pop_back();13 return C;14}
xxxxxxxxxx661//A*B FFT实现 O((N+M)log(N+M))2//https://www.luogu.com.cn/problem/P19193const long double PI = acosl(-1.0);4void FFT(std::vector<std::complex<long double>>& a, bool invert) {5 int n = a.size();6 if(n == 0) return;7
8 for(int i = 1, j = 0; i < n; i++) {9 int bit = n >> 1;10 for (; j & bit; bit >>= 1) j ^= bit;11 j ^= bit;12 if (i < j) std::swap(a[i], a[j]);13 }14
15 for(int len = 2; len <= n; len <<= 1) {16 long double ang = 2 * PI / len * (invert ? -1 : 1);17 std::complex<long double> wlen(cosl(ang), sinl(ang));18 for (int i = 0; i < n; i += len) {19 std::complex<long double> w(1.0);20 for (int j = 0; j < len / 2; j++) {21 std::complex<long double> u = a[i + j];22 std::complex<long double> v = a[i + j + len / 2] * w;23 a[i + j] = u + v;24 a[i + j + len / 2] = u - v;25 w *= wlen;26 }27 }28 }29 if(invert) {30 for (std::complex<long double>& x : a) x /= n;31 }32}33
34std::vector<long long> operator * (const std::vector<long long>&A,const std::vector<long long>&B){35 std::vector<long long>C;36 int n = A.size(), m = B.size();37 if((n == 1 && A[0] == 0) || (m == 1 && B[0] == 0)) return {0};38
39 int MAX_N = 1;40 while (MAX_N < n + m) MAX_N <<= 1;41
42 std::vector<std::complex<long double>> a(MAX_N, 0.0), b(MAX_N, 0.0);43 for(int i = 0; i < n; i++) a[i] = std::complex<long double>(A[i], 0);44 for(int i = 0; i < m; i++) b[i] = std::complex<long double>(B[i], 0);45
46 FFT(a, false);47 FFT(b, false);48 for(int i = 0; i < MAX_N; i++) a[i] *= b[i];49 FFT(a, true);50
51 std::vector<long long> temp(n + m, 0);52 for(int i = 0; i < n + m; i++) {53 long long val = (long long)(a[i].real() + 0.5);54 temp[i] = val;55 }56
57 long long carry = 0;58 for(int i = 0; i < n + m; i++) {59 long long total = temp[i] + carry;60 C.push_back(total % 10);61 carry = total / 10;62 }63 if(carry) C.push_back(carry);64 while(C.size() >= 2 && C.back() == 0) C.pop_back();65 return C;66}
除
xxxxxxxxxx351//A/b及其余数 2345using namespace std;6vector<int>A;//C为商7
8vector<int>div(vector<int>&A,int b,int&r){ //r是引用9 vector<int>C;10 for (int i = A.size() - 1;i >= 0;i--){ //除法此处倒序,然后再翻转11 r = r * 10 + A[i]; //上一位的余数*10再加上本位12 C.push_back(r / b); //将其对b的商记录进C数组13 r %= b; //然后变为其对b的余数供下一位使用14 }15 reverse(C.begin(), C.end());16 while (C.size() > 1 && C.back() == 0) C.pop_back();17 return C; 18}19
20int main() {21 string a; int b, r = 0; //r为余数22 cin >> a >> b;23 for (int i = a.size() - 1; i >= 0; i--) {24 A.push_back(a[i] - '0');25 }26
27 A = div(A, b, r);28
29 for (int i = A.size() - 1;i >= 0;i--){30 printf("%d", A[i]);31 }32 cout << endl << r << endl;33
34 return 0;35}
xxxxxxxxxx81//a/b 保留k位小数2long long a,b,k;cin >> a >> b >> k;3cout << a/b << '.';4a = a%b*10;5while(k--){6 cout << a/b;7 a = a%b*10;8}xxxxxxxxxx31//求a/b的第k位小数 相当于a*10^k/b%102long long a,b,k;cin >> a >> b >> k;3cout << a*qmi(10,k-1,b)*10/b%10;
模
给两个正整数a,b,输出他们的最大公约数 a<=1e10^6,b <= 1e9
首先有以下性质 1.(a+b)%mod等价于a%mod+b%mod 2.a * b%mod 等价于 a%mod*b%mod(仅当a * b没有溢出时) 该题求解gcd(a,b)a是大数 根据辗转相除法 gcd(a,b)=gcd(b,a%b) 因此我们可以先求a%b把a限制在1e9的范围内,然后做gcd 因为a很大,又可以表示为
(其中n为字符串的长度,ai为第i个字符) 又由性质1和2,我们就可以对每个ai求mod,同时通过乘和累加求出
xxxxxxxxxx211//A%b2//https://ac.nowcoder.com/acm/contest/86034/D34using namespace std;5using ll = long long;6
7ll gcd(ll a,ll b){return b?gcd(b,a%b):a;}8
9ll qmod(string& a,ll b){//高精度A % 低精度b10 ll t = 0;11 for(int i = 0;i < a.size();i++) {12 t=(t*10+a[i]-'0')%b;13 }14 return t;15}16
17int main(){18 string a;cin >> a;19 ll b;cin >> b;20 cout << gcd(b,qmod(a,b));21}
幂
xxxxxxxxxx171//中精度 2^n 2*n <= 30000 //n可以为负数2//仅适用于计算2^n的精确值34567using namespace std;8
9int main(){10 int n; cin>>n;11 stringstream ss;12 ss << fixed << setprecision(n>0?0:-n) << pow(2.0L,n);13 //string s = ss.str(); //字符串流,也可以用sprintf14 string s; ss >> s;15
16 cout << s;17}
xxxxxxxxxx101//高精度快速幂 A^b 一般b取不了太大2Bigint qmi(Bigint &a,int b){3 Bigint ans = 1;4 while(b){5 if(b&1) ans = ans*a;6 b >>= 1;7 a = a*a;8 }9 return ans;10}
python高精
xxxxxxxxxx101import sys2sys.set_int_max_str_digits(100005) #修改最大位数3a = int(input())4b = int(input())5print(a+b) #加6print(a-b) #减7print(a*b) #乘8print(a//b) #除9print(a%b) #模10print(a**b) #幂
数学运算
sqrt
xxxxxxxxxx71//向下取整2long long qsqrt(long long n) { 3 long long s = std::sqrt(n);4 while (s*s > n) { s--; }5 while ((s+1)*(s+1) <= n) { s++; }6 return s;7}
log
xxxxxxxxxx211//向上取整2long long logi(long long a, long long b) {//log(a,b) a^t ≥ b3 long long t = 0;4 long long v = 1;5 while (v < b) {6 v *= a;7 t++;8 }9 return t;10}11
12long long llog(long long a, long long b) {//loglog(a,b) a^(a^t) ≥ b13 if (a <= b) {14 int l = logi(a, b);15 return (l == 0 ? 0 : std::__lg(2 * l - 1));16 }17 assert(b != 1);18 long long l = logi(b, a + 1) - 1;19 assert(l > 0);20 return -std::__lg(l);21}
xxxxxxxxxx51//预处理log2, (向下取整)2lg2[0] = -1;3for(int i = 1;i < N;i++){4 lg2[i] = lg2[i>>1]+1;5}
除法取整
xxxxxxxxxx91long long ceilDiv(long long n, long long m) {//上取整2 if (n >= 0) return (n + m - 1) / m;3 else return n / m;4}5 6long long floorDiv(long long n, long long m) {//向下取整7 if (n >= 0) return n / m;8 else return (n - m + 1) / m;9}
分式运算
源自jiangly分数四则运算 博客园 (cnblogs.com)
Frac a(1,3); 表示+ - * / 和比较大小
xxxxxxxxxx891template<class T>2struct Frac {// num/den3 T num;4 T den;5 Frac(T num_, T den_) : num(num_), den(den_) {6 if (den < 0) {7 den = -den;8 num = -num;9 }10 }11 Frac() : Frac(0, 1) {}12 Frac(T num_) : Frac(num_, 1) {}13 explicit operator double() const {14 return 1. * num / den;15 }16 explicit operator long long() const{17 return num / den;18 }19 friend long long floor(const Frac &x){20 if(x.num >= 0) return x.num / x.den;21 else return (x.num - x.den + 1) / x.den;22 }23 friend long long ceil(const Frac &x){24 if(x.num >= 0) return (x.num + x.den - 1) / x.den;25 else return x.num / x.den;26 }27 Frac &operator+=(const Frac &rhs) {28 num = num * rhs.den + rhs.num * den;29 den *= rhs.den;30 return *this;31 }32 Frac &operator-=(const Frac &rhs) {33 num = num * rhs.den - rhs.num * den;34 den *= rhs.den;35 return *this;36 }37 Frac &operator*=(const Frac &rhs) {38 num *= rhs.num;39 den *= rhs.den;40 return *this;41 }42 Frac &operator/=(const Frac &rhs) {43 num *= rhs.den;44 den *= rhs.num;45 if (den < 0) {46 num = -num;47 den = -den;48 }49 return *this;50 }51 friend Frac operator+(Frac lhs, const Frac &rhs) {52 return lhs += rhs;53 }54 friend Frac operator-(Frac lhs, const Frac &rhs) {55 return lhs -= rhs;56 }57 friend Frac operator*(Frac lhs, const Frac &rhs) {58 return lhs *= rhs;59 }60 friend Frac operator/(Frac lhs, const Frac &rhs) {61 return lhs /= rhs;62 }63 friend Frac operator-(const Frac &a) {64 return Frac(-a.num, a.den);65 }66 friend bool operator==(const Frac &lhs, const Frac &rhs) {67 return lhs.num * rhs.den == rhs.num * lhs.den;68 }69 friend bool operator!=(const Frac &lhs, const Frac &rhs) {70 return lhs.num * rhs.den != rhs.num * lhs.den;71 }72 friend bool operator<(const Frac &lhs, const Frac &rhs) {73 return lhs.num * rhs.den < rhs.num * lhs.den;74 }75 friend bool operator>(const Frac &lhs, const Frac &rhs) {76 return lhs.num * rhs.den > rhs.num * lhs.den;77 }78 friend bool operator<=(const Frac &lhs, const Frac &rhs) {79 return lhs.num * rhs.den <= rhs.num * lhs.den;80 }81 friend bool operator>=(const Frac &lhs, const Frac &rhs) {82 return lhs.num * rhs.den >= rhs.num * lhs.den;83 }84 friend std::ostream &operator << (std::ostream &os, Frac x) {85 T g = std::gcd(x.num, x.den);86 if (x.den == g) { return os << x.num / g; } //87 else { return os << x.num / g << "/" << x.den / g; }88 }89};
BigInt
高精度整数运算,不支持负数运算(待完善),写得一坨,勉强能用只能说是==
| 操作 | A op b (高精度op低精度) | A op B (高精度op高精度) |
|---|---|---|
| 加法 | + 、+= | + 、+= |
| 减法(仅限大op小) | -、 -= | -、 -= |
| 乘法 | *、 *= | *、 *= (O(N*M)模拟),不推荐,建议换FFT) |
| 除法(下取整) | /、 /= | |
| 取模 | % 、%= | |
| 比较大小 | > < == != >= <= | > < == != >= <= |
允许直接A/B、A%B 但需要保证B在整型范围内(B <= 9.2e17)
初始化
xxxxxxxxxx11Bigint A(123);xxxxxxxxxx11Bigint A("123");xxxxxxxxxx21int n = 123;2Bigint A = n;xxxxxxxxxx21string s = 123;2Bigint A = s;xxxxxxxxxx11cin >> A;
xxxxxxxxxx1641struct Bigint{2 std::vector<long long> a;3
4 Bigint (){ }5
6 Bigint(const std::string &s){7 for(int i = s.size()-1;i >= 0;i--){8 if(s[i] >= '0' && s[i] <= '9') a.emplace_back(s[i] - '0');9 }10 while(a.size() >= 2 && a.back() == 0) a.pop_back();11 }12
13 Bigint(long long x){14 if(x == 0) {a.emplace_back(0); return;}15 if(x < 0) x = ~x + 1;16 while(x) a.emplace_back(x%10),x/=10;17 }18
19 friend long long Bigint_to_int(const Bigint &B){20 long long b = 0;21 for(int i = B.a.size()-1;i >= 0;i--){22 b = b*10 + B.a[i];23 }24 return b;25 }26
27 Bigint operator + (const Bigint &B){28 Bigint C;29 std::vector<long long>&c = C.a;30 const std::vector<long long>&b = B.a;31 long long t = 0;32 for(int i = 0;i < a.size() || i < b.size() || t;i++){33 if(i < a.size()) t += a[i];34 if(i < b.size()) t += b[i];35 c.emplace_back(t%10);36 t/=10;37 }38 return C;39 }40
41 Bigint operator + (long long b){42 return *this + Bigint(b);43 }44
45 Bigint operator * (long long b){46 long long t = 0;47 Bigint C;48 std::vector<long long>&c = C.a;49 for(int i = 0;i < a.size() || t;i++){50 if(i < a.size()) t += a[i]*b;51 c.emplace_back(t%10);52 t/=10;53 }54 while(c.size() >= 2 && c.back() == 0) c.pop_back();55 return C;56 }57
58 Bigint operator * (const Bigint &B){59 const auto &A = this->a;60 Bigint C;61 C.a = std::vector<long long>(A.size()+B.a.size());62 for(int i = 0;i < A.size();i++){63 for(int j = 0;j < B.a.size();j++){64 C.a[i+j] += A[i]*B.a[j];65 }66 }67 long long t = 0;68 for(int i = 0;i < C.a.size();i++){69 t += C.a[i];70 C.a[i] = t%10;71 t /= 10;72 }73 while(C.a.size() >= 2 && C.a.back() == 0) C.a.pop_back();74 return C;75 }76
77 Bigint operator - (const Bigint &B){78 Bigint C;79 std::vector<long long>&c = C.a;80 const std::vector<long long>&b = B.a;81 long long t = 0;82 for(int i = 0;i < a.size();i++){83 t += a[i];84 if(i < b.size()) t -= b[i];85 c.emplace_back((t+10)%10);86 if(t < 0) t = -1;87 else t = 0;88 }89 while(c.size() >= 2 && c.back() == 0) c.pop_back();90 return C;91 }92
93 Bigint operator - (long long b){94 return *this - Bigint(b);95 }96
97 Bigint operator / (long long b){98 Bigint C;99 std::vector<long long>&c = C.a;100 long long t = 0;101 for(int i = a.size()-1;i >= 0;i--){102 t = t*10 + a[i];103 c.emplace_back(t/b);104 t %= b;105 }//t as the remainder == A%b106 reverse(c.begin(),c.end());107 while(c.size() >= 2 && c.back() == 0) c.pop_back();108 return C;109 }110
111 Bigint operator / (const Bigint &B){112 return (*this)/Bigint_to_int(B);113 }114
115 Bigint operator % (long long b){116 long long t = 0;117 for(int i = a.size()-1;i >= 0;i--){118 t = (t*10 + a[i]) %b;119 }120 return Bigint(t);121 }122
123 Bigint operator % (const Bigint &B){124 return (*this)%Bigint_to_int(B);125 }126
127 int cmp (const Bigint &B){128 const std::vector<long long>&b = B.a;129 if(a.size() != b.size()) {130 return a.size() > b.size() ? 1 : -1;131 }132 for(int i = a.size()-1;i >= 0;i--){133 if(a[i] != b[i]){134 return a[i] > b[i] ? 1 : -1;135 }136 }137 return 0;138 };139
140 void operator += (const auto &b){*this = *this + b;}141 void operator -= (const auto &b){*this = *this - b;}142 void operator *= (const auto &b){*this = *this * b;}143 void operator /= (const auto &b){*this = *this / b;}144 void operator %= (const auto &b){*this = *this % b;}145 bool operator > (const auto &b){return cmp(b) == 1;}146 bool operator < (const auto &b){return cmp(b) == -1;}147 bool operator == (const auto &b){return cmp(b) == 0;}148 bool operator != (const auto &b){return cmp(b) != 0;}149 bool operator >= (const auto &b){return cmp(b) != -1;}150 bool operator <= (const auto &b){return cmp(b) != 1;}151
152 friend std::ostream &operator << (std::ostream &o,const Bigint &t){153 for(int i = t.a.size()-1;i >= 0;i--){154 o << t.a[i];155 }156 return o;157 }158
159 friend std::istream &operator >> (std::istream &o,Bigint &t){160 std::string s;o >> s;161 t = s;162 return o;163 }164};
初等数论
常见符号
整除符号:
,表示 整除 ,即 是 的因数。取模符号:
,表示 除以 得到的余数。互质符号:
,表示 , 互质。最大公约数:
,在无混淆意义的时侯可以写作 。最小公倍数:
,在无混淆意义的时侯可以写作 。
约数
平凡约数(平凡因数):对于整数
对于整数
公约数
求一个数g的所有约数,或两个数x,y的所有公约数 f(30) = {1 2 3 5 6 10 15 30} f(30,5) = {1,5}
xxxxxxxxxx251234using namespace std;5int gcd(int a,int b){return b?gcd(b,a%b):a;}6vector<int>v;7int main(){8 int x,y;cin >> x >> y;9 int g = gcd(x,y);10 11 //预处理所有公约数,这些公约数一定是最大公约数的约数12 for (int i = 1; i <= g/i; ++i) {13 if(g%i == 0) {14 v.push_back(i);15 if(i!=g/i) v.push_back(g/i);16 } 17 } 18 sort(v.begin(),v.end());//排序19 20 for (int i = 0; i < v.size(); ++i) {21 cout << v[i] << ' ';22 }23
24 return 0;25}
最大公约数
AcWing 872. 六种方法求最大公约数及其时间复杂度分析 - AcWing
xxxxxxxxxx21//GNU编译器2__gcd(a,b); //#include <algorithm> 返回a,b的最大公约数xxxxxxxxxx151//二进制优化,快个两三倍2int gcd(int a,int b){3 int az = __builtin_ctz(a),bz = __builtin_ctz(b);//末尾元素0的个数,对于LL类型,需要使用__builtin_ctzll4 int z = std::min(az,bz);5 b >>= bz;6 while(a) {7 a >>= az;8 int dif = b-a;9 az = __builtin_ctz(dif);10 if(a < b) b = a;11 if(dif < 0) a = -dif;12 else a = dif;13 }14 return b << z;15}[欧几里得算法][辗转相除法]求最大公约数:
xxxxxxxxxx21//a和b的最大公约数是a%b和b的最大公约数2int gcd(int a, int b) {return b ? gcd(b,a%b) : a;}
相邻的两个数gcd(n,n-1) = 1
扩展欧几里得
求解形如
裴蜀定理:对于任意整数a,b,一定存在一对整数x,y,满足:ax + by = gcd(a,b)
设d = gcd(a,b)
设x,y为当前层的一组解 ,x1,y1为下一层的一组解
当前层方程为
下一层方程为
联立当前层与下一层,则可得到当前层的解x,y,与下一层方程解x1,y1的关系
下层往上回溯时用下一层的解x1,y1更新当前层的解x,y
,
c%gcd(a,b) != 0 则无解)令
xxxxxxxxxx111int exgcd(int a,int b,int& x,int& y){//x,y引用传递2 if(!b){3 x = 1,y = 0;//当最终b = 0时,x = 1,y = 0 显然是方程的解a*1+0*0 = a4 return a;5 }6 int d = exgcd(b,a%b,x,y);7 int x1 = x,y1 = y;8 x = y1;9 y = x1 - a/b*y1;10 return d;11}
类似的,多元线性丢番图
最小公倍数
重要推论:gcd(a,b) * lcm(a,b) = a*b
xxxxxxxxxx71long long gcd(long long a, long long b) { return b ? gcd(b, a % b) : a; }2
3long long lcm(long long a, long long b) { return a / gcd(a, b) * b; }4//先算除法避免数据越界5
67//计算最小公倍数时,可以使用带参数的宏定义,比使用函数略微快一些(省去了函数的调用、返回和传参)
分解只因数
每个合数都可以写成几个质数相乘的形式,其中每个质数都是这个合数的因数
如:
试除法
时间复杂度
fac(12) = {(2,2),(3,1)}
xxxxxxxxxx221//https://www.acwing.com/problem/content/description/869/23using namespace std;4
5int main() {6 int t; cin >> t;7 while (t--){8 int n; cin >> n;9 for (int i = 2; i <= n / i; i++) {10 if (n % i == 0) {//循环里的i一定是n的素因子11 int s = 0;12 while (n%i == 0){13 n /= i;14 s++;15 }16 cout << i << ' ' << s << endl;17 }18 }19 if (n > 1) cout << n << ' ' << 1 << endl;20 cout << endl;21 }22}
约数分解
fac(12) = {1,2,3,4,6,12}
xxxxxxxxxx261//试除法求约数 https://www.acwing.com/problem/content/871/2vector<int>fac;3void dfs(vector<pair<int,int>>&v,int u,int now){4 if(u >= v.size()) return;5 for(int i = u;i < v.size();i++){6 int w = 1;7 for(int j = 0;j < v[i].second;j++){8 w *= v[i].first;9 fac.emplace_back(now*w);10 dfs(v,i+1,now*w);11 }12 }13}14
15void soviet(){16 int x; cin >> x;17 auto v = factorize(x);18 fac = {1};19 dfs(v,0,1);20 sort(fac.begin(),fac.end());21 cout << fac.size() << '\n';22 for(auto x:fac){23 cout << x << ' ';24 }25 cout << '\n';26}
筛法求质因数
primes[]存1~N的所有素数,0_idx
minp[x]为x的最小质因子
maxp[x]为x的最大质因子,诺maxp[x] == x则x为质数
xxxxxxxxxx371std::vector<int> primes, minp, maxp;2void sieve(int n = 1e6) {3 minp.resize(n + 1);4 maxp.resize(n + 1);5 for (int i = 2; i <= n; i++) {6 if (!minp[i]) {7 minp[i] = maxp[i] = i;8 primes.emplace_back(i);9 }10 for (auto &j : primes) {11 if (j > minp[i] || j > n / i) break;12 minp[i * j] = j;13 maxp[i * j] = maxp[i];14 }15 }16}17std::vector<std::pair<int,int>> factorize(int n) {//pair{质因数,次方}18 std::vector<std::pair<int,int>>ans;19 while (n > 1) {20 long long now = get_maxprime(x);21 ans.push_back({now,1});22 x /= now;23 while(x % now == 0) {24 ans.back().second++;25 x /= now;26 }27 }28 return ans;29}30
31int main(){32 sieve(1000000);33 int x; std::cin >> x;34 for(auto [p,k]:factorize(x)){35 std::cout << p << '^' << k << '\n';36 }37}
Pollard_Rho
泼辣的肉,期望时间复杂度为
xxxxxxxxxx831namespace Pollard_Rho{2 long long qmi(long long a,long long b,long long p){3 long long ans = 1;4 while(b){5 if(b&1) ans = __int128(ans) * a % p;6 b>>=1;7 a = __int128(a) * a % p;8 }9 return ans;10 }11
12 bool isprime(long long x) {//Miller-Rabin素数判断,时间复杂log~log^213 if (x < 2 || x % 6 % 4 != 1) return (x|1) == 3;14 long long s = __builtin_ctzll(x-1), d = x >> s;15 for (long long a : {2, 325, 9375, 28178, 450775, 9780504, 1795265022}) {16 long long p = qmi(a % x, d, x), i = s;17 while (p != 1 && p != x - 1 && a % x && i--) {18 p = __int128(p) * p % x;19 }20 if (p != x - 1 && i != s) return 0;21 }22 return 1;23 }24
25 long long gcd(long long a,long long b) {return b ? gcd(b,a%b) : a;}26 27 long long max_factor;28 long long Pollard_Rho(long long x) {29 long long s = 0, t = 0;30 long long c = (long long)rand() % (x - 1) + 1;31 long long val = 1;32 for (int goal = 1;; goal <<= 1, s = t, val = 1) {//倍增优化33 for (int step = 1; step <= goal; ++step) {34 t = ((__int128)t*t%x + c) % x;35 val = (__int128)val*std::abs(t-s)%x;36 if ((step % 127) == 0) {37 long long d = gcd(val, x);38 if (d > 1) return d;39 }40 }41 long long d = gcd(val, x);42 if (d > 1) return d;43 }44 }45
46 void fac(long long x) {47 if (x <= max_factor || x < 2) return;48 if (isprime(x)) {49 max_factor = std::max(max_factor, x);50 return;51 }52 long long p = x;53 while (p >= x) p = Pollard_Rho(x);54 while ((x % p) == 0) x /= p;55 fac(x), fac(p);56 }57 long long get_maxprime(long long x){//返回x的最大质因子58 max_factor = 0;59 fac(x);60 return max_factor;61 }62 std::vector<std::pair<long long,int>> factorize(long long x){//返回x的质因子vec{prime,k次方}63 std::vector<std::pair<long long,int>> ans;64 while(x > 1) {65 long long now = get_maxprime(x);66 if(ans.empty() || now != ans.back().first) ans.push_back({now,1});67 else ans.back().second++;68 x /= now;69 }70 std::reverse(ans.begin(),ans.end());71 return ans;72 }73};74using Pollard_Rho::isprime,Pollard_Rho::get_maxprime,Pollard_Rho::factorize;75
76int main(){//使用方法示例77 long long x; std::cin >> x;78 std::cout << isprime(x) << '\n';//素数判断79 std::cout << get_maxprime(x) << '\n';//最大质因子80 for(auto &[p,k]:factorize(x)){//分解质因数(从小到大排序)81 std::cout << p << '^' << k << '\n';82 }83}
约数个数
如果N =
约数个数 =
约数之和 =
xxxxxxxxxx281//https://www.acwing.com/problem/content/872/2//给定 n 个正整数 a[],请你输出这些数的乘积的约数个数,答案对 1e9+7 取模。345using namespace std;6const int mod = 1e9 + 7;7unordered_map<int, int>primes;8
9int main() {10 int t; cin >> t;11 while (t--){12 int n; cin >> n;13 for (int i = 2; i <= n / i;i++) {14 while (n % i == 0) {15 n /= i;16 primes[i]++;17 }18 }19 if (n > 1) primes[n]++;20 }21
22 long long ans = 1;23 for (auto& i : primes) {24 int a = i.second;25 ans = ans * (a + 1) % mod;26 }27 cout << ans << endl;28}
约数之和
如果N =
约数个数 =
约数之和 =
求a^b约数之和%mod,0 <= a,b <= 5e7
实现一个sum函数,sum(p, k)表示
方法一O(k):递推求p^0 + p^1 + ... + p^k
xxxxxxxxxx71ll sum0(ll p,ll k){2ll res = 1;3for(int i = 1;i <= k;i++){4res = (res*p+1)%mod;5}6return res;7}
方法二O(logK):递归求 k为偶数时sum(p,k) =>
=> => ) + ) => ) 当k为奇数时,为了更方便调用我们写的偶数项情况,可以单独拿出最后一项,把剩下的项转化为求偶数项的情况来考虑,再加上最后一项,就是奇数项的情况了,也即 xxxxxxxxxx51ll sum1(ll p,ll k){//sum1(p,k+1),调用时k要加12if(k == 1) return 1;//边界条件3if(!(k&1)) return (qmi(p,k/2)+1)*sum1(p,k/2)%mod;//k为偶数4return (qmi(p,k-1) + sum1(p,k-1))%mod;//k为奇数,k-1为偶数5}
方法三(OlogK):等比求和公式
------① ------② ②-①化简得sum = ,利用快速幂求逆元求解 xxxxxxxxxx41ll sum2(ll p,ll k){2if((p-1)%mod == 0) return k+1; //p-1与mod不互质,逆元不存在3return (qmi(p,k+1)-1)%mod*qmi(p-1,mod-2)%mod;4}
xxxxxxxxxx561//https://www.acwing.com/problem/content/99/234using namespace std;5using ll = long long;6const int mod = 9901;7
8ll qmi(ll a,ll b){9 ll ans = 1;10 while(b){11 if(b&1) ans = ans*a%mod;12 b >>= 1;13 a = a*a%mod;14 }15 return ans%mod;16}17
18ll sum0(ll p,ll k){//递推求p^0 + p^1 + ... + p^k19 ll res = 1;20 for(int i = 1;i <= k;i++){21 res = (res*p+1)%mod;22 }23 return res;24}25
26ll sum1(ll p,ll k){//递归求p^0 + p^1 + ... +p^(k-1)27 if(k == 1) return 1;//边界条件28 if(!(k&1)) return (qmi(p,k/2)+1)*sum1(p,k/2)%mod;//k为偶数29 return (qmi(p,k-1) + sum1(p,k-1))%mod;//k为奇数,k-1为偶数30}31
32ll sum2(ll p,ll k){//等比求和公式[p^(n+1)-a^0]/(p-1)33 if((p-1)%mod == 0) return k+1; //p-1与mod不互质,逆元不存在34 return (qmi(p,k+1)-1)%mod*qmi(p-1,mod-2)%mod;35}36
37int main(){38 ll a,b;cin >> a >> b;39 if(a == 0) return cout << 0,0;40 map<ll,ll>mp;41 for(int i = 2;i <= a/i;i++){42 while(a%i == 0){43 mp[i]+=b;44 a/=i;45 }46 }47 if(a > 1) mp[a]+=b;48
49 ll ans1 = 1;50 for(auto &[p,k]:mp){51 ans1 = ans1*sum1(p,k+1)%mod;//ans*=p^(0~k),sum为(0~k-1),所以k要加152 //ans2 = ans2*sum2(p,k)%mod;53 }54 cout << ans1;55 //cout << (ans2%mod+mod)%mod;56}
素数
试除法
时间复杂度
xxxxxxxxxx91bool isPrime(int n){2 if (n <= 1 || n == 4) return 0;3 if (n == 2 || n == 3) return 1;4 if (n % 6 != 1 && n % 6 != 5) return 0;5 for (int i = 5; i <= n/i; i += 6) {6 if (n % i == 0 || n % (i + 2) == 0) return 0;7 }8 return 1;9}
Miller-Rabin
快速判断一个数
xxxxxxxxxx221long long qmi(long long a,long long b,long long p){2 long long ans = 1;3 while(b){4 if(b&1) ans = __int128(ans) * a % p;5 b>>=1;6 a = __int128(a) * a % p;7 }8 return ans;9}10
11bool isprime(long long x) {12 if (x < 2 || x % 6 % 4 != 1) return (x|1) == 3;13 long long s = __builtin_ctzll(x-1), d = x >> s;14 for (long long a : {2, 325, 9375, 28178, 450775, 9780504, 1795265022}) {15 long long p = qmi(a % x, d, x), i = s;16 while (p != 1 && p != x - 1 && a % x && i--) {17 p = __int128(p) * p % x;18 }19 if (p != x - 1 && i != s) return 0;20 }21 return 1;22}
素数筛
xxxxxxxxxx231//线性筛 https://www.luogu.com.cn/problem/P391223using namespace std;4const int N = 1e8 + 5;5bool st[N];//i >= 2且st[i] == 0 则i是素数6int primes[N],cnt;//primes存质数7
8void initi(int n){9 //st[0] = st[1] = 1;10 for(int i = 2;i <= n;i++){11 if(!st[i]) primes[++cnt] = i;//如果没被筛过,则i是素数12 for(int j = 1;primes[j] <= n/i;j++){13 st[i*primes[j]] = 1;14 if(i%primes[j] == 0) break;15 }16 }17}18
19int main() {20 int n; cin >> n;21 initi(n);22 cout << cnt;23}
求[l,r]之间的所有质数
xxxxxxxxxx461//https://vjudge.net/problem/LightOJ-11972//用数组p存储√r以内的所有质数3//再用p筛选出l~r之间的所有合数,剩下的即为质数45
6const int N = 100005;7bool vis[N];8int primes[N],cnt;9void get_primes(int n){10 for(int i = 2;i <= n;i++){11 if(!vis[i]) primes[++cnt] = i;12 for(int j = 1;i*primes[j] <= n;j++){13 vis[i*primes[j]] = 1;14 if(i%primes[j] == 0) break;15 }16 }17}18
19bool st[N];20void sol(){21 long long l,r; std::cin >> l >> r;22 for(int i = 0;i < r-l+1;i++) st[i] = 0;23 24 for(int i = 1;(long long)primes[i]*primes[i] <= r;i++){25 long long p = primes[i];//p[i]的倍数即为合数26 for(long long j = std::max(p+p,p*((l+p-1)/p));j <= r;j += p){27 st[j-l] = 1;//j-l为数j相对于l的偏移位置28 }29 }30 int ans = 0;31 for(int i = 0;i < r-l+1;i++){32 if(!st[i] && l+i > 1) {//特判133 ans++;34 }35 }36 std::cout << ans << '\n';37}38
39int main(){40 get_primes(N-1);41 int t; std::cin >> t;42 for(int i = 1;i <= t;i++){43 printf("Case %d: ",i);44 sol();45 }46}
威尔逊定理
对于素数p > 1,
欧拉函数
ϕ(𝑁)表示1~N中与N互质的数的个数
诺将N分解质因数:N =
则欧拉函数计算公式ϕ(𝑁) =
xxxxxxxxxx261//求一个数的欧拉函数 √N2//根据计算公式,在分解质因数时顺便求欧拉函数3//https://www.acwing.com/problem/content/875/456using namespace std;7
8int phi(int n){9 int ans = n;10 for(int i = 2;i <= n/i;i++){11 if(n%i == 0){12 ans = ans/i*(i-1);13 while(n % i == 0){ n /= i; }14 }15 }16 if(n >= 2) ans = ans/n*(n-1);17 return ans;18}19
20int main(){21 int tt;cin >> tt;22 while(tt--){23 int x;cin >> x;24 cout << phi(x) <<endl;25 }26}
欧拉筛
xxxxxxxxxx331//筛选法求欧拉函数 O(N)2//在筛质数时顺便求欧拉函数3//https://www.acwing.com/problem/content/876/45using namespace std;6const int N = 1000006;7int n;8bool st[N];9int primes[N],cnt;10int phi[N];11
12void get_phi(int n){13 phi[1] = 1;14 for(int i = 2;i <= n;i++){15 if(!st[i]) {16 primes[++cnt] = i;17 phi[i] = i-1;//诺i为质数,则phi[i] = i-118 }19 for(int j = 1;primes[j] <= n/i;j++){20 st[i*primes[j]] = 1;21 if(i%primes[j] == 0) {22 phi[i*primes[j]] = phi[i]*primes[j];23 break;24 }25 else phi[i*primes[j]] = phi[i]*(primes[j]-1);26 }27 }28}29
30int main(){31 cin >> n;32 get_phi(n);33}
欧拉定理
诺正整数a与n互质,则
欧拉定理推论:
诺正整数a与n互质,对于任意正整数b,有
面对乘方算式对质数p取模,可以先把,可以先把底数对p取模、指数对 取模再计算乘方
诺正整数a与n互质,则满足
的最小正整数 是 的约数。
(n为正整数) xxxxxxxxxx31for(int i = 1;i <= n;i++){2if(n%i == 0){ ans += phi[i]; }3}//ans == n
求
在结论
中代入n=gcd(a,b),则有 其中,
称为 Iverson 括号,只有当命题 为真时 取值为 ,否则取 。对上式求和,就可以得到 这里关键的观察是
,即在 和 之间能够被 整除的 的个数是 。 利用这个式子,就可以遍历约数求和了。需要多组查询的时候,可以预处理欧拉函数的前缀和,利用数论分块查询。
仿照上文的推导,可以得出
xxxxxxxxxx411//https://www.luogu.com.cn/problem/P239823using namespace std;4using ll = long long;5const int N = 100005;6int primes[N],cnt;7int phi[N],sum[N];8bool st[N];9
10void get_phi(int n){11 phi[1] = 1;12 for(int i = 2;i <= n;i++){13 if(!st[i]){14 primes[++cnt] = i;15 phi[i] = i-1;16 }17 for(int j = 1;i*primes[j] <= n;j++){18 st[i*primes[j]] = 1;19 if(i%primes[j] == 0){20 phi[primes[j]*i] = phi[i]*primes[j];21 break;22 }23 else phi[primes[j]*i] = phi[i]*(primes[j]-1);24 }25 }26}27
28int main(){29 int n;cin >> n;30 get_phi(n);31 ll ans = 0;32 for(int i = 1;i <= n;i++){33 ans +=(long long)(n/i)*(n/i)*phi[i];34 }35 /*for(int l = 1,r;l <= n;l = r+1){//这部分计算,可以用整除分块进一步优化为O(sqrt(n))36 int t = n/l;37 r = n/t;38 ans += (long long)t*t * (sum[r] - sum[l-1]);//sum为phi的前缀和数组39 }*/40 cout << ans;41}
拓展欧拉定理 (欧拉降幂)
第一个要求a和m互质,第二个和第三个是广义欧拉降幂,不要求a和m互质,但要求b和phi(m)的大小关系。
P5091 【模板】扩展欧拉定理 - 洛谷 (luogu.com.cn)
求
xxxxxxxxxx4412using namespace std;3
4int phi(int n){5 int ans = n;6 for(int i = 2;i <= n/i;i++){7 if(n%i == 0){8 ans = ans/i*(i-1);9 while(n % i == 0){n /= i;}10 }11 }12 if(n >= 2) ans = ans/n*(n-1);13 return ans;14}15
16long long qmi(long long a,long long b,long long p){17 long long ans = 1;18 while(b){19 if(b&1) ans = ans*a%p;20 b >>= 1;21 a = a*a%p;22 }23 return ans%p;24}25
26int mo(string &b,int pm){//高精度取模,顺便与phi(m)比较大小27 int ans = 0;28 bool flag = 0;29 for(int i = 0;i < b.size();i++){30 int x = b[i] - '0';31 ans = ans*10+x;32 if(ans >= pm) flag = 1;33 ans %= pm;34 }35 if(flag) return ans+pm;//b >= phi(m)36 return ans;//b < phi(m)37}38
39int main(){40 int a,m;string b;cin >> a >> m >> b;41 int pm = phi(m);42 int ans = qmi(a,mo(b,pm),m);43 cout << ans;44}
P4139 上帝与集合的正确用法 - 洛谷 (luogu.com.cn)
给定 p,求
即
xxxxxxxxxx4812using namespace std;3const int N = 10000007;4int p;5int phi[N],primes[N],cnt;6bool st[N];7
8void get_phi(int n){9 phi[1] = 1;10 for(int i = 2;i <= n;i++){11 if(!st[i]) {12 primes[++cnt] = i;13 phi[i] = i-1;14 }15 for(int j = 1;primes[j] <= n/i;j++){16 st[i*primes[j]] = 1;17 if(i%primes[j] == 0){18 phi[i*primes[j]] = phi[i]*primes[j];19 break;20 }21 else phi[i*primes[j]] = phi[i]*(primes[j]-1);22 }23 }24}25
26long long qmi(long long a,long long b,long long p){27 long long ans = 1;28 while(b){29 if(b&1) ans = ans*a%p;30 b >>= 1;31 a = a*a%p;32 }33 return ans%p;34}35
36int sol(int p){37 if(p == 1) return 0;38 return qmi(2,sol(phi[p])+phi[p],p);39}40
41int main(){42 get_phi(N-1);43 int t;cin >> t;44 while(t--){45 cin >> p;46 cout << sol(p) << '\n';47 }48}[P10414 蓝桥杯 2023 国 A] 2023 次方 - 洛谷 (luogu.com.cn)
求
xxxxxxxxxx51int sol(int a,int p){2 if(p == 1) return 0;3 return qmi(a,sol(a+1,phi[p])+phi[p],p);4}5cout << sol(2,2023) << '\n';
模数
c++取模运算中 -7%4 = -3 而数学取模中 -7 % 4 = 4 题目往往要求数学取模
xxxxxxxxxx31int f(int x,int mod){//将c++取模转为数学取模2 return (x % mod + mod) % mod;3}
运算法则
模运算与基本四则运算有些相似,但是[除法例外][详见:乘法逆元]。其规则如下: (a + b) % p = (a % p + b % p) % p (a - b) % p = (a % p - b % p ) % p (a * b) % p = (a % p * b % p) % p (a * b * c)%p=(a%p * b%p * c%p) % p (a ^ b) % p = ((a % p) ^ b) % p
同余定理
同余定理:给定一个正整数m,如果两个整数a和b满足a-b能够被m整除,那么就称整数a与b对模m同余,
,记作 。对模m同余是整数的一个等价关系。
若数组a[ ]的所有元素对 m 取余相同,则任意两个元素的差都能被 m 整除。m 必须是区间内所有元素两两差值的最大公约数 GCD。即
线性同余
扩展欧几里得算法求线性同余方程
给定
,求出 使得 ,如果无解则输出impossible。
问题等价于
xxxxxxxxxx321//https://www.acwing.com/problem/content/880/23using namespace std;4
5int exgcd(int a,int b,int &x,int &y){6 if(!b){7 x = 1,y = 0;8 return a;9 }10 int d = exgcd(b,a%b,x,y);11 int x1 = x,y1 = y;12 x = y1;13 y = x1 - a/b*y1;14 return d;15}16
17int f(int a,int b,int m){18 int x,y,d = exgcd(a,m,x,y);19 if(b%d) return -1;//诺不能整除最大公约数则无解20 int t = std::abs(m/d);21 return ((long long)x*(b/d) % t + t ) % t;//返回最小正整数解22}23
24int main(){25 int q;cin >> q;26 while(q--){27 int a,b,m;cin >> a >> b >> m;28 int x = f(a,b,m);29 if(x == -1) cout << "impossible\n";30 else cout << x << '\n';31 }32}
高次同余
BSGS
求解
的最小非负整数解 (或返回无解),其中a,p互质, 。
BSGS(baby-step giant-step,大步小步算法)常用于求解离散对数问题,该算法可以在
xxxxxxxxxx181//模版题 https://www.luogu.com.cn/problem/P38462int bsgs(int a,int b,int p){//无解则返回-1,否则返回最小非负整数解3 map<int,int>hs;4 b %= p;5 int t = (int)sqrt(p) + 1;6 for(int j = 0;j < t;j++){7 int val = (long long) b * qmi(a,j,p) % p;8 hs[val] = j;9 }10 a = qmi(a,t,p);11 if(a == 0) return b == 0 ? 1 : -1;12 for(int i = 0;i <= t;i++){13 int val = qmi(a,i,p);14 int j = hs.find(val) == hs.end() ? -1 : hs[val];15 if(j >= 0 && i * t - j >= 0) return i * t - j;16 }17 return -1;18}
扩展BSGS
求解
的最小非负整数解 (或返回无解),其中a,p不一定互质, 或 。
xxxxxxxxxx321//模版题 https://www.luogu.com.cn/problem/P41952int exbsgs(int a,int b,int p){3 a%=p; b%=p;4 if(b == 1 || p == 1) return 0;5 int d,ax=1,cnt=0,x,y;6 while((d=exgcd(a,p,x,y))^1){7 if(b%d) return -1;8 b/=d; p/=d; cnt++;9 ax=1ll*ax*(a/d)%p;10 if(ax == b) return cnt;11 }12 exgcd(ax,p,x,y);13 int inv=(x%p+p)%p;14 b=1ll*b*inv%p;15
16 //下面为bsgs17 map<int,int>hs;18 b %= p;19 int t = (int)sqrt(p) + 1;20 for(int j = 0;j < t;j++){21 int val = (long long) b * qmi(a,j,p) % p;22 hs[val] = j;23 }24 a = qmi(a,t,p);25 if(a == 0) return b == 0 ? 1 : -1;26 for(int i = 0;i <= t;i++){27 int val = qmi(a,i,p);28 int j = hs.find(val) == hs.end() ? -1 : hs[val];29 if(j >= 0 && i * t - j >= 0) return i * t - j + cnt;//这里+cnt30 }31 return -1;32}
乘法逆元
费马小定理:对于任何一个整数a,以及素数p: 1.如果a是p的倍数,
2.如果a不是p的倍数,
将上述公式2变形得到:
xxxxxxxxxx271//快速幂求逆元2//给定n组a,p,其中p是质数,求a模p的乘法逆元,若逆元不存在则输出impossible34using namespace std;5using ll = long long;6
7ll qmi(ll a,ll b,ll p){8 ll ans = 1;9 while(b){10 if(b&1) ans = ans*a%p;11 b>>=1;12 a = a*a%p;13 }14 return ans%p;15}16
17ll gcd(ll a,ll b){return b?gcd(b,a%b):a;}18
19int main(){20 int t;cin >> t;21 while(t--){22 int a,p;cin >> a >> p;23 if(gcd(a,p) != 1) cout << "impossible" << endl;24 //a有逆元的充要条件是a与p互质25 else cout << qmi(a,p-2,p) << endl;26 }27}
中国剩余定理
扩展中国剩余定理(EXCRT):模数不互质
给定 a[1~n] 和 m[1~n],求一个最小的非负整数 𝑥,满足∀𝑖∈[1,𝑛],𝑥 ≡ 𝑚𝑖(𝑚𝑜𝑑 𝑎𝑖)。
输出最小非负整数解 𝑥,如果 𝑥 不存在,则输出 −1。
选第一个式子和第二个式子有:
, , //即求最小非负整数k1 ----------------① 令d = exgcd(a1,-a2),解为
如果m2-m1不能整除d则无解,返回-1 否则一对整数解为 对于①式又有性质
, k1最小非负整数解为 //不确定d的正负,取绝对值
令 , 则 //再用此式子与其它式子依次递推,x=m即为当前x最小正整数解
给定
时间复杂度
xxxxxxxxxx441//https://www.luogu.com.cn/problem/P47772//数据略强:q <= 1e5;a[i],b[i] <= 1e12;345using namespace std;6const int N = 100005;7
8struct node{9 long long a,b;10};11
12long long exgcd(long long a,long long b,long long& x,long long& y){13 if(!b){14 x = 1,y = 0;15 return a;16 }17 long long d = exgcd(b,a%b,x,y);18 long long x1 = x,y1 = y;19 x = y1,y = x1 - a/b*y1;20 return d;21}22
23long long excrt(const std::vector<node> &e){//0_idx24 long long ans = e[0].b,M = e[0].a,x = 0,y = 0;25 for(int i = 1;i < e.size();i++){26 long long B = ((e[i].b - ans) % e[i].a + e[i].a) % e[i].a;27 long long d = exgcd(M,e[i].a,x,y);28 if(B%d) {return -1;}//诺B不能整除最大公约数d,则无解。29 x = (__int128)x*(B/d) % e[i].a;//__int128或龟速乘防止爆long long30 ans += M*x;31 M *= e[i].a / d;32 ans = (ans + M) % M;//ans即为前i(0_idx)个柿子的解33 }34 return ans;35}36
37int main(){38 int n;cin >> n;39 std::vector<node>e(n);40 for(int i = 0;i < n;i++) {41 std::cin >> e[i].a >> e[i].b;//x%a == b42 }43 cout << excrt(e);44}
数论分块
快速求解形如
[P2261 CQOI2007] 余数求和 - 洛谷 (luogu.com.cn)
给定n,k,求
。
例如样例 n = 10,k = 5
| i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|
| 5 | 2 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 |
发现
xxxxxxxxxx1412using namespace std;3
4int main(){5 long long n,k;cin >> n >> k;6 long long ans = n * k;7 for(int l = 1,r;l <= n;l = r + 1){8 long long t = k/l;9 if(t == 0) r = n;10 else r = min(k/t,n);11 ans -= t * (l+r)*(r-l+1)/2;//区间[l,r]值均为t,快速计算12 }13 cout << ans;14}
其它
一些数学常数
圆周率: π = acos(-1) = 3.14159265358979323846264338327950288419716939937510
自然常数: e = 2.7182818284590452353602874713526624977572470936999595749
⑨的倍数
如果一个数能整除9,那么它的所有位数之和也能整除9
x个k连在一起组成的正整数
可以表示为
一堆正整数相乘后,末尾0的个数
cnt2统计所有乘数中质因数2的个数
cnt5统计所有乘数中质因数5的个数
则末尾0的个数 =min(cnt2,cnt5)
组合数学
排列组合
排列(nPr):
组合(nCr):
A[7,3] = 7 * 6 * 5 = 210 C[7,3] = (7 * 6 * 5)/(3 * 2 * 1) = 6
性质
组合数判定奇偶:对于C(n,m),诺 n&m==m 则为奇数,否则为偶数。
多重集
多重集是指包含重复元素的广义集合,设 S = {
S的全排列个数
从S中选 r (
二项式定理
一些常见的组合计数
n*m的网格从左上角(1,1)走到右下角(n,m),每次只能往下或右走,不同的路径的总数
需要的总步数为n+m-2,其中向下走n-1步,向右走m-1步,不同的路径总共有
或 种。
n个相同的物品分成m组,每组至少1个元素的方案数:
考虑拿
m-1个板子插到n个元素形成的n-1个空里面,将物品分成m组。答案就是。 本质是求 的正整数解的组数。其中
n个相同的物品分成m组,每组可以有0个元素的方案数:
显然此时没法直接插板了,因为可能出现很多块板子插到同一个空里面,不好计算。 先借m个元素过来,在这n+m个形成的
n+m-1个空里面插入m-1个板子,方案数为或 。 开头借了m个元素用于保证每组至少有一个元素,插完板后将借来的m个元素删除,因为元素是相同的,所以转化过的情况和转化前的情况可以一一对应,答案也就是相等的。 也相当于是求多重集{n个物品,m-1个板子}的全排列数。 本质是求
的非负整数解的组数。其中
n个物品分成m组,要求对于第
类比上一个问题,我们借
个元素过来,保证第 组至少能分到 个,也就是令 。 得到新方程: 其中
。然后就转化为了上一个问题,直接用插板法公式得到答案为 本质是求
的解的数目,其中
从1~n的自然数中选m个,且这m个数中任意两个数差值大于k的方案有
例题:Don't be too close(★6) - AtCoder typical90_o - (vjudge.net)
杨辉三角
O(
) 预处理 O(1)询问
xxxxxxxxxx101const int mod = 1e9+7,N = 2005;2long long C[N][N];3void init() {//初始化4 for (int i = 0; i <= 2000 ; i ++ ){5 for (int j = 0; j <= i; j ++ ){6 if (!j) C[i][j] = 1;7 else C[i][j] = (C[i - 1][j] + C[i - 1][j - 1]) % mod;8 }9 }10}
逆元求组合数
预处理 O(1)询问
xxxxxxxxxx4012using ll = long long;3using namespace std;4const int N = 100005,P = 1e9+7;5ll fact[N],infact[N];6
7ll qmi(ll a,ll b,ll p){8 ll ans = 1;9 while(b){10 if(b&1) ans = ans * a % p;11 b >>= 1;12 a = a * a % p;13 }14 return ans%p;15}16
17void initi(){18 fact[0] = infact[0] = 1;//0的阶乘等于119 for(int i = 1;i < N;i++){//预处理阶乘20 fact[i] = fact[i-1]*i%P;21 }22 infact[N-1] = qmi(fact[N-1],P-2,P);//预处理阶乘的逆元,倒着处理只用算一次快速幂求逆元23 for(int i = N - 2;i >= 1;i--){24 infact[i] = infact[i+1]*(i+1)%P;25 }26}27
28ll C(int a,int b){29 //if(a < b) return 0;30 return fact[a]*infact[b]%P*infact[a-b]%P;31}32
33int main(){34 initi();35 int t;cin >> t;36 while(t--){37 int a,b;cin >> a >> b;38 cout << C(a,b) << '\n';39 }40}
卢卡斯定理
,其中 为质数 O(
)询问
xxxxxxxxxx3712using namespace std;3using ll = long long;4
5ll qmi(ll a,ll b,ll p){6 ll ans = 1;7 while(b){8 if(b&1) ans = ans*a%p;9 b >>= 1;10 a = a*a%p;11 }12 return ans%p;13}14
15ll C(ll a,ll b,ll p){//O(blogP)朴素求组合数16 ll ans = 1;17 for(int i = 1,j = a;i <= b;i++,j--){18 ans = ans*j%p;19 ans = ans*qmi(i,p-2,p)%p;20 }21 return ans;22}23
24ll lucas(ll a,ll b,ll p){ 25 if(a < p && b < p) return C(a,b,p);26 return C(a%p,b%p,p)*lucas(a/p,b/p,p)%p;27}28
29int main(){30 int q; cin >> q;31 while(q--){32 ll a,b,p; cin >> a >> b >> p;33 cout << lucas(a,b,p) << endl;34 }35
36 return 0;37}
高精度排列组合
阶乘质因数分解+高精度乘法
博客:阶乘(n!)的素因数分解_正整数 (sohu.com)
算术基本定理:任意一个大于1的正整数n,它都可以分解为以下形式,其中p为质数,a为正整数
其中 P[]为n以内的所有素数
xxxxxxxxxx691//https://www.acwing.com/problem/content/890/234using namespace std;5const int N = 5003;6int primes[N],cnt;7bool st[N];8int sum[N];9
10void initi(int n){11 for(int i = 2;i <= n;i++){12 if(!st[i]) primes[++cnt] = i;13 for(int j = 1;primes[j] <= n/i;j++){14 st[primes[j]*i] = 1;15 if(i % primes[j] == 0) break;16 }17 }18}19
20int get(int n,int p){//计算n!里面分解为p^k的k值21 //12! = 1*2*3*4*5*6*7*8*9*10*11*1222 //p = 2 {2,4,6,8,10,12} ans+=623 //p^2 = 4 {4,8,12} ans+=324 //p^3 = 8 {8} ans+=125 //get(12,2) = 6+3+1 = 1026 int ans = 0;27 while(n){28 ans += n/p;29 n/=p;30 }31 return ans;32}33
34vector<int> mul(vector<int>&A,int b){35 vector<int>C;36 int t = 0;37 for(int i = 0;i < A.size();i++){38 t += A[i]*b;39 C.push_back(t%10);40 t /= 10;41 }42 while(t){43 C.push_back(t%10);44 t/=10;45 }46 return C;47}48
49int main(){50 int a,b;cin >> a >> b;51 initi(a);52
53 for(int i = 1;i <= cnt;i++){54 int p = primes[i];55 sum[i] = get(a,p) - get(b,p) - get(a-b,p); 56 }57
58 vector<int>A(1,1);59 for(int i = 1;i <= cnt;i++){60 for(int j = 1;j <= sum[i];j++){//A *= primes[i]^sum[i]61 A = mul(A,primes[i]);62 }63 }64
65 for(int i = A.size()-1;i >= 0;i--){66 cout << A[i];67 }68 return 0;69}
错位排列
没有任何元素出现在原有位置的排列。即,对于1~n的排列P,如果满足
递推关系式:
xxxxxxxxxx61//递推计算错位排列数列 O(N)2a[0] = 1;3for(int i = 1;i < N;i++){4 a[i] = i*a[i-1] + (i&1?-1:1);5}6for(int i = 1;i < N;i++) cout << a[i] << ' ';//0 1 2 9 44 265 ...
其它关系:
错位排列数有一个简单的取整表达式,增长速度与阶乘仅相差常数:
随着元素数量的增加,形成错位排列的概率 P 接近:
容斥原理
把上述问题推广到一般情况,就是我们熟知的容斥原理
把包含于某内容中的所有对象的数目先计算出来,然后再把计数时重复计算的数目排斥出去,使得计算的结果既无遗漏又无重复
即
xxxxxxxxxx311//https://www.acwing.com/problem/content/description/892/2//给定一个整数 n 和 m 个不同的质数 p1,p2,…,pm。3//请你求出 1∼n中能被 p1,p2,…,pm 中的至少一个数整除的整数有多少个。45using namespace std;6using ll = long long;7int p[20];8ll ans = 0;9
10int main(){11 int n,m;cin >> n >> m;12 for(int i = 1;i <= m;i++){13 cin >> p[i];14 }15
16 for(int i = 1;i < 1 << m;i++){//二进制1~11...11枚举所有状态17 ll cnt = 0,t = 1;18 for(int k = 0;k < m;k++){19 if(i >> k & 1){20 cnt++;21 t *= p[k+1];22 if(t > n){t = -1;break;}23 }24 }25 if(t == -1) continue;26 if(cnt&1) ans += n/t;27 else ans -= n/t;28 }29 cout << ans;30 return 0;31}
数列
等差/等比
等差数列
通项公式:
求和公式:
等比数列
通项公式:
求和公式:
诺无法求逆元可以考虑递归:
数列递推
求解递推数列
其中
代入求和得
斐波那契
斐波那契数列相关 - zhoukangyang - 博客园 (cnblogs.com)
f(0) = 0,f(1) = 1,f(n) = f(n-2)+f(n-1)
0 1 1 2 3 5 8 13 21 ....
快速倍增法
O(logN) 询问 比矩阵乘法常数更小,返回值为pair<Fib(n),Fib(n+1)>;
xxxxxxxxxx91pair<ll,ll> fib(ll n) {2 if (n == 0) return {0,1};3 auto [a,b] = fib(n >> 1);4 ll c = a * (2*b%mod - a + mod)%mod;5 ll d = (a*a%mod + b*b%mod)%mod;6 if (n&1) return {d,c+d};7 else return {c,d};8}9//cout << fib(n).first;
一些性质
卡西尼性质:
。附加性质:
。取上一条性质中
,我们得到 。由上一条性质可以归纳证明,
。上述性质可逆,即
。GCD 性质:
。
广义斐波那契数列
O(logN)询问
例:求
可以得到矩阵公式:
利用矩阵快速幂求
有关base转移矩阵的构造:
| F[n] | F[n-1] | |
|---|---|---|
| F[n-1] | p | 1 |
| F[n-2] | q | 0 |
xxxxxxxxxx461//https://www.luogu.com.cn/problem/P13492//给定p,q和前两项f(1),f(2) 求f(n)%mod34using namespace std;5int mod;6
7struct mat{8 long long a[2][2];9};10
11mat operator * (const mat &a,const mat &b){12 mat ans = {};13 for(int i = 0;i < 2;i++){14 for(int j = 0;j < 2;j++){15 for(int k = 0;k < 2;k++){16 ans.a[i][j] = (ans.a[i][j] + a.a[i][k] * b.a[k][j]) % mod;17 }18 }19 }20 return ans;21}22
23mat qmi(mat a,long long b){24 mat ans = {};25 for(int i = 0;i < 2;i++){26 ans.a[i][i] = 1;27 }28 while(b){29 if(b & 1) ans = ans * a;30 b >>= 1;31 a = a * a;32 }33 return ans;34}35
36int main(){37 int p,q,a1,a2,n; cin >> p >> q >> a1 >> a2 >> n >> mod;38 if(n == 1){cout << a1;return 0;}39 if(n == 2){cout << a2;return 0;}40 41 mat ans = {a2,a1};42 mat base = {p,1,q,0};43 ans = ans * qmi(base,n-2);44
45 cout << ans.a[0][0];46}
皮萨诺周期
模p意义下的斐波那契数列最下正周期被称为皮萨诺周期。
皮萨诺周期总是不超过6p,且只有在
当需要计算第
如果
xxxxxxxxxx111//https://codeforces.com/contest/2033/problem/F2//G(n,k)为第n个能被k整除的数的下标 n<=1e18,k<=1e53//暴力找出第一次能被k整除时的下标i,n*i即为第n个能被k整除的下标4void sol(){5 ll n,k;cin >> n >> k;6 ll l = 0,r = 1;7 for(ll i = 1;;i++){8 tie(l,r) = make_pair(r%k,(l+r)%k);9 if(!l) { cout << n%mod*i%mod << '\n'; return; }10 }11}
卡特兰数
组合数学中一个常出现在各种计数问题中出现的数列
卡特兰数(Catalan)公式、证明、代码、典例._卡特兰数公式
1,1,2,5,14,42,132,429...(从第0项开始)
通项公式:
递推公式:
例:n对括号有多少种匹配方式?(诺有k种括号,则答案为
xxxxxxxxxx391//组合数求卡特兰数2//O(N)预处理,O(logN)询问34using namespace std;5using ll = long long;6const int N = 200005,P = 1e9+7;7int n;8ll fact[N],infact[N];9
10ll qmi(ll a,ll b,ll p){11 ll ans = 1;12 while(b){13 if(b&1) ans = ans*a%p;14 b >>= 1;15 a = a*a%p;16 }17 return ans;18}19
20void init(){21 fact[0] = infact[0] = 1;22 for(int i = 1;i < N;i++){23 fact[i] = fact[i-1]*i%mod;24 }25 infact[N-1] = qmi(fact[N-1],mod-2,mod);26 for(int i = N-2;i >= 1;i--){27 infact[i] = infact[i+1]*(i+1)%mod;28 }29}30
31ll C(int a,int b,int p){32 return fact[a]*infact[b]%p*infact[a-b]%p;33}34
35int main(){36 cin >> n;37 initi();38 cout << C(2*n,n,P)*qmi(n+1,P-2,P)%P;39}xxxxxxxxxx261//递推求卡特兰数2//O(NlogN)预处理,O(1)询问34using namespace std;5using ll = long long;6const int N = 200005, P = 1e9+7;7ll h[N];8
9ll qmi(ll a,ll b,ll p){10 ll ans = 1;11 while(b){12 if(b&1) ans = ans*a%p;13 b >>= 1;14 a = a*a%p;15 }16 return ans;17}18
19int main(){20 int n;cin >> n;21 h[0] = 1;22 for(int i = 1;i <= n;i++){23 h[i] = (4*i - 2)*h[i-1]%P*qmi(i+1,P-2,P)%P;24 }25 cout << h[n]; 26}
xxxxxxxxxx71#python大法 高精度求卡特兰数2import math3n = int(input())4A = math.factorial(2 * n)5B = math.factorial(n)6ans = A/B/B/(n+1);7print(ans)
贝尔数 (集合划分)
xxxxxxxxxx131//O(N^2)预处理 O(1)询问2//贝尔三角形,每行的首项是贝尔数3const int N = 2003,mod = 1e9+7;4void get_bell(int n) {5 bell[0][0] = 1;6 for (int i = 1; i <= n; i++) {7 bell[i][0] = bell[i-1][i-1];8 for (int j = 1; j <= i; j++)9 bell[i][j] = (bell[i-1][j-1] + bell[i][j-1])%mod;10 }11}12
13for(int i = 0;i <= n;i++) cout << bell[i][0] << endl;
康托展开 (全排列排名)
康托展开:给定一个长度为N的全排列a[ ],求它是第几个排列。 逆康托展开:给点一个全排列的长度N,求第rk个排列是什么?
重要柿子:
S(i)表示1 ~ a[i]-1中未出现过的数的个数,它可以看作是一种特殊的进制,也叫做阶乘进制。
如:
时间复杂度
假设原排列为a[ ],阶乘进制为s[ ],排名为rk,下面直接给出转换代码(排名从0开始,数组下标均从1开始)
xxxxxxxxxx261vector<long long> a_to_s(int n,vector<long long>&a){2 vector<long long>s(n+1);3 vector<long long>t(n+1);4 auto add = [&](int i,int x){5 while(i <= n){6 t[i] += x;7 i += i&-i;8 }9 };10 auto query = [&](int i)->long long{11 long long ans = 0;12 while(i){13 ans += t[i];14 i -= i&-i;15 }16 return ans;17 };18 for(int i = 1;i <= n;i++){19 add(i,1);20 }21 for(int i = 1;i <= n;i++){22 s[i] = query(a[i]-1);23 add(a[i],-1);24 }25 return s;26}xxxxxxxxxx441vector<long long> s_to_a(int n,vector<long long>&s){2 vector<long long>a(n+1);3 struct st{4 int l,r,sum;5 };6 st* t = new st[(n<<2)+5];//权值线段树,求全局第k小7
8 auto pushup = [&](int p){9 t[p].sum = t[p<<1].sum + t[p<<1|1].sum;10 };11
12 auto build = [&](auto& build, int p, int l, int r) -> void {13 t[p].l = l;14 t[p].r = r;15 if (l == r) {16 t[p].sum = 1;17 return;18 }19 int mid = (l + r) >> 1;20 build(build, p << 1, l, mid); build(build, p << 1 | 1, mid + 1, r);21 pushup(p);22 };23
24 auto kth = [&](auto &kth,int p,int l,int r,int k)->int{25 if(t[p].l == t[p].r){26 t[p].sum = 0;//找到第k小时,顺便将他删除27 return t[p].l;28 }29 int mid = t[p].l + t[p].r >> 1;30 int res = 0;31 if(k <= t[p<<1].sum) res = kth(kth,p<<1,l,mid,k);32 else res = kth(kth,p<<1|1,mid+1,r,k-t[p<<1].sum);33 pushup(p);34 return res;35 };36
37 build(build,1,1,n);38
39 for(int i = 1;i <= n;i++){40 a[i] = kth(kth,1,1,n,s[i]+1);41 }42 //delete[] t;43 return a;44}xxxxxxxxxx201long long s_to_rk(int n,vector<long long>&s){2 vector<long long>fact(n+1);3 fact[0] = 1;4 for(int i = 1;i <= n;i++){5 fact[i] = fact[i-1]*i%mod;6 }7 long long rk = 0;8 for(int i = 1;i <= n;i++){9 rk = (rk + s[i]*fact[n-i])%mod;10 }11 return rk;12}13
14void s_add_rk(int n,vector<long long>&s,long long rk){15 s[n] += rk;16 for(int i = n;i >= 1;i--){17 s[i-1] += s[i] / (n-i+1);18 s[i] %= (n-i+1);19 }20}xxxxxxxxxx81vector<long long> rk_to_s(int n,long long rk){2 vector<long long>s(n+1);3 for(int i = 1;i <= n;i++){4 s[n-i+1] = rk % i;5 rk /= i;6 }7 return s;8};一些例题:a[ ]->s[ ]->rk:P5367 【模板】康托展开 - 洛谷 (luogu.com.cn) s[ ]->a[ ]:UVA11525 Permutation - 洛谷 (luogu.com.cn) a[ ]->s[ ],s[ ]+rk->a[ ] U72177 火星人plus - 洛谷 (luogu.com.cn)
线性代数
矩阵
矩阵乘法
矩阵相乘只有在第一个矩阵的列数和第二个矩阵的行数相同时才有意义。
设
其中矩阵
矩阵乘法满足结合律,不满足一般的交换律。利用结合律,矩阵乘法可以用快速幂的思想优化。
矩阵快速幂
时间复杂度
P3390 【模板】矩阵快速幂 - 洛谷 (luogu.com.cn)
给点一个n*n的矩阵A,求
xxxxxxxxxx5912using namespace std;3const int N = 105,mod = 1e9+7;4long long n,k;5
6struct mat{7 long long a[N][N];8};9
10mat operator * (const mat &a,const mat &b){11 mat ans = {};12 for(int i = 1;i <= n;i++){13 for(int j = 1;j <= n;j++){14 for(int k = 1;k <= n;k++){15 ans.a[i][j] = (ans.a[i][j] + a.a[i][k] * b.a[k][j]) % mod;16 }17 }18 }19 return ans;20}21
22mat qmi(mat a,long long b){23 mat ans = {};//单位矩阵24 for(int i = 1;i <= n;i++){25 ans.a[i][i] = 1;26 }27 while(b){28 if(b & 1) ans = ans * a;29 b >>= 1;30 a = a * a;31 }32 return ans;33}34
35void qmi(mat &ans,mat a, long long b){36 while(b){37 if(b & 1)ans = ans * a;38 b >>= 1;39 a = a * a;40 }41}42
43int main(){44 cin >> n >> k;45 mat base = {};46 for(int i = 1;i <= n;i++){47 for(int j = 1;j <= n;j++){48 cin >> base.a[i][j];49 }50 }51
52 mat ans = qmi(base,k);53
54 for(int i = 1;i <= n;i++){55 for(int j = 1;j <= n;j++){56 cout << ans.a[i][j] << ' ';57 }cout << '\n';58 }59}
P10502 Matrix Power Series - 洛谷 (luogu.com.cn)
给定一个
的矩阵 和一个正整数 ,求 。
xxxxxxxxxx82123
4int mod;5
6struct Mat{7 int n;8 std::vector<std::vector<int> >a;9
10 Mat(){}11 Mat(int _n) {12 n = _n;13 a = std::vector<std::vector<int> >(n,std::vector<int> (n));14 }15
16 Mat operator * (const Mat &m2){17 Mat ans(n);18 for(int i = 0;i < n;i++){19 for(int j = 0;j < n;j++){20 for(int k = 0;k < n;k++){21 ans.a[i][j] = (ans.a[i][j] + a[i][k] * m2.a[k][j]) % mod;22 }23 }24 }25 return ans;26 }27
28 Mat operator + (const Mat &m2){29 Mat ans = *this;30 for(int i = 0;i < n;i++){31 for(int j = 0;j < n;j++){32 ans.a[i][j] = (ans.a[i][j] + m2.a[i][j]) % mod;33 }34 }35 return ans;36 }37
38 Mat qmi(long long b){39 Mat base = *this;40 Mat ans(n);41 for(int i = 0;i < n;i++) ans.a[i][i] = 1;42 while(b){43 if(b & 1) ans = ans * base;44 b >>= 1;45 base = base * base;46 }47 return ans;48 }49};50
51Mat A;52
53Mat f(int k){54 if(k == 1) return A;55 if(k&1) {56 return f(k-1) + A.qmi(k);57 }58 else{59 Mat B = f(k/2);60 return B + B * A.qmi(k/2);61 }62}63
64int main(){65 int n,k; std::cin >> n >> k >> mod;66 A = Mat(n);67 for(int i = 0;i < n;i++){68 for(int j = 0;j < n;j++){69 std::cin >> A.a[i][j];70 A.a[i][j] %= mod;71 }72 }73
74 A = f(k);75
76 for(int i = 0;i < n;i++){77 for(int j = 0;j < n;j++){78 std::cout << A.a[i][j] << ' ';79 }80 std::cout << '\n';81 }82}
矩阵封装
默认0_idx、N*N的矩阵大小,根据实际需要修改代码。
| Mat | ||
|---|---|---|
A +* B | 矩阵加/乘法 | |
auto B = A.qmi(k) | 矩阵快速幂 | |
A.norm() | 化为单位矩阵 | |
auto B = inv(A) | 求逆矩阵 | 诺不存在则B[0][0] == -1 |
det(A) | 行列式求值 | |
add(x,y,w) | 矩阵树连边 | 注意0_idx |
xxxxxxxxxx1341const int mod = 1e9+7;//2namespace MAT{3
4 template<typename T>5 struct Mat{//0_idx6 int n;7 std::vector<std::vector<T>>a;8
9 Mat(int _n,T val = 0) {10 n = _n;11 a = std::vector<std::vector<T>>(n,std::vector<T>(n,val));12 }13
14 Mat operator * (const Mat &m2){15 Mat ans(n,0);16 for(int i = 0;i < n;i++){17 for(int j = 0;j < n;j++){18 for(int k = 0;k < n;k++){19 ans.a[i][j] = (ans.a[i][j] + a[i][k] * m2.a[k][j]) % mod;20 }21 }22 }23 return ans;24 }25
26 Mat operator + (const Mat &m2){27 auto ans = *this;28 for(int i = 0;i < n;i++){29 for(int j = 0;j < n;j++){30 ans.a[i][j] = (ans.a[i][j] + m2.a[i][j]) % mod;31 }32 }33 return ans;34 }35 void operator *= (const Mat &m2) {*this = *this * m2;}36 void operator += (const Mat &m2) {*this = *this + m2;}37
38 void norm(){39 for(int i = 0;i < n;i++) a[i][i] = 1;40 }41
42 void add(int x,int y,int w){//Mat_tree:add_edge(x,y,w);43 if(x == y) return;44 a[y][y] = (a[y][y] + w) % mod;45 a[x][y] = (a[x][y] - w) % mod;46 }47
48 Mat qmi(long long b){49 auto base = *this;50 Mat ans(n);51 ans.norm();52 while(b) {53 if(b&1) ans = ans * base;54 b >>= 1;55 base = base * base;56 }57 return ans;58 }59 };60
61 long long qmi(long long a,long long b,long long p){62 long long ans = 1;63 while(b){64 if(b&1) ans = ans*a%p;65 b>>=1;66 a = a*a%p;67 }68 return ans;69 }70
71 template<typename T>72 Mat<T> inv(Mat<T> mt){73 int n = mt.n;74 std::vector<std::vector<long long>> aug(n, std::vector<long long>(2 * n, 0));75 for (int i = 0; i < n; i++) {76 for (int j = 0; j < n; j++) { aug[i][j] = mt.a[i][j]; }77 aug[i][i + n] = 1;78 }79
80 for (int i = 0; i < n; i++) {81 int pivot = -1;82 for (int r = i; r < n; r++) {83 if (aug[r][i] != 0) {84 pivot = r;85 break;86 }87 }88 if (pivot == -1) { mt.a[0][0] = -1;return mt; }//No_inv89 if (i != pivot) { std::swap(aug[i], aug[pivot]); }90 long long inv_val = qmi(aug[i][i], mod - 2, mod);91 for (int j = 0; j < 2 * n; j++) {92 aug[i][j] = aug[i][j] * inv_val % mod;93 }94 for (int j = 0; j < n; j++) {95 if (j == i) continue;96 long long mul = aug[j][i];97 for (int k = 0; k < 2 * n; k++) {98 aug[j][k] = (aug[j][k] - mul * aug[i][k] % mod + mod) % mod;99 }100 }101 }102
103 for (int i = 0; i < n; i++) {104 for (int j = 0; j < n; j++) {105 mt.a[i][j] = aug[i][j + n];106 }107 }108 return mt;109 }110
111 template<typename T>112 T det(Mat<T> mt){113 int n = mt.n;114 long long ans = 1;115 for(int i = 0;i < n;i++){//if(mat_tree) please i begin with 1116 for(int j = i+1;j < n;j++){117 while(mt.a[j][i]){118 long long t = mt.a[i][i]/mt.a[j][i];119 for(int k = i;k < n;k++) {120 mt.a[i][k] = (mt.a[i][k] - mt.a[j][k] * t % mod + mod) % mod;121 //mt.a[i][k] -= t * mt.a[j][k];122 }123 std::swap(mt.a[i],mt.a[j]);124 ans = -ans;125 }126 }127 if(!mt.a[i][i]) return 0;128 ans = (ans * mt.a[i][i] % mod + mod) % mod;129 //ans *= mt.a[i][i];130 }131 return std::abs(ans);132 }133}134using MAT::Mat;
行列式
定义
对于一个矩阵
一些性质
单位矩阵的行列式为1。
交换两行(列),行列式的值变号。
诺某一行(列)乘以
,行列式的值也就乘以 。若行列式某一行(列)的元素是两数之和,则行列式可拆成两个行列式的和。
行列式某一行(列)元素加上另一行(列)对应元素的k倍,行列式的值不变。
诺有两行(列)一样,则行列式的值为0
行列式求值
高斯消元实现,时间复杂度
xxxxxxxxxx431//模版 https://www.luogu.com.cn/problem/P711223using namespace std;4
5int mod;6
7struct Mat{//1_idx8 std::vector<std::vector<long long>>a;9
10 Mat(int n){11 a = std::vector<std::vector<long long>>(n+1,std::vector<long long>(n+1));12 }13 14 long long det(int n){15 long long ans = 1;16 for(int i = 1;i <= n;i++){17 for(int j = i+1;j <= n;j++){18 while(a[j][i]){19 long long t = a[i][i]/a[j][i];20 for(int k = i;k <= n;k++) {21 a[i][k] = (a[i][k] - a[j][k] * t % mod + mod) % mod;22 }23 std::swap(a[i],a[j]);24 ans = -ans;25 }26 }27 if(!a[i][i]) return 0;28 ans = (ans * a[i][i] % mod + mod) % mod;29 }30 return std::abs(ans);31 }32};33
34int main(){35 int n; cin >> n >> mod;36 Mat mat(n);37 for(int i = 1;i <= n;i++){38 for(int j = 1;j <= n;j++){39 cin >> mat.a[i][j];40 }41 }42 cout << mat.det(n);43}
线性基
常用来解决子集异或一类题目
从a[ ]中选任意个整数(可能包含重复元素),使得选出的整数异或和最大,求这个异或和最大值可能是多少
xxxxxxxxxx311//https://www.luogu.com.cn/problem/P38122//时间复杂度O(NlogV)345using namespace std;6using ll = long long;7const int N = 100005;8ll n,a[N],p[N],cnt;9
10void add(ll x){11 for(int i = 1;i <= cnt;i++){12 x = min(x,x^p[i]);13 }14 if(x){15 p[++cnt] = x;16 sort(p+1,p+cnt+1,greater<ll>());17 }18}19
20int main(){21 cin >> n;22 for(int i = 1;i <= n;i++){23 cin >> a[i];24 add(a[i]);25 }26 ll x = 0;;27 for(int i = 1;i <= cnt;i++){28 x = max(x,x^p[i]);29 }30 cout << x;31}
应用:
快速查询一个数是否可以被一堆数异或出来
快速查询一堆数可以异或出来的最大/最小值
快速查询一堆数可以异或出来的第k大值
概率论
一般情况下,解决概率问题需要顺序循环,而解决期望问题使用逆序循环
期望
若随机变量
对任意实数
,有 。 。
通常把终止状态作为初值,把起始状态作为目标,倒着进行计算。因为在很多情况下,起始状态是唯一的,而终止状态很多。根据数学期望的定义,诺我们正着计算,则还需求出从起始状态到每个终止状态的概率,与F值相乘求和才能得到答案,增加了难度,也很容易出错
xxxxxxxxxx441//绿豆蛙的归宿https://www.luogu.com.cn/problem/P43162//F[N]=0,我们的目标是求出F[1],故我们从终点出发,在反图执行拓扑排序,在拓扑排序的过程中顺便计算F[x]即可34using namespace std;5const int N = 200005;6int n,m;7int h[N],e[N],ne[N],w[N],idx;8int in[N],out[N];9double ans[N];10
11void add(int a,int b,int c){12 w[idx] = c,e[idx] = b,ne[idx] = h[a],h[a] = idx++;13}14
15void uuz(int beg){16 ans[beg] = 0;17 queue<int>q;18 q.push(beg);19 while(q.size()){20 auto t = q.front();q.pop();21 for(int i = h[t];~i;i = ne[i]){22 int k = e[i];23 ans[k] += (ans[t] + w[i])/in[k];24 out[k]--;25 if(out[k] == 0) q.push(k);26 }27 }28}29
30void sol(){31 memset(h,-1,sizeof h);32 cin >> n >> m;33 for(int i = 1;i <= m;i++){34 int a,b,c;cin >> a >> b >> c;35 add(b,a,c);//反向建边36 in[a]++;out[a]++;37 }38 uuz(n);39 cout << fixed << setprecision(2) << ans[1];40}41
42int main() {43 while(T--){ sol(); }44}
方差
设随机变量
方差的性质
若随机变量
对任意常数
都有
数值算法
数值积分
自适应辛普森算法
求定积分
xxxxxxxxxx201double f(double x){//f(x)2 3}4
5double simpson(double l, double r) {6 double mid = (l + r) / 2;7 return (r - l) * (f(l) + 4 * f(mid) + f(r)) / 6; // 辛普森公式8}9
10double asr(double l, double r, double eps, double ans, int step) {11 double mid = (l + r) / 2;12 double fl = simpson(l, mid), fr = simpson(mid, r);13 if (abs(fl + fr - ans) <= 15 * eps && step < 0)14 return fl + fr + (fl + fr - ans) / 15; // 足够相似的话就直接返回15 return asr(l, mid, eps / 2, fl, step - 1) + asr(mid, r, eps / 2, fr, step - 1); // 否则分割成两段递归求解16}17
18double calc(double l, double r, double eps) {// calc(l,r,eps)19 return asr(l, r, eps, simpson(l, r), 12);20}
P4525 【模板】自适应辛普森法 1 - 洛谷 (luogu.com.cn)
试计算积分
结果保留至小数点后6位。
xxxxxxxxxx2912using namespace std;3double a,b,c,d,l,r;4
5double f(double x){6 return (c*x+d)/(a*x+b);7}8
9double simpson(double l, double r) {10 double mid = (l + r) / 2;11 return (r - l) * (f(l) + 4 * f(mid) + f(r)) / 6;12}13
14double asr(double l, double r, double eps, double ans, int step) {15 double mid = (l + r) / 2;16 double fl = simpson(l, mid), fr = simpson(mid, r);17 if (abs(fl + fr - ans) <= 15 * eps && step < 0)18 return fl + fr + (fl + fr - ans) / 15;19 return asr(l, mid, eps / 2, fl, step - 1) + asr(mid, r, eps / 2, fr, step - 1);20}21
22double calc(double l, double r, double eps) {23 return asr(l, r, eps, simpson(l, r), 12);24}25
26int main() {27 cin >> a >> b >> c >> d >> l >> r;28 cout << fixed << setprecision(6) << calc(l,r,1e-9);29}
高斯消元
高斯消元解线性方程组
运用初等行变换,把增广矩阵,变为阶梯型矩阵。最后再把阶梯型矩阵从下到上回代到第一层即可得到方程的解。
时间复杂度
枚举每一列c
找到当前列绝对值最大的一行,放到上面 将该行该列第一个数变成1 再将下面其他所有行该列变成0
xxxxxxxxxx641//https://www.acwing.com/problem/content/885/23using namespace std;4
5const double eps = 1e-9;6int gauss(vector<vector<double>>&a) {//a[m+1][n+2]; 1_idx7 int m = a.size()-1,n = a[0].size()-2;8 int c,r;//col列 row行9
10 for (c = 1,r = 1; c <= n; c++) {11 int tr = r;//找到当前这一列,绝对值最大的所在行12 for (int i = r+1; i <= m; i++) {13 if (fabs(a[i][c]) > fabs(a[tr][c])) { tr = i; }14 }15 if (fabs(a[tr][c]) < eps) continue; //诺都是0直接跳过;16
17 swap(a[r],a[tr]);//把当前这一行换到第最上面(第r行)18 for(int j = n+1;j >= c;j--) {a[r][j] /= a[r][c];}//把当前该行该列第一个数变成1(从后往前系数倒着算)19
20 for (int i = r+1; i <= m; i++) {//把当前列下面所有系数变为021 if(fabs(a[i][c]) > eps){//已经是0则可以跳过22 for(int j = n+1;j >= c;j--){//每一行从后往前,系数-=行首系数*第c行同一列系数23 a[i][j] -= a[i][c]*a[r][j];24 }25 }26 }27 r++;28 }29
30 if (r < n+1) {//r<n+1说明已经解出来的x不足以解出原方程31 for (int i = r; i <= m; i++) {32 if (fabs(a[i][n+1]) > eps) {//左边=0,右边b!=0,无解33 return 0;34 }35 }36 return 2;//0=0说明有无穷解37 }38
39 for(int i = n;i >= 1;i--){//从下往上回代,解出原方程40 for(int j = i+1;j <= n;j++){41 a[i][n+1] -= a[j][n+1]*a[i][j];42 }43 }44 return 1;45}46
47int main() {48 int n, m; cin >> n;m = n;49
50 vector<vector<double>> a(m+1, vector<double>(n+2));51 for (int i = 1; i <= m; i++) {52 for (int j = 1; j <= n+1; j++) {53 cin >> a[i][j];54 }55 }56 int t = gauss(a);57 if (t == 0) cout << "No solution";58 if (t == 2) cout << "Infinite group solutions";59 if (t == 1) {60 for (int i = 1; i <= n; i++) {61 printf("%.2lf\n", a[i][n+1]);62 }63 }64}
高斯消元解异或方程组
在消元的时候使用「异或消元」而非「加减消元」,且不需要进行乘除改变系数(因为系数均为0和 1)
异或可以看做不进位的加法
xxxxxxxxxx601//https://www.acwing.com/problem/content/description/886/234using namespace std;5
6int gauss_xor(vector<vector<int>>&a){//a[m+1][n+2]; 1_idx7 int m = a.size()-1,n = a[0].size()-2;8 int c,r;9 for(c = 1,r = 1;c <= n;c++){10 int tr = r;11 for(int i = r;i <= m;i++){12 if(a[i][c]) {tr = i;break;}13 }14 if(a[tr][c] == 0) continue;15
16 swap(a[r],a[tr]);17
18 for(int i = r+1;i <= m;i++){19 if(a[i][c]){20 for(int j = n+1;j >= c;j--){21 a[i][j] ^= a[r][j];22 }23 }24 }25 r++;26 }27
28 if(r < n+1){29 for(int i = r;i <= m;i++){30 if(a[i][n+1]) return 0;31 }32 return 2;33 }34
35 for(int i = n;i >= 1;i--){36 for(int j = i+1;j <= n;j++){37 if(a[i][j]) a[i][n+1] ^= a[j][n+1];38 }39 }40 return 1;41}42
43int main(){44 int n,m;cin >> n;m = n;45 vector<vector<int>>a(m+1,vector<int>(n+2));46 for(int i = 1;i <= m;i++){47 for(int j = 1;j <= n+1;j++){48 cin >> a[i][j];49 }50 }51
52 int t = gauss_xor(a);53 if(t == 1){54 for(int i = 1;i <= n;i++){55 cout << a[i][n+1] << '\n';56 }57 }58 if(t == 2) cout << "Multiple sets of solutions";59 if(t == 0) cout << "No solution";60}
插值
插值是一种通过已知的、离散的数据点推算一定范围内的新数据点的方法。插值法常用于函数拟合中。
拉格朗日插值
P4781 【模板】拉格朗日插值 - 洛谷 (luogu.com.cn)
对于
现在,给定这样
xxxxxxxxxx411//O(N^2)实现234using namespace std;5const int mod = 998244353;6
7long long qmi(long long a,long long b,long long p){8 long long ans = 1;9 while(b){10 if(b&1) ans = ans*a%p;11 b>>=1;12 a = a*a%p;13 }14 return ans%p;15}16
17long long lagrange_interpolation(vector<pair<long long,long long>> &v,int k){//1_idx18 long long ans = 0;19 for(int i = 1;i < v.size();i++){20 auto &[x1,y1] = v[i];21 long long s1 = y1,s2 = 1;22 for(int j = 1;j < v.size();j++){23 if(i == j) continue;24 auto &[x2,y2] = v[j];25 s1 = s1 * (k-x2)%mod;26 s2 = s2 * (x1-x2)%mod;27 }28 ans += s1 * qmi(s2,mod-2,mod) % mod;29 ans = (ans + mod) % mod;30 }31 return ans;32}33
34int main(){35 int n,k; cin >> n >> k;36 vector<pair<long long,long long>>v(n+1);37 for(int i = 1;i <= n;i++){38 cin >> v[i].first >> v[i].second;39 }40 cout << lagrange_interpolation(v,k) << '\n';41}
博弈论
Nim游戏
n堆物品,每堆有
博弈图和状态
如果将每个状态视为一个节点,再从每个状态向它的后继状态连边,我们就可以得到一个博弈状态图。
例如,如果节点
定义 必胜状态 为 先手必胜的状态,必败状态 为 先手必败的状态。 通过推理,我们可以得出下面三条定理:
定理 1:没有后继状态的状态是必败状态。
定理 2:一个状态是必胜状态当且仅当存在至少一个必败状态为它的后继状态。
定理 3:一个状态是必败状态当且仅当它的所有后继状态均为必胜状态。
对于定理 1,如果游戏进行不下去了,那么这个玩家就输掉了游戏。
对于定理 2,如果该状态至少有一个后继状态为必败状态,那么玩家可以通过操作到该必败状态;此时对手的状态为必败状态——对手必定是失败的,而相反地,自己就获得了胜利。
对于定理 3,如果不存在一个后继状态为必败状态,那么无论如何,玩家只能操作到必胜状态;此时对手的状态为必胜状态——对手必定是胜利的,自己就输掉了游戏。
如果博弈图是一个有向无环图,则通过这三个定理,我们可以在绘出博弈图的情况下用O(N+M)的时间(其中N为状态种数,M为边数)得出每个状态是必胜状态还是必败状态。
Nim和
通过绘画博弈图,我们可以在
xxxxxxxxxx141//https://www.acwing.com/problem/content/893/23using namespace std;4
5int main(){6 int ans = 0;7 int n;cin >> n;8 for(int i = 1;i <= n;i++){9 int x;cin >> x;10 ans^=x;11 }12 if(ans) cout << "Yes";13 else cout << "No";14}
SG函数
有向图游戏是一个经典的博弈游戏——实际上,大部分的公平组合游戏都可以转换为有向图游戏。
在一个有向无环图中,只有一个起点,上面有一个棋子,两个玩家轮流沿着有向边推动棋子,不能走的玩家判负。
定义
例如
对于状态
而对于由
这一定理被称作 Sprague–Grundy 定理(Sprague–Grundy Theorem), 简称 SG 定理,适用于任何公平的两人游戏, 它常被用于决定游戏的输赢结果。
计算给定状态的 Grundy 值的步骤一般包括:
获取从此状态所有可能的转换;
每个转换都可以导致 一系列独立的博弈(退化情况下只有一个)。计算每个独立博弈的 Grundy 值并对它们进行 异或求和。
在为每个转换计算了 Grundy 值之后,状态的值是这些数字的
。如果该值为零,则当前状态为输,否则为赢。
将 Nim 游戏转换为有向图游戏
我们可以将一个有
给定n堆石子和一个由m个不同正整数构成的数字集合S。两位玩家轮流操作,每次可以从任意一堆石子中拿取石子,每次拿取的石子数量必须包含于集合S,判断先手是否必胜?
xxxxxxxxxx3612345using namespace std;6const int N = 105,M = 10004;7int n,m;8int f[M],s[N];//f储存所有情况的sg值,s存可供选择的集合9
10int sg(int x){11 if(f[x] != -1) return f[x];//如果当前数的sg已经确定,直接返回即可12 set<int>st;13 for(int i = 0;i < m;i++){14 if(x >= s[i]) st.insert(sg(x-s[i]));//set存当前能到达状态的所有sg值15 }16 for(int i = 0;;i++){17 if(st.find(i) == st.end()) {18 return f[x] = i;//f[x] = 不属于集合set的最小非负整数19 }20 }21}22
23int main(){24 memset(f,-1,sizeof f);25 cin >> m;26 for(int i = 0;i < m;i++)cin >> s[i];27 28 cin >> n;29 int ans = 0;30 for(int i = 0;i < n;i++){31 int x;cin >> x;32 ans^=sg(x);33 }34 if(ans) cout << "Yes";35 else cout << "No";36}
VS AtCoder(★6) - AtCoder typical90_ae - Virtual Judge (vjudge.net)
给定n堆石子,每堆石子由w[i]颗白石和b[i]颗蓝石。两人轮流进行以下操作之一:
选择一堆白石w >= 1的石子,向选择的石子中加入w颗蓝石,然后移除1颗白石。
选择一堆蓝石b >= 2的石子,移除k颗蓝石子,其中
。 最后无法操作的输掉比赛。
xxxxxxxxxx371234using namespace std;5const int N = 100005;6int w[N],b[N];7int f[55][1505];8
9int sg(int x,int y){10 if(f[x][y] != -1) return f[x][y];11 set<int>se;12 if(x >= 1) se.insert(sg(x-1,y+x));13 if(y >= 2) {14 for(int i = 1;i*2 <= y;i++){15 se.insert(sg(x,y-i));16 }17 }18 for(int i = 0;;i++){19 if(se.find(i) == se.end()){20 return f[x][y] = i;21 }22 }23}24
25int main(){26 memset(f,-1,sizeof f);27 int n;cin >> n;28 for(int i = 1;i <= n;i++) cin >> w[i];29 for(int i = 1;i <= n;i++) cin >> b[i];30
31 int ans = 0;32 for(int i = 1;i <= n;i++){33 ans ^= sg(w[i],b[i]);34 }35 if(ans) cout << "First";36 else cout << "Second";37}
计算几何
距离
| 距离 | 二维计算 | 多维计算 |
|---|---|---|
| 欧氏距离 | 三维: | |
| 曼哈顿距离 | n维: | |
| 切比雪夫距离 | n维: |
曼哈顿距离与切比雪夫距离转换
曼哈顿->切比雪夫 :
切比雪夫->曼哈顿 :
曼哈顿坐标系是通过切比雪夫坐标系旋转
[P5098 USACO04OPEN] Cave Cows 3 - 洛谷 (luogu.com.cn)
求平面内任意一点到其它点的最大距离
直接曼哈顿转切比雪夫坐标,答案就是max(当前的点的 x 和 x的极值做差的绝对值 , 当前的点的 y 和 y 的极值做差的绝对值的)
xxxxxxxxxx3312345using namespace std;6const int N = 100005;7int n;8vector<long long>dx,dy;9
10struct node{11 int x,y;12}p[N];13
14int main(){15 cin >> n;16 for(int i = 1;i <= n;i++){17 auto &[x,y] = p[i];cin >> x >> y;18 tie(x,y) = pair{x+y,x-y};19 dx.emplace_back(x);//也可以用4个变量分别维护x和y的极大值和极小值,O(N)20 dy.emplace_back(y);21 }22 sort(dx.begin(),dx.end());23 sort(dy.begin(),dy.end());24 long long ans = 0;25 for(int i = 1;i <= n;i++){26 auto [x,y] = p[i];27 long long nx = max(abs(dx.back()-x),abs(dx[0]-x));28 long long ny = max(abs(dy.back()-y),abs(dy[0]-y));29 long long now = max(nx,ny);30 ans = max(ans,now);31 }32 cout << ans;33}
扫描线
扫描线的思路就是数据结构维护一维,暴力模拟另一维。
面积并
P5490 【模板】扫描线 & 矩形面积并 - 洛谷 (luogu.com.cn)
给每一个矩形的上下边进行标记,下面的边标记为 1,上面的边标记为 -1。每遇到一个水平边时,让这条边(在横轴投影区间)的权值加上这条边的标记。当前的宽度就是整个数轴上权值大于 0 的区间总长度,总面积即为
xxxxxxxxxx61线段树维护的是区间段(左闭右开)而不是离散点2点: 1 3 5 73区间: [1,3) [3,5) [5,7)4索引: 1 2 35modify当处理矩形边[1,5]时,覆盖的区间段索引 = [hs[1],hs[5]-1] = [1,2]6pushup计算节点p对应区间段时,节点p的左右端点[1,2],对应区间[v[1],v[2+1]) = [1,5)
xxxxxxxxxx831234
5const int N = 200005;6int n;7std::vector<int>hs(1,-2e9);8
9struct line{//从下往上扫描10 int l,r,h;//记录每条线其左右区间[l,r]和所在高度h11 int w;//w=1为下边,w=-1为上边12 bool operator < (const line &e2)const{13 if(h == e2.h) return w > e2.w;14 return h < e2.h;15 }16};17
18struct ST{19 int l,r;20 int cnt,len;//cnt:被覆盖次数,len:覆盖长度21}t[N<<2];22
23void build(int p,int l,int r){24 t[p] = {l,r};25 if(l == r) {26 return;27 }28 int mid = l + r >> 1;29 build(p<<1,l,mid);build(p<<1|1,mid+1,r);30}31
32void pushup(int p){33 if(t[p].cnt){//完全覆盖,取整个区间长度34 t[p].len = hs[t[p].r+1] - hs[t[p].l]; 35 }36 else{37 if(t[p].l == t[p].r) t[p].len = 0; //叶子节点且未覆盖38 else t[p].len = t[p<<1].len + t[p<<1|1].len; //合并子节点39 }40}41
42void modify(int p,int l,int r,int x){43 if(l <= t[p].l && r >= t[p].r){44 t[p].cnt += x;45 pushup(p);//更新当前节点,标记永久化(利用覆盖计数的特性)46 return;47 }//不需要pushdown下传标记48 int mid = t[p].l + t[p].r >> 1;49 if(l <= mid) modify(p<<1,l,r,x);50 if(r > mid) modify(p<<1|1,l,r,x);51 pushup(p);//更新当前节点52}53
54int main(){55 std::cin >> n;56 std::vector<line>e(1);57 for(int i = 1;i <= n;i++){//读入坐标,并转化为线58 int x1,y1,x2,y2;std::cin >> x1 >> y1 >> x2 >> y2;59 e.push_back({x1,x2,y1,1});60 e.push_back({x1,x2,y2,-1});61 hs.push_back(x1);62 hs.push_back(x2);63 }64 //离散化65 std::sort(e.begin()+1,e.end());66 std::sort(hs.begin()+1,hs.end());67 hs.erase(std::unique(hs.begin()+1,hs.end()),hs.end());68 int m = hs.size()-1;69 for(int i = 1;i <= n << 1;i++){70 e[i].l = std::lower_bound(hs.begin()+1,hs.end(),e[i].l) - hs.begin();71 e[i].r = std::lower_bound(hs.begin()+1,hs.end(),e[i].r) - hs.begin();72 }73
74 build(1,1,m-1);//建树范围为区间段个数,m个点对应m-1个区间(开m个也没关系)75
76 long long ans = 0;77 for(int i = 1;i <= n << 1;i++){//最后一条边不用累加上答案,但仍需要modify处理,否则多测会导致线段树数据残留78 auto &[l,r,h,w] = e[i];79 modify(1,l,r-1,w);//左闭右开区间80 if(i < n << 1) ans += (long long)t[1].len * (e[i+1].h - e[i].h);//面积+=当前全局覆盖长度*上下高度差81 }82 std::cout << ans;83}
周长并
P1856 [IOI 1998][USACO5.5] 矩形周长 Picture - 洛谷 (luogu.com.cn)

横边的总长度 =
xxxxxxxxxx971234
5const int N = 10004;6int n;7std::vector<int>hs(1,-2e9);8
9struct line{10 int l,r,h,w;11 bool operator < (const line &e2) const{12 if(h == e2.h) return w > e2.w;//高度相同先扫描底边,否则会多算这条边13 return h < e2.h;14 }15};16
17struct ST{18 int l,r;19 int len,cnt;20 int c;//c:区间线段条数21 bool lc,rc;//lc、rc:左右端点是否被覆盖,辅助计算c22}t[N<<2];23
24void build(int p,int l,int r){25 t[p] = {l,r};26 if(l == r){27 return;28 }29 int mid = l + r >> 1;30 build(p<<1,l,mid);build(p<<1|1,mid+1,r);31}32
33void pushup(int p){34 if(t[p].cnt){//当前区间完全被覆盖35 t[p].len = hs[t[p].r+1] - hs[t[p].l];36 t[p].lc = t[p].rc = t[p].c = 1;37 }38 else{39 if(t[p].l == t[p].r) {//当前区间未被覆盖且为子区间40 t[p].len = 0;41 t[p].lc = t[p].rc = t[p].c = 0;42 }43 else {//合并两个子区间44 t[p].len = t[p<<1].len + t[p<<1|1].len;45 t[p].lc = t[p<<1].lc;46 t[p].rc = t[p<<1|1].rc;47 t[p].c = t[p<<1].c + t[p<<1|1].c;48 if(t[p<<1].rc && t[p<<1|1].lc) t[p].c--;49 }50 }51}52
53void modify(int p,int l,int r,int x){54 if(l <= t[p].l && r >= t[p].r){55 t[p].cnt += x;56 pushup(p);57 return;58 }59 int mid = t[p].l + t[p].r >> 1;60 if(l <= mid) modify(p<<1,l,r,x);61 if(r > mid) modify(p<<1|1,l,r,x);62 pushup(p);63}64
65int main(){66 std::cin >> n;67 std::vector<line>e(1);68 for(int i = 1;i <= n;i++){69 int x1,y1,x2,y2;std::cin >> x1 >> y1 >> x2 >> y2;70 e.push_back({x1,x2,y1,1});71 e.push_back({x1,x2,y2,-1});72 hs.push_back(x1);73 hs.push_back(x2);74 }75
76 std::sort(e.begin()+1,e.end());77 std::sort(hs.begin()+1,hs.end());78 hs.erase(std::unique(hs.begin()+1,hs.end()),hs.end());79 for(int i = 1;i <= n << 1;i++){80 e[i].l = std::lower_bound(hs.begin()+1,hs.end(),e[i].l) - hs.begin();81 e[i].r = std::lower_bound(hs.begin()+1,hs.end(),e[i].r) - hs.begin();82 }83 int m = hs.size() - 1;84
85 build(1,1,m-1);86
87 long long ans = 0;88 long long last = 0;89 for(int i = 1;i <= n << 1;i++){90 auto &[l,r,h,w] = e[i];91 modify(1,l,r-1,w);92 ans += std::abs(t[1].len - last);//计算横边(i <= 2*n)93 last = t[1].len;94 if(i < n << 1) ans += 2 * t[1].c * (e[i+1].h - e[i].h);//计算竖边(i <= 2*n-1)95 }96 std::cout << ans;97}
二维数点
U245713 【模板】 二维数点 - 洛谷 (luogu.com.cn)
给定一个长度为
n的序列a[],然后进行m次询问:求区间[l,r]内大小在[x,y]范围内的数的个数。
把a[i]看作平面上的一个点(i,a[i]),询问即为求矩形内包含多少个点。
我们将询问离线,用一条线从左往右扫描,每遇到一个点就把它加入一个集合,表示当前所有扫过的点,用权值树状数组维护集合。每个询问的答案即为1~r内位于[x,y]的数的个数减去1~l-1内位于[x,y]的数的个数。
xxxxxxxxxx901234
5const int N = 1000006;6
7struct ask{8 int l,r,x,y; 9};10
11struct node{12 int x,y,w,id;//w = 1表示当前为r,w = -1表示当前为l-113};14std::vector<node>e[N];15
16int t[N*3],siz;//对离散化后的a[],x[],y[]建立权值树状数组,至少开三倍大小17void add(int i,int x){18 while(i <= siz){19 t[i] += x;20 i += i&-i;21 }22}23int query(int i){24 int res = 0;25 while(i){26 res += t[i];27 i -= i&-i;28 }29 return res;30}31
32std::vector<int> sol(std::vector<int>&a,std::vector<ask>&q){33 std::vector<int>ans(q.size());34 std::vector<int>hs(1,-2e9);35
36 for(int i = 1;i < a.size();i++){37 hs.push_back(a[i]);38 }39
40 for(int i = 1;i < q.size();i++){41 auto &[l,r,x,y] = q[i];42 hs.push_back(x);43 hs.push_back(y);44 }45
46 std::sort(hs.begin()+1,hs.end());47 hs.erase(std::unique(hs.begin()+1,hs.end()));48
49 siz = hs.size();50
51 for(int i = 1;i < a.size();i++){52 a[i] = std::lower_bound(hs.begin()+1,hs.end(),a[i]) - hs.begin();53 }54 for(int i = 1;i < q.size();i++){55 auto &[l,r,x,y] = q[i];56 x = std::lower_bound(hs.begin()+1,hs.end(),x) - hs.begin();57 y = std::lower_bound(hs.begin()+1,hs.end(),y) - hs.begin();58 e[l-1].push_back({x,y,-1,i});59 e[r].push_back({x,y,1,i});60 }61
62 for(int i = 1;i < a.size();i++){63 add(a[i],1);64 for(auto &[x,y,w,id]:e[i]){65 ans[id] += w*(query(y) - query(x-1));//query(y) - query(x-1)即为当前位于[x,y]的点的个数66 }67 }68 return ans;69}70
71int main(){72 std::ios::sync_with_stdio(false);std::cin.tie(0);73 int n,m; std::cin >> n >> m;74 std::vector<int> a(n+1);75 std::vector<ask> q(m+1);76 for(int i = 1;i <= n;i++){77 std::cin >> a[i];78 }79
80 for(int i = 1;i <= m;i++){81 auto &[l,r,x,y] = q[i];82 std::cin >> l >> r >> x >> y;83 }84
85 auto ans = sol(a,q);86
87 for(int i = 1;i <= m;i++){88 std::cout << ans[i] << '\n';89 }90}类似的,诺给定二维平面上的点集[x,y],然后给出诺干个询问求矩形[x1,y1,x2,y2]内有多少个点,则需要分别将所有横纵坐标都离散化。例题[P2163 SHOI2007] 园丁的烦恼 - 洛谷 (luogu.com.cn)
xxxxxxxxxx1011234
5const int N = 500005;6
7struct Point{8 int x,y;9};10
11struct Ask{12 int x1,y1,x2,y2;13};14
15struct Node{16 int x,y,w,id;17};18std::vector<Node>e[N*3];//x,x1,x219
20int t[N*3],siz;//y,y1,y121void add(int i,int x){22 while(i <= siz){23 t[i] += x;24 i += i&-i;25 }26}27int query(int i){28 int res = 0;29 while(i){30 res += t[i];31 i -= i&-i;32 }33 return res;34}35
36std::vector<int> sol(std::vector<Point>&p,std::vector<Ask>&ask){37 std::vector<int> ans(ask.size());38 std::vector<int> dx(1,-2e9),dy(1,-2e9);39 for(int i = 1;i < p.size();i++){40 auto &[x,y] = p[i];41 dx.push_back(x);42 dy.push_back(y);43 }44
45 for(int i = 1;i < ask.size();i++){46 auto &[x1,y1,x2,y2] = ask[i];47 dx.push_back(x1); dx.push_back(x2);48 dy.push_back(y1); dy.push_back(y2);49 }50
51 std::sort(dx.begin()+1,dx.end()); dx.erase(std::unique(dx.begin()+1,dx.end()),dx.end());52 std::sort(dy.begin()+1,dy.end()); dy.erase(std::unique(dy.begin()+1,dy.end()),dy.end());53 siz = dy.size()-1;54
55 for(int i = 1;i < p.size();i++){56 auto &[x,y] = p[i];57 x = std::lower_bound(dx.begin()+1,dx.end(),x) - dx.begin();58 y = std::lower_bound(dy.begin()+1,dy.end(),y) - dy.begin();59 e[x].push_back({y,y,0,0});60 }61
62 for(int i = 1;i < ask.size();i++){63 auto &[x1,y1,x2,y2] = ask[i];64 x1 = std::lower_bound(dx.begin()+1,dx.end(),x1) - dx.begin();65 x2 = std::lower_bound(dx.begin()+1,dx.end(),x2) - dx.begin();66 y1 = std::lower_bound(dy.begin()+1,dy.end(),y1) - dy.begin();67 y2 = std::lower_bound(dy.begin()+1,dy.end(),y2) - dy.begin();68 e[x1-1].push_back({y1,y2,-1,i});69 e[x2].push_back({y1,y2,1,i});70 }71
72 //这里没有分开将点和询问分开存储,而是用e[i].w为0或者1/-1,表示当前是点还是询问。因为是先push点,再push询问,保证了处理询问前会先处理与询问相关的点73 for(int i = 1;i < dx.size();i++){74 for(auto &[y1,y2,w,id]:e[i]){75 if(w == 0){ add(y1,1); }//扫描到点,则将点加入集合76 else{ ans[id] += w*(query(y2) - query(y1-1)); }//否则处理询问77 }78 }79 return ans;80}81
82int main(){83 int n,m; std::cin >> n >> m;84 std::vector<Point>p(n+1); 85 std::vector<Ask>ask(m+1);86 for(int i = 1;i <= n;i++){87 auto &[x,y] = p[i];88 std::cin >> x >> y;89 }90
91 for(int i = 1;i <= m;i++){92 auto &[x1,y1,x2,y2] = ask[i];93 std::cin >> x1 >> y1 >> x2 >> y2;94 }95
96 auto ans = sol(p,ask);97
98 for(int i = 1;i <= m;i++){99 std::cout << ans[i] << '\n';100 }101}
平面几何
点线封装
| 点、向量 Point | ||
|---|---|---|
p1 +- p2 | 向量加减 | |
p */ x | 向量乘除标量 | |
dot(p1,p2) | 点积 | |
cross(p1,p2) | 叉积 | |
length(p) | 向量的模 | |
normalize(p) | 单位向量 | |
distance(p1,p2) | 两点之间距离(欧几里得距离) | |
rotate(p) | 向量逆时针旋转90度 | |
sgn(p) | 判断向量所在象限(一/四:1,二/三:-1) | |
pointInPolygon(p,vec<Point>) | 判断 | 多边形的点需要按顺/逆时针顺序排列,仅支持简单多边形(不自交) |
| 线 Line | ||
|---|---|---|
length(l) | ||
distancePS(p,l) | ||
pointOnSegment(p,l) | 判断 | |
distanceSS(l1,l2) | 两 | |
segmentIntersection(l1,l2) | 返回值: tuple(类型,交点1,交点2) | 0:不相交 1:严格相交(交点在两线段内部) 2:重叠(返回重叠部分的两个端点) 3:端点相交(交点为线段端点) |
segmentInPolygon(l,vec<Point>) | (需满足:端点在内且不与任何边 | |
distancePL(p,l) | ||
parallel(l1,l2) | 判断两 | 1:共线 2:平行 0:相交 |
lineIntersection(l1,l2) | 求两 | |
pointOnLineLeft(p,l) | 以直线的方向向量 | |
hp(vec<Line>) | 返回 | 传入的参数不需要预先排列 |
xxxxxxxxxx3451template<typename T>2struct Point {3 T x,y;4 Point (const T &_x = 0,const T &_y = 0) : x(_x),y(_y){}5
6 template<typename U>//自动类型转换7 operator Point<U>() {8 return Point<U>(U(x), U(y));9 }10
11 Point &operator+=(const Point &p) & {12 x += p.x; y += p.y;13 return *this;14 }15 Point &operator-=(const Point &p) & {16 x -= p.x; y -= p.y;17 return *this;18 }19 Point &operator*=(const T &v) & {20 x *= v; y *= v;21 return *this;22 }23 Point &operator/=(const T &v) & {24 x /= v; y /= v;25 return *this;26 }27 Point operator-() const { return Point(-x, -y); }28 friend Point operator+(Point a, const Point &b) { return a += b; }29 friend Point operator-(Point a, const Point &b) { return a -= b; }30 friend Point operator*(Point a, const T &b) { return a *= b; }31 friend Point operator/(Point a, const T &b) { return a /= b; }32 friend Point operator*(const T &a, Point b) { return b *= a; }33 friend bool operator==(const Point &a, const Point &b) { return a.x == b.x && a.y == b.y; }34 friend bool operator!=(const Point &a,const Point &b) { return !(a == b);}35 36 friend std::istream &operator>>(std::istream &is, Point &p) {37 return is >> p.x >> p.y;38 }39 friend std::ostream &operator<<(std::ostream &os, const Point &p) {40 return os << "(" << p.x << "," << p.y << ")";41 }42};43
44template<typename T>//点乘45T dot(const Point<T> &p1,const Point<T>&p2){46 return p1.x*p2.x + p1.y*p2.y;47}48
49template<typename T>//叉乘50T cross(const Point<T> &p1,const Point<T>&p2){51 return p1.x*p2.y - p1.y*p2.x;52}53
54template<typename T>//向量的模55double length(const Point<T> &p){56 return std::sqrt(dot(p,p));57}58
59template<typename T>//单位向量60Point<T> normalize(const Point<T> &p) {61 return p/length(p);62}63
64template<typename T>//两点之间距离(欧几里得距离)65double distance(const Point<T> &p1,const Point<T> &p2){66 return length(p1-p2);67}68
69template<typename T>//向量逆时针旋转90度70Point<T> rotate(const Point<T> &p){71 return Point(-p.y,p.x);72}73
74template<typename T>// 判断向量所在象限(第一/四象限为1,第二/三象限为-1)75int sgn(const Point<T> &a) {76 return a.y > 0 || (a.y == 0 && a.x > 0) ? 1 : -1;77}78
79
80template<typename T>81struct Line{82 Point<T>a,b;83 Line (const Point<T> &_a = Point<T>(),const Point<T> &_b = Point<T>()) : a(_a),b(_b){}84 85 template<typename U> 86 operator Line<U>(){87 return Line<U>(Point<U>(a),Point<U>(b));88 }89};90
91template<typename T>//线段长度92double length(const Line<T> &l){93 return length(l.a-l.b);94}95
96template<typename T>//判断两直线是否平行(叉积为0)97int parallel(const Line<T> &l1,const Line<T> &l2){98 if(cross(l1.b-l1.a,l2.b-l2.a) == 0){99 if(distancePL(l2.b,l1) == 0) return 1;//共线100 else return 2;//平行101 }102 return 0;103}104
105template<typename T>//点到直线距离106double distancePL(const Point<T> &p,const Line<T> &l){107 return std::abs(cross(l.a-l.b,l.a-p)) / length(l);108}109
110template<typename T>//点到线段距离111double distancePS(const Point<T> &p, const Line<T> &l) {112 if (dot(p - l.a, l.b - l.a) < 0) { return distance(p, l.a); }113 if (dot(p - l.b, l.a - l.b) < 0) { return distance(p, l.b); }114 return distancePL(p, l);115}116
117template<typename T>//点是否在直线左侧118bool pointOnLineLeft(const Point<T> &p, const Line<T> &l) {119 return cross(l.b - l.a, p - l.a) > 0;120}121
122template<typename T>//求两直线交点(需确保不平行)123Point<T> lineIntersection(const Line<T> &l1, const Line<T> &l2) {124 return l1.a + (l1.b - l1.a) * (cross(l2.b - l2.a, l1.a - l2.a) / cross(l2.b - l2.a, l1.a - l1.b));125}126
127template<typename T>//判断点是否在线段上128bool pointOnSegment(const Point<T> &p, const Line<T> &l) {129 return cross(p - l.a, l.b - l.a) == 0 && std::min(l.a.x, l.b.x) <= p.x && p.x <= std::max(l.a.x, l.b.x)130 && std::min(l.a.y, l.b.y) <= p.y && p.y <= std::max(l.a.y, l.b.y);131}132
133template<typename T>//判断点是否在多边形内部(或多边形上) 0_idx,多边形点集需要按顺序排列134bool pointInPolygon(const Point<T> &a, const std::vector<Point<T>> &p) {135 int n = p.size();136 for (int i = 0; i < n; i++) {137 if (pointOnSegment(a, Line(p[i], p[(i + 1) % n]))) {138 return true;139 }140 }141 142 int t = 0;143 for (int i = 0; i < n; i++) {144 auto u = p[i];145 auto v = p[(i + 1) % n];146 if (u.x < a.x && v.x >= a.x && pointOnLineLeft(a, Line(v, u))) {147 t ^= 1;148 }149 if (u.x >= a.x && v.x < a.x && pointOnLineLeft(a, Line(u, v))) {150 t ^= 1;151 }152 }153 return t == 1;154}155
156/*157线段相交判定158返回值:tuple(类型,交点1,交点2)159类型:160 0:不相交161 1:严格相交(交点在两线段内部)162 2:重叠(返回重叠部分的两个端点)163 3:端点相交(交点为线段端点)164*/165template<typename T>166std::tuple<int, Point<T>, Point<T>> segmentIntersection(const Line<T> &l1, const Line<T> &l2) {167 if (std::max(l1.a.x, l1.b.x) < std::min(l2.a.x, l2.b.x)) {168 return {0, Point<T>(), Point<T>()};169 }170 if (std::min(l1.a.x, l1.b.x) > std::max(l2.a.x, l2.b.x)) {171 return {0, Point<T>(), Point<T>()};172 }173 if (std::max(l1.a.y, l1.b.y) < std::min(l2.a.y, l2.b.y)) {174 return {0, Point<T>(), Point<T>()};175 }176 if (std::min(l1.a.y, l1.b.y) > std::max(l2.a.y, l2.b.y)) {177 return {0, Point<T>(), Point<T>()};178 }179 if (cross(l1.b - l1.a, l2.b - l2.a) == 0) {180 if (cross(l1.b - l1.a, l2.a - l1.a) != 0) {181 return {0, Point<T>(), Point<T>()};182 } 183 else {184 auto maxx1 = std::max(l1.a.x, l1.b.x);185 auto minx1 = std::min(l1.a.x, l1.b.x);186 auto maxy1 = std::max(l1.a.y, l1.b.y);187 auto miny1 = std::min(l1.a.y, l1.b.y);188 auto maxx2 = std::max(l2.a.x, l2.b.x);189 auto minx2 = std::min(l2.a.x, l2.b.x);190 auto maxy2 = std::max(l2.a.y, l2.b.y);191 auto miny2 = std::min(l2.a.y, l2.b.y);192 Point<T> p1(std::max(minx1, minx2), std::max(miny1, miny2));193 Point<T> p2(std::min(maxx1, maxx2), std::min(maxy1, maxy2));194 if (!pointOnSegment(p1, l1)) { std::swap(p1.y, p2.y); }195 if (p1 == p2) return {3, p1, p2};196 else return {2, p1, p2}; 197 }198 }199 auto cp1 = cross(l2.a - l1.a, l2.b - l1.a);200 auto cp2 = cross(l2.a - l1.b, l2.b - l1.b);201 auto cp3 = cross(l1.a - l2.a, l1.b - l2.a);202 auto cp4 = cross(l1.a - l2.b, l1.b - l2.b);203 204 if ((cp1 > 0 && cp2 > 0) || (cp1 < 0 && cp2 < 0) || (cp3 > 0 && cp4 > 0) || (cp3 < 0 && cp4 < 0)) {205 return {0, Point<T>(), Point<T>()};206 }207 208 Point p = lineIntersection(l1, l2);209 if (cp1 != 0 && cp2 != 0 && cp3 != 0 && cp4 != 0) return {1, p, p};210 else return {3, p, p};211}212
213template<typename T>//返回两线段最短距离214double distanceSS(const Line<T> &l1, const Line<T> &l2) {215 if (std::get<0>(segmentIntersection(l1, l2)) != 0) {216 return 0.0;217 }218 return std::min({distancePS(l1.a, l2), distancePS(l1.b, l2), distancePS(l2.a, l1), distancePS(l2.b, l1)});219}220
221template<typename T>//线段是否在多边形内部(需满足:端点在内且不与任何边严格相交),多边形点集需要按顺序排列222bool segmentInPolygon(const Line<T> &l, const std::vector<Point<T>> &p) {223 int n = p.size();224 if (!pointInPolygon(l.a, p)) { return false; }225 if (!pointInPolygon(l.b, p)) { return false; }226 for (int i = 0; i < n; i++) {227 auto u = p[i];228 auto v = p[(i + 1) % n];229 auto w = p[(i + 2) % n];230 auto [t, p1, p2] = segmentIntersection(l, Line(u, v));231
232 if (t == 1) { return false; }233 if (t == 0) { continue; }234 if (t == 2) {235 if (pointOnSegment(v, l) && v != l.a && v != l.b) {236 if (cross(v - u, w - v) > 0) {237 return false;238 }239 }240 } 241 else {242 if (p1 != u && p1 != v) {243 if (pointOnLineLeft(l.a, Line(v,u)) || pointOnLineLeft(l.b, Line(v,u))) {244 return false;245 }246 } 247 else if (p1 == v) {248 if (l.a == v) {249 if (pointOnLineLeft(u, l)) {250 if (pointOnLineLeft(w, l) && pointOnLineLeft(w, Line(u, v))) {251 return false;252 }253 } 254 else {255 if (pointOnLineLeft(w, l) || pointOnLineLeft(w, Line(u, v))) {256 return false;257 }258 }259 } 260 else if (l.b == v) {261 if (pointOnLineLeft(u, Line(l.b, l.a))) {262 if (pointOnLineLeft(w, Line(l.b, l.a)) && pointOnLineLeft(w,Line(u,v))) {263 return false;264 }265 } 266 else {267 if (pointOnLineLeft(w, Line(l.b,l.a)) || pointOnLineLeft(w,Line(u,v))) {268 return false;269 }270 }271 } 272 else {273 if (pointOnLineLeft(u, l)) {274 if (pointOnLineLeft(w, Line(l.b,l.a)) || pointOnLineLeft(w,Line(u,v))) {275 return false;276 }277 } 278 else {279 if (pointOnLineLeft(w, l) || pointOnLineLeft(w, Line(u, v))) {280 return false;281 }282 }283 }284 }285 }286 }287 return true;288}289
290template<typename T>//返回凸多边形顶点集 0_idx291std::vector<Point<T>> hp(std::vector<Line<T>> lines) {292 std::sort(lines.begin(), lines.end(), [&](auto l1, auto l2) {293 auto d1 = l1.b - l1.a;294 auto d2 = l2.b - l2.a;295 296 if (sgn(d1) != sgn(d2)) {297 return sgn(d1) == 1;298 }299 300 return cross(d1, d2) > 0;301 });302 303 std::deque<Line<T>> ls;304 std::deque<Point<T>> ps;305 for (auto l : lines) {306 if (ls.empty()) {307 ls.push_back(l);308 continue;309 }310 311 while (!ps.empty() && !pointOnLineLeft(ps.back(), l)) {312 ps.pop_back();313 ls.pop_back();314 }315 316 while (!ps.empty() && !pointOnLineLeft(ps[0], l)) {317 ps.pop_front();318 ls.pop_front();319 }320 321 if (cross(l.b - l.a, ls.back().b - ls.back().a) == 0) {322 if (dot(l.b - l.a, ls.back().b - ls.back().a) > 0) {323 if (!pointOnLineLeft(ls.back().a, l)) {324 assert(ls.size() == 1);325 ls[0] = l;326 }327 continue;328 }329 return {};330 }331 332 ps.push_back(lineIntersection(ls.back(), l));333 ls.push_back(l);334 }335 336 while (!ps.empty() && !pointOnLineLeft(ps.back(), ls[0])) {337 ps.pop_back();338 ls.pop_back();339 }340 if (ls.size() <= 2) { return {}; }341
342 ps.push_back(lineIntersection(ls[0], ls.back()));343 344 return std::vector(ps.begin(), ps.end());345}
其它
中位数
一般要求最大化中位数之类的题目,可以二分一个数x,诺a[i] >= x则赋权值w[i]为1,否则为-1,判断某一区间中位数是否大于等于x,即求该区间和是否大于0。
对顶堆
动态维护中位数,插入/删除O(log),查找O(1)
se1的末尾元素即为中位数
xxxxxxxxxx271multiset<int>se1,se2;2
3void balance(){//每次插入/删除后平衡两个集合4 while(se1.size() > se2.size()){5 se2.insert(*se1.rbegin());6 se1.erase(prev(se1.end()));7 }8 while(se1.size() < se2.size()){9 se1.insert(*se2.begin());10 se2.erase(se2.begin());11 }12}13
14void add(int x){//插入元素15 se1.insert(x);16 balance();17}18
19void del(int x){//删除元素20 if(x <= *se1.rbegin()) se1.erase(se1.find(x));21 else se2.erase(se2.find(x));22 balance();23}24
25int query(){//查找中位数26 return *se1.rbegin();27}
例题:
题意:定义一个数组的连续子数组(a[l] ~ a[r])每一项满足
为一个彩虹子数组,可以至多进行 k 次操作使任意 或 ,求能构造出的最长彩虹子数组 思路:考虑
可以转化为 ,所以先预处理把所有的 减去自己下标,问题就变成了:把一个区间的所有数字全部变成一个相同的数字,很容易知道,最小代价就是把所有数变成中位数即可,所以接下来就用一个双指针滑动窗口来维护,求最大的区间长度,这里动态维护滑动窗口的中位数用两个multiset
xxxxxxxxxx791//https://codeforces.com/gym/104901/problem/K2345using namespace std;6using ll = long long;7int n;8
9multiset<int>se1,se2;10ll sum1,sum2;//sum1统计se1的和,sum2统计se2的和11
12void balance(){13 while(se1.size() > se2.size()){14 sum2 += *se1.rbegin();15 sum1 -= *se1.rbegin();16 se2.insert(*se1.rbegin());17 se1.erase(prev(se1.end()));18 }19 while(se1.size() < se2.size()){20 sum1 += *se2.begin();21 sum2 -= *se2.begin();22 se1.insert(*se2.begin());23 se2.erase(se2.begin());24 }25}26
27void add(int x){28 se1.insert(x);29 sum1 += x;30 balance();31}32
33void del(int x){34 if(x <= *se1.rbegin()) {se1.erase(se1.find(x)),sum1 -= x;}35 else {se2.erase(se2.find(x)),sum2 -= x;}36 balance();37}38
39ll query(){40 return *se1.rbegin();41}42
43ll all(){//返回 使所有数变为中位数所需要的操作次数44 return (se1.size()*query()-sum1) + (sum2-se2.size()*query());45}46
47void sol(){48 se1.clear();se2.clear();49 sum1 = sum2 = 0;50 ll n,k;cin >> n >> k;51 vector<ll>a(n+1);52 for(int i = 1;i <= n;i++){53 cin >> a[i];54 a[i] -= i;55 }56 int ans = 1;57 ll sum = 0;58 for(int l = 0,r = 0;r <= n;){59 while(sum <= k){60 ans = max(ans,r-l+1);61 r++;62 if(r > n) break;63 add(a[r]);64 sum = all();65 }66 while(sum > k){67 l++;68 del(a[l]);69 sum = all();70 }71 }72 cout << ans-1 << endl;73}74
75int main(){76 int tt;cin >> tt;77 while(tt--){ sol(); }78 return 0;79}
第k小数求中位数
查找O(N)
快排 或STL函数nth_element()
主席树
可持久化权值线段树
求区间第k小,不支持修改
xxxxxxxxxx701//https://ac.nowcoder.com/acm/contest/91177/F2345using namespace std;6constexpr int MAXN = 1e5;7int tot, n, m;8int sum[(MAXN << 5) + 10], rt[MAXN + 10], ls[(MAXN << 5) + 10],rs[(MAXN << 5) + 10];9int a[MAXN + 10], ind[MAXN + 10], len;10
11int getid(const int &val) { // 离散化12 return lower_bound(ind + 1, ind + len + 1, val) - ind;13}14
15int build(int l, int r) { // 建树16 int root = ++tot;17 if (l == r) return root;18 int mid = l + r >> 1;19 ls[root] = build(l, mid);20 rs[root] = build(mid + 1, r);21 return root; // 返回该子树的根节点22}23
24int update(int k, int l, int r, int root) { // 插入操作25 int dir = ++tot;26 ls[dir] = ls[root], rs[dir] = rs[root], sum[dir] = sum[root] + 1;27 if (l == r) return dir;28 int mid = l + r >> 1;29 if (k <= mid)30 ls[dir] = update(k, l, mid, ls[dir]);31 else32 rs[dir] = update(k, mid + 1, r, rs[dir]);33 return dir;34}35
36int query(int u, int v, int l, int r, int k) { // 查询操作37 int mid = l + r >> 1,38 x = sum[ls[v]] - sum[ls[u]]; // 通过区间减法得到左儿子中所存储的数值个数39 if (l == r) return l;40 if (k <= x) // 若 k 小于等于 x ,则说明第 k 小的数字存储在在左儿子中41 return query(ls[u], ls[v], l, mid, k);42 else // 否则说明在右儿子中43 return query(rs[u], rs[v], mid + 1, r, k - x);44}45
46void init() {47 scanf("%d%d", &n, &m);48 for (int i = 1; i <= n; ++i) scanf("%d", a + i);49 memcpy(ind, a, sizeof ind);50 sort(ind + 1, ind + n + 1);51 len = unique(ind + 1, ind + n + 1) - ind - 1;52 rt[0] = build(1, len);53 for (int i = 1; i <= n; ++i) rt[i] = update(getid(a[i]), 1, len, rt[i - 1]);54}55
56int l, r, k;57
58void work() {59 while (m--) {60 cin >> l >> r;//查询区间[l,r]中第k小61 //cin >> k;62 k = (r-l+2) >> 1;63 printf("%d\n", ind[query(rt[l - 1], rt[r], 1, len, k)]);64 }65}66
67int main() {68 init();69 work();70}
均值不等式
约瑟夫环
n 个人标号
。逆时针站一圈,从 号开始,每一次从当前的人逆时针数 个,然后让这个人出局。问最后剩下的人是谁。
xxxxxxxxxx61//O(N)2int josephus(int n, int k) {3 int res = 0;4 for (int i = 1; i <= n; ++i) res = (res + k) % i;5 return res;6}xxxxxxxxxx111//O(KlogN)2int josephus(int n, int k) {3 if (n == 1) return 0;4 if (k == 1) return n - 1;5 if (k > n) return (josephus(n - 1, k) + k) % n; // 线性算法6 int res = josephus(n - n / k, k);7 res -= n % k;8 if (res < 0) res += n; // mod n9 else res += res / (k - 1); // 还原位置10 return res;11}
吉姆拉尔森日期公式
给定具体日期,推当天是星期几。也可以求两个日期的天数之差等等。
xxxxxxxxxx221const int d[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};2
3bool isLeap(int y) {//判断当前年是否为闰年4 return y % 400 == 0 || (y % 4 == 0 && y % 100 != 0);5}6
7int daysInMonth(int y, int m) {8 return d[m - 1] + (isLeap(y) && m == 2);9}10
11int getDay(int y, int m, int d) {12 int ans = 0;13 for (int i = 1970; i < y; i++) {14 ans += 365 + isLeap(i);15 }16 for (int i = 1; i < m; i++) {17 ans += daysInMonth(y, i);18 }19 ans += d;20 //return ans; //返回今天是从1970.1.1起的第几天?(1970.1.1为第1天,星期4)21 return (ans + 2) % 7 + 1;//返回今天是星期几?[1,2,3,4,5,6,7]22}
二维和一维坐标转化
下标从0开始
n行m列
idx = i*m + j
(i,j) = (idx/m,idx%m)
| 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|
| 5 | 6 | 7 | 8 | 9 |
| 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 |
下标从1开始
n行m列
idx = (i-1)*m + j
(i,j) = ((idx+m-1)/m,(idx%m)? idx%m : m)
| 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|
| 6 | 7 | 8 | 9 | 10 |
| 11 | 12 | 13 | 14 | 15 |
| 16 | 17 | 18 | 19 | 20 |
基础算法
前缀和&差分
在头文件
中也包含了一维前缀和/差分函数 int a[6] = {1,2,3,4,5,6},s[6]; partial_sum(a,a+6,s); //s = {1,3,6,10,15,21} adjacent_difference(a,a+6,s); //s = {1,1,1,1,1,1}
一维前缀和
pre[i] = a[1]+a[2] +...+a[i] = pre[i-1]+a[i] 前缀和数组一般下标从1开始,方便写
xxxxxxxxxx141//求第a个数到第b个数的和23using namespace std;4int arr[1000006]; int pre[1000006];5int main() {6 int k; cin >> k;7 for (int i = 1;i <= k;i++){8 scanf("%d", &arr[i]);9 pre[i] += pre[i - 1] + arr[i];10 }11 int a, b; cin >> a >> b;12 cout << pre[b] - pre[a - 1] << endl;13 return 0;14}xxxxxxxxxx51//异或前缀和2for(int i = 1;i <= n;i++){3 s[i] = s[i-1]^a[i];4}5ans = s[r]^s[l-1];
二维前缀和
子矩阵的和:
xxxxxxxxxx61//每个点的S的求法,可以直接推2s[i][j] = s[i-1][j] + s[i][j-1] - s[i-1][j-1] + a[i][j];3 4//也可以先枚举维,然后对每一维做前缀和5s[i][j]=s[i][j-1]+a[i][j];6s[i][j]=s[i-1][j]+a[i][j];
xxxxxxxxxx21//某一块S的求法:s[x1][y1]~s[x2][y2]2s[x2][y2] - s[x1-1][y2] - s[x2][y1-1] + s[x1-1][y1-1];
| x1,y1 | ||||||
| x2,y2 | ||||||
xxxxxxxxxx2412using namespace std;3const int N = 10006;4int arr[N][N]; int s[N][N];5int n, m;6
7int query(int x1,int y1,int x2,int y2){8 return s[x2][y2] - s[x1-1][y2] - s[x2][y1-1] + s[x1-1][y1-1];9}10
11int main() {12 cin >> n >> m;13 for (int i = 1; i <= n; i++)14 for (int j = 1; j <= m; j++)15 scanf("%d", &arr[i][j]);16 for (int i = 1; i <= n; i++) //利用前缀和求每一点的S17 for (int j = 1; j <= m; j++)18 s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + arr[i][j];19 20 int x1, y1, x2, y2;21 cin >> x1 >> y1 >> x2 >> y2;//求子矩阵x1,y1 ~ x2,y2的S22 cout << query(x1,y1,x2,y2) << endl;23 return 0;24}
一维差分
dif[1] + dif[2] + ... + dif[i] = a[i]; 往往与前缀和联系
dif[i] = arr[i] - arr[i-1];
xxxxxxxxxx241//https://www.luogu.com.cn/problem/P236723using namespace std;4int arr[5000006], dif[5000006];5int main() {6 int n, p; cin >> n >> p;7 for (int i = 1; i <= n; i++){8 scanf("%d", &arr[i]);//输入数据较多时建议scanf/printf 比cin/cout快,9 dif[i] = arr[i] - arr[i - 1];10 }11 while (p--){12 int x, y, z;13 scanf("%d%d%d", &x, &y, &z);//区间[x,y]加x14 dif[x] += z;15 dif[y + 1] -= z;16 }17 int low = 1e9;18 for (int i = 1; i <= n; i++){19 arr[i] = arr[i - 1] + dif[i];20 low = low < arr[i] ? low : arr[i];21 }22 cout << low << endl;23 return 0;24}xxxxxxxxxx291//增减序列:https://www.acwing.com/problem/content/102/2//给定数组a[n],每次可选一个区间[l,r]内的数都+1或-1,求使所有数都一样的最小操作次数和最终序列有多少种345using namespace std;6const int N = 100005;7using ll = long long;8int n;9
10ll a[N],dif[N];11
12int main(){13 cin >> n;14 for(int i = 1;i <= n;i++){15 cin >> a[i];16 dif[i] = a[i] - a[i-1];17 }18
19 ll pos = 0,neg = 0;20 21 //如果这个数列的值都是一样的,最后我们的差分数组一定是b[1]=一个常数,b[2]~b[n]都等于022 for(int i = 2;i <= n;i++){23 if(dif[i] > 0) pos += dif[i];//pos表示sum(正数->0)24 if(dif[i] < 0) neg += -dif[i];//neg表示sum(负数->0)25 }//正负两个数可以相消二者取最小min,最后差分就只剩同符号的数abs(pos-neg)26 cout << min(pos,neg) + abs(pos - neg) << endl;27 cout << abs(pos - neg) + 1;28 return 0;29}
二维差分
xxxxxxxxxx391//https://www.acwing.com/problem/content/800/2//给定n*m的矩阵和q次插入操作,每次操作将子矩阵(x1,y1,x2,y2)的值加上c,求q次操作后的矩阵34using namespace std;5int a[1006][1006]; int dif[1006][1006];6int n, m, q;7
8void insert(int x1, int y1, int x2, int y2, int c) {9 dif[x1][y1] += c;10 dif[x2 + 1][y1] -= c;11 dif[x1][y2 + 1] -= c;12 dif[x2 + 1][y2 + 1] += c;13}14
15int main() {16 cin >> n >> m >> q;17 for (int i = 1; i <= n; i++){18 for (int j = 1; j <= m; j++){19 scanf("%d", &a[i][j]);20 insert(i, j, i, j, a[i][j]);//初始化插入a[i][j]21 }22 }23
24 while (q--) {25 int x1, y1, x2, y2, c; //进行q次插入操作26 scanf("%d%d%d%d%d", &x1, &y1, &x2, &y2, &c);27 insert(x1, y1, x2, y2, c);28 }29
30 for (int i = 1; i <= n; i++)//利用前缀和求改变后的数组31 for (int j = 1; j <= m; j++)32 dif[i][j] += dif[i-1][j] + dif[i][j-1] - dif[i-1][j-1];33
34 for (int i = 1; i <= n; i++) {35 for (int j = 1; j <= m; j++)36 printf("%d ", dif[i][j]);37 printf("\n");38 }39}
二分
二分查找
目标数组需要为非降序排列
xxxxxxxxxx3612using namespace std;3int n, x, arr[1000006];4bool check1(int mid) { 5 return arr[mid] >= x;6}7bool check2(int mid) {8 return arr[mid] <= x;9}10
11int main() {12 scanf("%d%d", &n, &x);13 for (int i = 0; i < n; i++) {14 scanf("%d", &arr[i]);15 }16
17 int l = 0, r = n - 1;//找到第一个>=x值的下标 (与lower_bound略有不同)18 while (l < r) { //0000x1111型 (x为目标) 19 int mid = l + r >> 1;20 if (check1(mid)) r = mid;21 else l = mid + 1;22 }23 if(arr[l] < x) cout << -1 << endl;//诺不存在>=x的值,返回的是最后一个元素下标,而lb返回的是end()24 else cout << l << endl;25
26 l = 0, r = n - 1;//找到最后一个<=x值的下标 (与upper_bound不同)27 while (l < r) { //1111x0000型 (x为目标)28 int mid = l + r + 1 >> 1;29 if (check2(mid)) l = mid;30 else r = mid - 1;31 }32 if(arr[l] > x) cout << -1 << endl;//诺不存在<=x的值,返回的是第一个元素的下标33 cout << l << endl;34
35 return 0;36}
二分答案
单调区间中,每次查找去掉不符合条件的一半区间,直到找到答案(整数二分)或者和答案十分接近
xxxxxxxxxx191//模版与二分查找一样2//check(mid)为:...00001111...型 (第一个1为目标)3int l = 0, r = 1e9;4 while (l < r) {5 int mid = l + r >> 1;6 if (check(mid)) r = mid;7 else l = mid + 1;8 }9cout << l;10
11
12//check(mid)为:...111110000...型 (最后一个1为目标)13int l = 0, r = 1e9;14 while (l < r){15 int mid = l + r + 1 >> 1;16 if (check(mid)) l = mid ;17 else r = mid - 1;18 }19cout << l;
xxxxxxxxxx571//最小化极值:https://codeforces.com/contest/2013/problem/D2//题意:对于数组a,可以任意次选择i(1~n-1),使a[i]--,a[i+1]++,请最小化极差3//思路:分别二分最小的最大值,最大的最小值,二者之差即为答案45using ll = long long;6using namespace std;7const int N = 200005;8int n;9
10bool check1(ll mid,vector<ll>&a){11 ll cnt = 0;12 for(int i = 1;i <= n;i++){13 if(a[i] > mid) cnt+=a[i]-mid;14 if(a[i] < mid) cnt = max((ll)0,cnt - (mid-a[i]));15 }16 return cnt == 0;17}18
19bool check2(ll mid,vector<ll>&a){20 ll cnt = 0;21 for(int i = 1;i <= n;i++){22 if(a[i] > mid) cnt += a[i]-mid;23 if(a[i] < mid) cnt -= mid-a[i];24 if(cnt < 0) return 0;25 }26 return 1;27}28
29void sol(){30 cin >> n;31 vector<ll>a(n+1);32 for_each(a.begin()+1,a.begin()+n+1,[](auto &x){cin >> x;});33 34 ll l = 0,r = 1e18;35 while(l < r){36 ll mid = l+r>> 1;37 if(check1(mid,a)) r=mid;38 else l=mid+1;39 }40 ll ans1 = l;41
42 l = 0,r = 1e18;43 while(l < r){44 ll mid = l+r+1 >> 1;45 if(check2(mid,a)) l = mid;46 else r = mid-1;47 }48 ll ans2 = l;49 cout << ans1 - ans2 << endl;50}51
52int main() {53 int T = 1; //fastio;54 cin >> T;55 while(T--){ sol(); }56 return 0;57}
实数域二分
xxxxxxxxxx71//一般实现为限制二分次数,或达到某个精度就停止2for(int ti = 1;ti <= 100;ti++){3 double mid = (l + r) / 2;4 if(check(mid)) r = mid;5 else l = mid;6}7std::cout << l << '\n';
xxxxxxxxxx111//例如:求一个数的开三次方2const double eps = 1e-8;3
4double n;cin >> n;5double l = -10000,r = 10000;6while(r-l >= eps){7 double mid = (l+r)/2;8 if(mid*mid*mid >= n) r = mid;9 else l = mid;10}11printf("%.6lf",r);
分数规划
分数规划用来求一个分式的极值。 每种物品有两个权值ai和bi,求一组w
{0,1},选出若干个物品使得 最小/最大。 一般分数规划问题还会有一些奇怪的限制,比如『分母至少为k』。
假如我们要求最大值,二分check一个mid,然后推柿子
==> ==> //诺不等式大于0说明mid可行 主要难点在于如何求
的最大值/最小值
xxxxxxxxxx481//https://vjudge.net/problem/POJ-2976#author=02//n门课至少选k门,使得平均学分最大3456using namespace std;7const int N = 1003;8int n,k;9vector<double>a(N),b(N);10
11bool cmp(double x,double y){12 return x > y;13}14
15bool check(double mid){//判断是否存在k个物品,其累计平均分>mid16 double s = 0;17 vector<double>c(n);18
19 for(int i = 0;i < n;i++){20 c[i] = a[i]-mid*b[i];//把c[i] = a[i]-mid*b[i]作为第i个物品的权值21 }22 sort(c.begin(),c.begin()+n,cmp);23 for(int i = 0;i < k;i++){24 s+=c[i];//贪心地选择权值最大的k个25 }26 return s > 0;27}28
29void uuz(){30 for(int i = 0;i < n;i++){cin >> a[i];}31 for(int i = 0;i < n;i++){cin >> b[i];}32 double l = 0,r = 1e9;33 while(r-l > 1e-6){34 double mid = (l+r)/2;35 //cout << mid << endl;36 if(check(mid)) l = mid;37 else r = mid;38 }39 int ans = r*100;40 cout << ans+(r*100-ans >= 0.5) << endl;41}42
43int main(){44 while(cin >> n >> k,n||k){45 k = n-k;46 uuz();47 }48}
子段和问题
无长度限制
求一个子段,它的和最大,没有“长度大于等于F”这个限制 无长度限制的最大子段和问题是一个经典问题,只需要O(n)扫描该序列,不断 地把新数加入子段,当子段和变为负数的时候,就把当前整个子段清空。扫描过程 中出现过的最大的子段和即为所求
区间[l,r]的最大子段和:详见线段树
有长度限制
求一个子段,他的和最大,长度不超过k:135. 最大子段和
详见 单调队列优化
求一个子段,他的和最大,长度不小于k:
子段可以转化为前缀和相减的形式,则有
xxxxxxxxxx341//最佳牛围栏:https://www.acwing.com/problem/content/104/2345using namespace std;6const int N = 100005;7int n,f;8int a[N];9double s[N];10
11bool check(double mid){//判断是否存在连续子段(长度>=f),其最大平均数max_avg >= 当前mid12 for(int i = 1;i <= n;i++){//相当于判断是否存在 max_avg - mid >= 013 s[i] = s[i-1]+(a[i] - mid);//a[i]-mid 的前缀和14 }15 double avg = 0;16 for(int i = f;i <= n;i++){17 avg = min(avg,s[i-f]);18 if(s[i]-avg >= 0) return 1;//诺s[i]-mins >= 0,说明存在子段其平均数>mid19 }20 return 0;21}22
23int main(){24 cin >> n >> f;25 for(int i = 1;i <= n;i++){cin >> a[i];}26 double l = 0,r = 2000;27 while(r-l > 1e-6){28 double mid = (l+r)/2;29 if(check(mid)) l = mid;//将问题转变为判定问题30 else r = mid;31 }32 r*=1000;//r为结果33 cout << (int)r << endl;34}
三分
如果需要求出单峰函数的极值点,通常使用二分法衍生出的三分法求单峰函数的极值点
三分法每次操作会舍去两侧区间中的其中一个。为减少三分法的操作次数,应使两侧区间尽可能大。因此,每一次操作时的
xxxxxxxxxx321//https://www.luogu.com.cn/problem/P33822//给出一个N次函数,保证在范围[𝑙,𝑟]内存在一点𝑥,使得[l,𝑥]上单调增,[𝑥,𝑟]上单调减。试求出𝑥的值。345using namespace std;6const int N = 20;7const double eps = 1e-7;8int n;9double l,r,a[N];10
11double f(double x){12 double ans = 0;13 for(int i = n;i >= 0;i--){14 ans+=a[i]*powl(x,i);15 }16 return ans;17}18
19int main(){20 cin >> n >> l >> r;21 for(int i = n;i >= 0;i--){ cin >> a[i]; }22 while(r-l > eps){23 double mid = (l+r)/2;24 double lmid = mid - eps;25 double rmid = mid + eps;26 if(f(lmid) > f(rmid)) r = mid;//诺要求凹函数峰值,把符号换成 < 即可27 else l = mid;28 }29 printf("%.6lf",l);30 31 return 0;32}
双指针
for(int i = 0,j = 0;i < n;i++){ while(j < i && check[i] [j]) { j++; //每道题的逻辑 } } 可以将O(n^2)优化到O(n);
xxxxxxxxxx231//最长连续无重复子序列23using namespace std;4const int N = 1000006;5int a[N], s[N];//s[a[i]]判断a[i]出现的次数6int n, res;7int main() {8 cin >> n;9 for (int i = 0;i < n;i++)10 cin >> a[i];11
12 for (int i = 0,j = 0;i < n;i++){13 s[a[i]] ++;14 while (s[a[i]] > 1) {15 //当右端点i的数出现次数>1,s[a[j]]--,左端点j右移16 s[a[j]]--;17 j++; 18 }19 res = max(res, i - j + 1); 20 }21 cout << res << endl;22 return 0;23}
排序
sort
需包含头文件
平均复杂度为O(NlogN),不保证稳定性
语法:sort(begin, end, cmp);
其中begin为指向待sort()的数组的
第一个元素的指针,end为指向待sort()的数组的最后一个元素的下一个位置的指针,cmp参数为排序准则(不写默认从小到大进行排序);
xxxxxxxxxx121//默认从小到大:2sort(arr, arr+n);//数组下标从0开始3sort(arr+1,arr+n+1)//数组下标从1开始4sort(v.begin(),v.end());5
6
7//从大到小:8sort(str.begin(),str.end(),greater<int>());9sort(str.rbegin(),str.rend());10sort(str.begin(),str.end(),cmp);//bool cmp(int a,int b){return a > b;}以a,b大小为依据排序11 //返回值为真,则a排在b前面12sort(e+1,e+n+1,[](auto &x,auto &y){return x.a > y.b;});//lambda表达式,结构体数组以a为权重排序
stable_sort()
stable_sort() 也是一种O(n log n)时间复杂度的排序算法,但它保证稳定性,即相等的元素将保持其原始顺序。这使得它在需要保持相等元素顺序的场景中很有用。注意,稳定排序的代价是额外的空间复杂度。
快速排序
不稳定
快速排序的最优时间复杂度和平均时间复杂度为
xxxxxxxxxx2412using namespace std;3const int N = 1e6 + 5;4int n;5int q[N];6void quick_sort(int q[], int l, int r) {7 if (l >= r) return;8 int x = q[l], i = l - 1, j = r + 1;9 while (i < j) {10 do i++; while (q[i] < x);11 do j--; while (q[j] > x);12 if (i < j) swap(q[i], q[j]);13 }14 quick_sort(q, l, j); //注意边界问题15 quick_sort(q, j + 1, r);16}17int main() {18 scanf("%d", &n);19 for (int i = 0; i < n; i++) scanf("%d", &q[i]);20 21 quick_sort(q, 0, n - 1);22
23 for (int i = 0; i < n; i++)printf("%d", q[i]);24}
第k小的数
xxxxxxxxxx261//https://www.luogu.com.cn/problem/P19232//k从第0小开始,诺要从1开始则++k即可,时间复杂度O(N)34using namespace std;5const int N = 5e6 + 5;6int n,k;7int q[N];8void quick_sort(int q[], int l, int r) {9 if (l >= r) return;10 int x = q[l], i = l - 1, j = r + 1;11 while (i < j) {12 do i++; while (q[i] < x);13 do j--; while (q[j] > x);14 if (i < j) swap(q[i], q[j]);15 }16 if(k <= j)quick_sort(q, l, j);//每次判断条件,只需排一边17 else quick_sort(q, j + 1, r);18}19int main() {20 scanf("%d %d", &n,&k);21 for (int i = 0; i < n; i++) scanf("%d", &q[i]);22 quick_sort(q, 0, n - 1);23 //stl函数实现了类似功能:nth_element(q,q+k,q+n);24 //诺下标从1开始:++k; nth_element(a+1,a+k,a+n+1);25 cout << q[k];26}
归并排序
稳定
时间复杂度在最优、最坏与平均情况下均为
先排左半边,再排右半边,最后合并。
xxxxxxxxxx211void merge_sort(int q[], int l, int r){2 //递归的终止条件3 if(l >= r) return;4
5 //第一步:分成子问题6 int mid = l + r >> 1;7
8 //第二步:递归处理子问题9 merge_sort(q, l, mid ), merge_sort(q, mid + 1, r);10
11 //第三步:合并子问题12 int i = l, j = mid + 1, k = 0, tmp[r - l + 1];13 while(i <= mid && j <= r){14 if(q[i] <= q[j]) tmp[k++] = q[i++];15 else tmp[k++] = q[j++];16 }17 while(i <= mid) tmp[k++] = q[i++];18 while(j <= r) tmp[k++] = q[j++];19
20 for(k = 0, i = l; i <= r; k++, i++) q[i] = tmp[k];21}
逆序对
xxxxxxxxxx311//https://www.luogu.com.cn/problem/P19082//O(NlogN)归并排序求逆序对数量、冒泡排序需要交换的次数3//也可离散化后用树状数组或线段树求逆序对45using namespace std;6const int N = 500005;7int n,a[N];8long long ans;9
10void merge_sort(int a[],int l,int r){11 if(l == r) return;12 int mid = l + r >> 1;13 merge_sort(a,l,mid);merge_sort(a,mid+1,r);14 int i = l,j = mid+1,t[r-l+1],k = 0;15 while(i <= mid && j <= r){16 if(a[i] <= a[j]) t[k++] = a[i++];17 else t[k++] = a[j++],ans += mid-i+1;//核心代码18 }19 while(i <= mid) t[k++] = a[i++];20 while(j <= r) t[k++] = a[j++];21 for(k = 0,i = l;i <= r;k++,i++){22 a[i] = t[k];23 }24}25
26int main(){27 cin >> n;28 for(int i = 1;i <= n;i++){ scanf("%d",&a[i]); }29 merge_sort(a,1,n);30 cout << ans;31}xxxxxxxxxx31//奇数码问题 https://www.acwing.com/problem/content/110/2//n*n (n为奇数) 二维按行化为一维(不包括0),归并排序1 ~ n*n-1求逆序对数量;3//诺两个数码逆序对奇偶性相同则可以转变。对于一个排列,交换任意两个元素的位置,必然改变逆序对奇偶性。
分治
就是把一个复杂的问题分成两个或更多的相同或相似的子问题,直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。
大概的流程可以分为三步:分解 -> 解决 -> 合并。
分解原问题为结构相同的子问题。
分解到某个容易求解的边界之后,进行递归求解。
将子问题的解合并成原问题的解。
分治法能解决的问题一般有如下特征:
该问题的规模缩小到一定的程度就可以容易地解决。
该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质,利用该问题分解出的子问题的解可以合并为该问题的解。
该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子问题。
P7883 平面最近点对(加强加强版) - 洛谷 (luogu.com.cn)
给定平面内n个点对p(x,y),求距离最近的两个点的距离(的平方)。由于输入的点为整点,因此这个值一定是整数。
将所有点排序后,将整个区间一分为二,分别递归处理,
xxxxxxxxxx68123using namespace std;4const int N = 400005;5int b[N];6
7struct node{8 long long x,y;9 bool operator < (const node& e2){10 return x != e2.x ? x < e2.x : y < e2.y;11 }12}a[N];13
14long long pow2(long long x) {return x*x;}15
16long long dist(node p1,node p2){17 return pow2(p1.x-p2.x) + pow2(p1.y-p2.y);18}19
20void merge(int l,int r){21 int mid = l + r >> 1;22 int i = l,j = mid+1,k = 0;23 node temp[r-l+1];24
25 while(i <= mid && j <= r){26 if(a[i].y < a[j].y) temp[k++] = a[i++];27 else temp[k++] = a[j++];28 }29 while(i <= mid) temp[k++] = a[i++];30 while(j <= r) temp[k++] = a[j++];31
32 for(i = l,k = 0;i <= r;i++,k++){33 a[i] = temp[k];34 }35}36
37long long sol(int l,int r){38 if(l >= r) return 8e18;39 if(l+1 == r){//边界:当前区间仅有两个点,直接计算即可。40 merge(l,r);41 return dist(a[l],a[r]);42 }43 int mid = l + r >> 1;44 long long midx = a[mid].x,cnt = 0;//因为合并时a[mid]会变,需要提前记录45 long long d = min(sol(l,mid),sol(mid+1,r));46 merge(l,r);47 for(int i = l;i <= r;i++){48 if(pow2(a[i].x - midx) < d){49 b[++cnt] = i;50 }51 }52 for(int i = 1;i <= cnt;i++){53 for(int j = i+1;j <= cnt && pow2(a[b[i]].y - a[b[j]].y) < d;j++){54 d = min(d,dist(a[b[i]],a[b[j]]));55 }56 }57 return d;58}59
60int main(){61 int n;cin >> n;62 for(int i = 1;i <= n;i++){63 cin >> a[i].x >> a[i].y;64 }65 sort(a+1,a+n+1);66 cout << sol(1,n);67// printf("%.4lf",sqrt(sol(1,n)));68}
二进制
xxxxxxxxxx101//求n的二进制第k位 从0开始数23using namespace std;4//10 = (1010)25int main() {6 int n, k;7 cin >> n >> k;8 cout << (n >> k & 1); //右k移位,再&1,k的值不会改变9 return 0;10}
lowbit
求n的二进制 第一次出现的1对应的值:
368: n = 101110000 -n = 010001111 -n+1 = 010010000 //以补码形式储存 n&-n = 10000 = 16
xxxxxxxxxx11int lowbit(int n) {return n&-n;}
lowbit的应用:
xxxxxxxxxx111//求n的二进制中出现了多少个12int lowbit(int n) { return n & -n; }3
4int main() {5 int n,res = 0;cin >> n;6 while (n) {7 n -= lowbit(n);8 res++;9 }10 cout << res << endl;11}
xxxxxxxxxx71//判断一个数是不是2的倍数2int lowbit(int n) {return n&-n;}3
4int main(){5 if(n == lowbit(n)) "yes"6 else "no"7}
求正整数n的二进制长度
xxxxxxxxxx11int len = log2(n)+1;
求正整数n的二进制第k位代表的十进制数(
xxxxxxxxxx21long long deg(long long num, int deg) { return num & (1LL << deg); }2//25 = {1,0,0,8,16}
状态压缩
用二进制表示状态 例如,a[ ] = {1,2,3,4,5,6,7,8}; 则v[22]表示10110 = a[5]+a[3]+a[2]
xxxxxxxxxx71const int N = 1 << 10;2int a[] = {1,2,3,4,5,6,7,8};3for(int i = 1;i < 1 << 10;i++){4 for(int k = 0;k < 10;k++){5 if(i >> k & 1) v[i]+=a[k];6 }7}
xxxxxxxxxx611//费解的5*5开关:https://www.acwing.com/problem/content/97/2//先固定第一行状态(暴力枚举00000~11111),每一行的暗灯都由下面一行去点亮,此时已经固定了当前状态的最终答案3//再枚举第i=(2,3,4,5)行,诺第a[i-1][j]为0,则需要turn[i][j]开关,统计能否点亮所有灯和turn的次数即可456using namespace std;7const int N = 7;8bool a[N][N],b[N][N];9int di[] = {0,-1,1,0,0},dj[] = {0,0,0,-1,1};10int cnt,ans;11
12void turn(int i,int j){13 for(int w = 0;w < 5;w++){14 int x = i + di[w],y = j + dj[w];15 if(x >= 1 && x <= 5 && y >= 1 && y <= 5){16 if(a[x][y]){cnt--;}17 else {cnt++;}18 a[x][y]^=1;19 }20 }21}22
23void sol(){24 ans = 0x3f3f3f3f,cnt = 0;25 for(int i = 1;i <= 5;i++){26 string s;cin >> s;27 for(int j = 1;j <= 5;j++){28 a[i][j] = s[j-1] - '0';29 if(a[i][j]) cnt++;30 }31 }32 memcpy(b,a,sizeof b);33 int temp = cnt;34
35 for(int q = 0;q < 1 << 5;q++){//q表示第一行状态,从00000~1111136 cnt = temp;//cnt统计亮灯的个数37 int step = 0;//step记录开关操作次数38
39 for(int k = 0;k < 5;k++){40 if(q >> k & 1){ step++;turn(1,k+1); }41 }//用当前状态q固定第一行42
43 for(int i = 2;i <= 5;i++){//枚举第2~5行44 if(step > 6) break;45 for(int j = 1;j <= 5;j++){46 if(a[i-1][j] == 0){ step++; turn(i,j); }//每一行的暗灯都由下面一行去点亮47 if(cnt == 25){ans = min(ans,step);}//更新答案48 }49 }50 memcpy(a,b,sizeof a);//还原数组a51 }52
53 if(ans > 6) cout << -1 << endl;54 else cout << ans << endl;55}56
57int main(){58 int tt; cin >> tt;59 while(tt--) {sol();}60 return 0;61}
位运算
a+b = a|b + a&b = a^b + (a&b)*2
^ 异或
相同为0,不同为1,可以看做不进位加法
0^0 = 01^0 = 10^1 = 11^1 = 0
A ^ A = 0A ^ 0 = Aa ^ b ^ b = a自反性a ^ b ^ c ^ d = b ^ d ^ a ^ c无序性a^b = c可移项为a = b^c,移项时无需改变符号
如果i为偶数,则有
i^(i+1) = 1
比较两数值是否同号,
a^b > 0替换掉a*b > 0
a ^= b ^= a ^= b相当于swap(a,b)
判断成对的数中只出现一次的数
xxxxxxxxxx41//给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现**两次**。找出那个只出现了一次的元素。2int ans = 0;3for(int i = 1;i <= n;i++) {ans ^= a[i];}4cout << ans;
异或前缀和
xxxxxxxxxx71for(int i = 1;i <= n;i++){2 s[i] = s[i-1]^a[i];3}4
5int query(int l,int r){6 return s[r]^s[l-1];7}如果
(n-1)%4 == 0,则0~n-1的异或前缀和为0,S[0,3,7,11,15...] = 0
& 与
判断奇偶
xxxxxxxxxx21int n;cin >> n;2if(n&1) //则为奇数
>> 移位
n >> k 相当于
n << k 相当于
xxxxxxxxxx51//十进制转二进制2int n; cin >> n;//32位3for (int i = 31; i >= 0;i--) {4 cout << (n >> i & (1));5}
离散化
数组下标跨度长,但只用到了部分下标
xxxxxxxxxx71vector<int>arr; //储存所有待离散化的值2sort(arr.begin(),arr.end()); //将所有值排序3arr.erase(unique(arr.begin(),arr.end()),arr.end()); //去掉重复元素4
5int find(int x){6 return lower_bound(arr.begin(),arr.end(),x)-arr.begin()+1;//加1从下标为1开始映射,不加则从0开始7}xxxxxxxxxx91for(int i = 1;i <= n;i++){2 cin >> a[i];3 hs[i] = a[i];4}5sort(hs+1,hs+n+1);6int m = unique(hs+1,hs+n+1) - hs - 1;//a[1~m]即为去重后的元素(离散至1~m(<=n)),不去重也没关系(离散至1~n)7for(int i = 1;i <= n;i++){8 a[i] = lower_bound(hs+1,hs+m+1,a[i]) - hs + 1;//a[i]即为离散后的数值,hs[a[i]]为a[i]原来的值9}
部分涉及区间染色的题目,对于区间[l,r]离散化时,需要将[l-1,r-1]也一起加入离散化数组并去重,否则可能出现以下情况:例如用区间[1,3]和[6,10]覆盖区间[1,10]时,常规离散化会导致[1,10]整个区间被覆盖。例题:[P3740 HAOI2014] 贴海报 - 洛谷 (luogu.com.cn)
搜索
DFS
深度优先搜索,递归实现
在深度优先搜索中,对于最新发现的顶点,如果它还有以此为顶点而未探测到的边,就沿此边继续探测下去,当顶点v的所有边都已被探寻过后,搜索将回溯到发现顶点v有起始点的那些边。这一过程一直进行到已发现从源顶点可达的所有顶点为止。如果还存在未被发现的顶点,则选择其中一个作为源顶点,并重复上述过程。整个过程反复进行,直到所有的顶点都被发现时为止。
括号序列
xxxxxxxxxx211//按字典序输出长度为n的所有合法括号序列 1 <= n <= 202//https://vjudge.net/problem/AtCoder-typical90_b#author=DeepSeek_zh34using namespace std;5int n;6
7void dfs(string s,int l,int r){//当前序列为s,且用了l个左括号的r个右括号8 if(l == n && r == n){9 cout << s << '\n';10 return ;11 }12 if(l < n) dfs(s+'(',l+1,r);13 if(r < l) dfs(s+')',l,r+1);//对于所有位置i而言,其左括号的数量>=右括号的数量14}15
16int main(){17 cin >> n;18 if(n&1) return 0;19 n/=2;20 dfs("",0,0);21}全排列
xxxxxxxxxx181//n选m的全排列,标记数组写法2void dfs(int cnt){//dfs(1)3 if(cnt > m){4 for(int i = 1;i <= m;i++){5 cout << path[i] << ' ';6 }7 cout << '\n';8 return;9 }10 for(int i = 1;i <= n;i++){11 if(!st[i]){12 path[cnt] = i;13 st[i] = 1;14 dfs(cnt+1);15 st[i] = 0;16 }17 }18}xxxxxxxxxx301//n选m的全排列,u表示二进制状态替换标记数组2void dfs(int u,int cnt){//dfs(1,1)3 if(cnt > m){4 for(int i = 1;i <= m;i++){5 cout << path[i] << ' ';6 }7 cout << '\n';8 return;9 }10 for(int i = 1;i <= n;i++){11 if(u>>i&1) continue;12 path[cnt] = i;13 dfs(u|1<<i,cnt+1);14 }15}16
17//n选m的全组合18void dfs(int u,int cnt){//dfs(1,1)19 if(cnt > m){20 for(int i = 1;i <= m;i++){21 cout << path[i] << ' ';22 }23 cout << '\n';24 return;25 }26 for(int i = u;i <= n;i++){27 path[cnt] = i;28 dfs(i+1,cnt+1);29 }30}
xxxxxxxxxx521//n选m的全排列&全组合(1~n)2//(t.path[]可以从任意合法排列开始)345using namespace std;6
7struct PC{//PC t(n,m); 1_idx8 int n,m;9 vector<int>path;10 PC(int n,int m){11 this->n = n;this->m = m;12 path.resize(n+1);13 for(int i = 1;i <= n;i++) path[i] = i;14 }15 16 bool next_comb(){17 int j = m;18 while (j >= 1 && path[j] == n - m + j) j--;19 if (j == 0) return 0;20 path[j]++;21 for (int i = j + 1; i <= m; i++) path[i] = path[i - 1] + 1;22 return 1;23 }24 25 bool next_perm(){26 for (int i = m,used = 0; i >= 1; i--,used = 0) {27 for (int j = 1; j < i; ++j) used |= 1 << path[j];28 for (int x = path[i]+1;x <= n;x++){29 if((used >> x & 1)) continue;30 path[i] = x; used = ~ (used | 1 << x) ^ 1;31 for(int j = i+1;j <= m;j++){32 int w = __builtin_ctz(used);33 path[j] = w;34 used ^= 1 << w;35 }36 return 1;37 }38 }39 return 0;40 }41};42
43int main(){44 int n,m; cin >> n >> m;45 PC t(n,m);//初始化46 do{47 for(int i = 1;i <= t.m;i++){48 cout << t.path[i] << ' ';49 }50 cout << '\n';51 }while(t.next_perm());52}
剪枝
优化搜索顺序
搜索前排序,减小搜索规模、优先搜索分支较少的节点
排除等效冗余
如果沿着几条不同分支到达的子树是等效的,那么只需要对其中一条分支进行搜索
可行性剪枝 (上下界剪枝)
对当前状态进行检查,如果当前分支已经无法到达递归的边界,就进行回溯
最优化剪枝
在最优化问题的搜索过程中,如果当前花费的代价超过了已经搜到的最优解,此时可以停止搜索,直接回溯
记忆化
记录每个状态的搜索结果,在重复遍历一个状态时直接检索并返回
其它优化
使用数据结构、状态压缩、位运算等优化
xxxxxxxxxx411//导弹防御系统 https://www.luogu.com.cn/problem/P104902//dfs+剪枝+全局最小值34using namespace std;5const int N = 55;6int n;7int arr[N];8int up[N],down[N];9int ans;10
11void dfs(int u,int su,int sd){//前u个导弹,已经使用su个up系统,sd个down系统12 if(su + sd >= ans) return;//如果已经超过了当前最优答案,直接剪枝13 if(u == n){ans = su + sd;return;}//所有导弹都考虑过了,并且没有超过当前最优答案,则更新最优答案14
15 //方案一:使用up系统拦截第u个导弹16 int i = 0;17 while(i < su && up[i] >= arr[u]) i++;//贪心找到当前低于u导弹的最高的up系统i (up[]为降序数组)18 int temp = up[i];//temp用于回溯时恢复现场19 up[i] = arr[u];20 if(i >= su)dfs(u+1,su+1,sd);//诺i没找到则新建一个up系统拦截21 else dfs(u+1,su,sd);//否则使用原来的up系统拦截22 up[i] = temp;//恢复现场23
24 //方案二:使用down系统拦截第u个导弹25 i = 0;26 while(i < sd && down[i] <= arr[u]) i++;27 temp = down[i];28 down[i] = arr[u];29 if(i >= sd)dfs(u+1,su,sd+1);30 else dfs(u+1,su,sd);31 down[i] = temp;32}33
34int main(){35 while(cin >> n,n){36 ans = n;37 for(int i = 0;i < n;i++){ cin >> arr[i]; }38 dfs(0,0,0);39 cout << ans <<endl;40 }41}
xxxxxxxxxx871//数独 https://www.luogu.com.cn/problem/P1048123using namespace std;4const int N = 9,M = 1 << N;5int a[N][N];6int mp[M];//预处理每个lowbit对应的数7int ones[M];//预处理每个状态可填的数,优先搜索分支最小的状态8int sx[N],sy[N],sz[N];//状态压缩,表示每行、每列、每块方格还能填的数9
10int get(int x,int y){11 return sx[x]&sy[y]&sz[x/3*3+y/3];12}13
14bool dfs(int cnt){15 if(!cnt) return 1;16 int mn = 10;17 int x,y;18 for(int i = 0;i < N;i++){//优先找分支较少的进行拓展19 for(int j = 0;j < N;j++){20 if(a[i][j] == 0){21 int now = ones[get(i,j)];22 if(now < mn){23 mn = now;24 x = i,y = j;25 }26 }27 }28 }29 for(int i = get(x,y);i;i -= i&-i){30 int k = mp[i&-i];31 a[x][y] = k+1;32 sx[x] ^= 1 << k;33 sy[y] ^= 1 << k;34 sz[x/3*3+y/3] ^= 1 << k;35
36 if(dfs(cnt-1)) return 1;37
38 a[x][y] = 0;39 sx[x] ^= 1 << k;40 sy[y] ^= 1 << k;41 sz[x/3*3+y/3] ^= 1 << k;42 }43 return 0;44}45
46void sol(){47 for(int i = 0;i < N;i++) sx[i] = sy[i] = sz[i] = (1<<N)-1;48 int cnt = 0;49 for(int i = 0;i < N;i++){50 for(int j = 0;j < N;j++){51 if(a[i][j]){52 int k = a[i][j]-1;53 sx[i] ^= 1 << k;54 sy[j] ^= 1 << k;55 sz[i/3*3+j/3] ^= 1 << k;56 }57 else cnt++;58 }59 }60 dfs(cnt);61}62
63int main(){64 for(int i = 0;i < N;i++) mp[1<<i] = i;65 for(int i = 0;i < 1 << N;i++){66 for(int j = i;j;j -= j&-j){67 ones[i]++;68 }69 }70 int mt;cin >> mt;71 while(mt--) {72 for(int i = 0;i < N;i++){73 string s;cin >> s;74 for(int j = 0;j < N;j++){75 char x = s[j];76 if(x == '.') a[i][j] = 0;77 else a[i][j] = x - '0';78 }79 }80 sol();81 for(int i = 0;i < N;i++){82 for(int j = 0;j < N;j++){83 cout << a[i][j];84 }cout << '\n';85 }86 }87}
迭代加深
即每次限制搜索深度的深度优先搜索。
迭代加深搜索的本质还是深度优先搜索,只不过在搜索的同时带上了一个深度 ,当
达到设定的深度时就返回,一般用于找最优解。如果一次搜索没有找到合法的解,就让设定的深度加一,重新从根开始。
当状态随着深度增加比较多时,BFS队列的空间复杂度会很大。当BFS在空间上不够优秀,而且问题要找最优解时,应该考虑迭代加深搜索,迭代加深也可以近似看做BFS。
过程
首先设定一个较小的深度作为全局变量,进行 DFS。每进入一次 DFS,将当前深度加一,当发现
xxxxxxxxxx61IDDFS(u,d)2 if (d > limit) return3 for each edge (u,v){4 IDDFS(v,d+1) 5 }6 return
BFS
广度优先搜索,队列实现,求最短路径
每次都尝试访问同一层的节点。 如果同一层都访问完了,再访问下一层。
xxxxxxxxxx401//走迷宫 https://www.acwing.com/problem/content/description/846/2345using namespace std;6const int N = 110;7int n,m;8int g[N][N];//存图9int d[N][N];//存点到起点的距离10queue<pair<int, int>>q;//队列实现11
12int dfs() {13 memset(d, -1, sizeof d);//初始化为-1表示没有走过14 q.push({ 0,0 });//表示起点走过了15 d[0][0] = 0;16
17 int dx[4] = { -1,1,0,0 }, dy[4] = { 0,0,-1,1 };18 while (q.size()) {//队列不为空19 auto t = q.front();//取队头20 q.pop();21 for (int i = 0; i < 4;i++) {//枚举四个方向,扩展t22 int x = t.first + dx[i], y = t.second + dy[i];23 if (x >= 0 && x < n && y >= 0 && y < m && d[x][y] == -1 && g[x][y] == 0) {24 //在边界内 是空地 且之前没有走过25 d[x][y] = d[t.first][t.second] + 1;//则到起点的距离+126 q.push({ x,y });//新坐标入队27 }28 }29 }30 return d[n - 1][m - 1];31}32int main() {33 cin >> n >> m;34 for (int i = 0; i < n;i++) {35 for (int j = 0; j < m;j++) {36 cin >> g[i][j];37 }38 }39 cout << dfs();40}
双端队列BFS
时间复杂度为严格的O(N+M)
对于一张边权为0或1的无向图。搜索时如果边权为0则将该新节点从队头入队,如果为1则从队尾入队
xxxxxxxxxx511//电车 https://www.acwing.com/problem/content/4252/2345
6const int N = 105,M = N*N;7int n,s,t;8int h[N],e[M],ne[M],w[M],idx;9int dist[N];10bool vis[N];11
12void add(int a,int b,int c){13 w[idx] = c,e[idx] = b,ne[idx] = h[a],h[a] = idx++;14}15
16void uuz(){17 std::memset(dist,0x3f,sizeof dist);18 std::deque<int>dq;19 dq.push_back(s);20 dist[s] = 0;21 while(dq.size()){22 int x = dq.front();23 dq.pop_front();24 if(x == t) return;25 for(int i = h[x];~i;i = ne[i]){26 int y = e[i];27 if(dist[y] > dist[x] + w[i]){28 dist[y] = dist[x] + w[i];29 if(w[i] == 0) { dq.push_front(y); }30 else { dq.push_back(y); }31 }32 }33 }34}35
36int main(){37 std::memset(h,-1,sizeof h);38 std::cin >> n >> s >> t;39 for(int i = 1;i <= n;i++){40 int k;41 std::cin >> k;42 for(int j = 1;j <= k;j++){43 int x;std::cin >> x;44 if(j == 1) add(i,x,0);45 else add(i,x,1);46 }47 }48 uuz();49 if(dist[t] == 0x3f3f3f3f) std::cout << -1;50 else std::cout << dist[t];51}
[P1948 USACO08JAN] Telephone Lines S - 洛谷 (luogu.com.cn)
求出一条路径,使点1到n中第k+1长的边最短,输出这条边的长度。 二分一个答案x,搜索时诺当前w > x则权值看作1,代表由通信公司免费报销。 诺最终需要报销的边数 <= k 则说明当前 x 符合条件。
xxxxxxxxxx521234using namespace std;5const int N = 1003,M = 20004;6int n,m,k;7int h[N],e[M],ne[M],w[M],idx;8int dist[N];9
10void add(int a,int b,int c){11 w[idx] = c,e[idx] = b,ne[idx] = h[a],h[a] = idx++;12}13
14bool check(int x){15 memset(dist,0x3f,sizeof dist);16 dist[1] = 0;17 deque<int>q;18 q.push_back(1);19 while(q.size()){20 int t = q.front();21 q.pop_front();22 for(int i = h[t];~i;i = ne[i]){23 int k = e[i];24 int val = w[i] > x ? 1 : 0;25 if(dist[k] > dist[t] + val){26 dist[k] = dist[t] + val;27 if(val) q.push_back(k);28 else q.push_front(k);29 }30 }31 }32 return dist[n] <= k;33}34
35int main(){36 memset(h,-1,sizeof h);37 cin >> n >> m >> k;38
39 for(int i = 1;i <= m;i++){40 int a,b,c;cin >> a >> b >> c;41 add(a,b,c);add(b,a,c);42 }43
44 int l = 0,r = 1e6;45 while(l < r){46 int mid = l + r >> 1;47 if(check(mid)) r = mid;48 else l = mid + 1;49 }50 if(dist[n] == 0x3f3f3f3f) cout << -1;51 else cout << l;52}
Maze Challenge with Lack of Sleep(★4) - AtCoder typical90_aq - Virtual Judge (vjudge.net)
给定一个N*M的网格,
#代表障碍,.代表路径,求从起点(rs,cs)到终点(rt,ct),最少转向次数。
设定状态(x,y,dir)表示当前位置为(x,y),方向为dir,拓展到下一个节点(nx,ny,i)时,如果dir与i方向一致,则入队首,否则入队尾。
xxxxxxxxxx491234using namespace std;5const int N = 1003;6int n,m,rs,cs,rt,ct;7string s[N];8int dx[] = {-1,0,1,0},dy[] = {0,1,0,-1};9int dist[N][N][4];10int ans = 0x3f3f3f3f;11
12struct node{13 int x,y,dir;14};15
16void uuz(){17 memset(dist,0x3f,sizeof dist);18 deque<node>q;19 for(int i = 0;i < 4;i++){20 q.push_back(node{rs,cs,i});21 dist[rs][cs][i] = 0;22 }23 while(q.size()){24 auto [x,y,dir] = q.front();25 q.pop_front();26
27 if(x == rt && y == ct) ans = min(ans,dist[x][y][dir]);28
29 for(int i = 0;i < 4;i++){30 int nx = x + dx[i],ny = y + dy[i];31 if(nx < 1 || nx > n || ny < 1 || ny > m || s[nx][ny] == '#') continue;32 if(dist[nx][ny][i] > dist[x][y][dir] + (i != dir)){33 dist[nx][ny][i] = dist[x][y][dir] + (i != dir);34 if(i == dir)q.push_front(node{nx,ny,i});35 else q.push_back(node{nx,ny,i});36 }37 }38 }39}40
41int main(){42 cin >> n >> m;43 cin >> rs >> cs >> rt >> ct;44 for(int i = 1;i <= n;i++){45 cin >> s[i];s[i] = ' ' + s[i];46 }47 uuz();48 cout << ans;49}
优先队列BFS
每次从队列中取出当前代价状态最小的进行扩展,当每个状态第一次从队列中被取出时,就得到了起点到该状态的最小状态,之后再被取出时则可直接忽略。
UVA11367 Full Tank? - 洛谷 (luogu.com.cn)
有N个城市M条无向边,每个城市都有一个加油站,加每单位油花费p[i]。 回答q次询问,每次给定油箱容量为c,起始点为st,终点为ed。求st到ed的最小花费?
xxxxxxxxxx6012345using namespace std;6const int N = 1003,M = 20004;7int n,m,q;8int p[N];9int h[N],e[M],ne[M],w[M],idx;10
11struct node{12 int x,oil,cost;13 bool operator < (const node &e2)const{14 return cost > e2.cost;//小根堆,cost小的在堆顶,每次用花费最小的进行拓展,类似dijkstra15 }16};17
18void add(int a,int b,int c){19 w[idx] = c,e[idx] = b,ne[idx] = h[a],h[a] = idx++;20}21
22void query(){23 int c,st,ed;cin >> c >> st >> ed;//dist[x][oil]表示当前位于点x,油量为oil的最小花费24 vector<vector<int>>dist(n+2,vector<int>(c+2,0x3f3f3f3f)),vis(n+2,vector<int>(c+2));25 priority_queue<node>pq;26 pq.push({st,0,0});27 dist[st][0] = 0;28 while(pq.size()){29 auto [x,oil,cost] = pq.top();30 pq.pop();31 if(x == ed){cout << cost << '\n';return;}32 if(oil < c){//诺油量没满,则可以选择原地不动加油33 if(dist[x][oil+1] > dist[x][oil] + p[x]){34 dist[x][oil+1] = dist[x][oil] + p[x];35 pq.push({x,oil+1,dist[x][oil+1]});36 }37 }38 for(int i = h[x];~i;i = ne[i]){39 if(oil < w[i]) continue;//诺oil < w[i],则可以更新其它点40 int y = e[i];41 if(dist[y][oil-w[i]] > dist[x][oil]){42 dist[y][oil-w[i]] = dist[x][oil];43 pq.push({y,oil-w[i],dist[y][oil-w[i]]});44 }45 }46 }47 cout << "impossible\n";48}49
50int main(){51 memset(h,-1,sizeof h);52 cin >> n >> m;53 for(int i = 0;i < n;i++) cin >> p[i];54 for(int i = 1;i <= m;i++){55 int a,b,c;cin >> a >> b >> c;56 add(a,b,c);add(b,a,c);57 }58 cin >> q;59 while(q--) query();60}
双向搜索
诺问题有明确的“初态”和“终态”,可以从两端同时进行BFS或DFS,在中间交汇,组成最终答案。
双向BFS
P10487 Nightmare II - 洛谷 (luogu.com.cn)
给定一个N*M的网格,“M”代表男孩每秒可以走3格,“G”代表女孩每秒可以走1格,“Z”代表鬼每秒拓展2格,“X”代表墙,求在不进入鬼的占领区的前提下,男孩和女孩能否会合,诺能则输出最短会合时间。
从起始状态和目标状态分别开始,两边轮流进行,每次各扩展一整层,当两边各自有一个状态发生重复时,就说明这两个搜索过程相遇了,就可以合并出起点到终点的最少步数。
xxxxxxxxxx711234using namespace std;5const int N = 805;6string s[N];7
8int dx[] = {-1,1,0,0},dy[] = {0,0,-1,1};9
10int sol(){11 int n,m,time = 0;cin >> n >> m;12 vector<pair<int,int>>z;13 vector<vector<int>>vis1(n+5,vector<int>(m+5)),vis2(n+5,vector<int>(m+5));//分别表示男孩和女孩访问过点的状态,诺同时为1则说明相遇了。14 queue<pair<int,int>>q1,q2;15 auto check = [&](int x,int y,int time)->bool{//判断当前点是否合法(未进入鬼区)16 auto dis = [&](int x1,int y1,int x2,int y2){17 return abs(x1-x2) + abs(y1-y2);18 };19 for(int i = 0;i < 2;i++){20 if(2*time >= dis(x,y,z[i].first,z[i].second)) return 0;21 }22 return 1;23 };24 for(int i = 1;i <= n;i++){25 cin >> s[i];s[i] = ' ' + s[i];26 for(int j = 1;j <= m;j++){27 if(s[i][j] == 'M') {q1.push({i,j});vis1[i][j] = 1;}28 if(s[i][j] == 'G') {q2.push({i,j});vis2[i][j] = 1;}29 if(s[i][j] == 'Z') z.push_back({i,j});30 }31 }32 while(q1.size() || q2.size()){33 time++;34 for(int t = 1;t <= 3;t++){//q1每次拓展3层35 int siz = q1.size();36 while(siz--){37 auto [x,y] = q1.front();38 q1.pop();39 if(!check(x,y,time)) continue;40 for(int i = 0;i < 4;i++){41 int nx = x + dx[i],ny = y + dy[i];42 if(nx < 1 || nx > n || ny < 1 || ny > m || s[nx][ny] == 'X' || vis1[nx][ny]) continue;43 if(vis2[nx][ny]) return time;44 vis1[nx][ny] = 1;45 q1.push({nx,ny});46 }47 }48 }49 for(int t = 1;t <= 1;t++){//q2每次拓展一层50 int siz = q2.size();51 while(siz--){52 auto [x,y] = q2.front();53 q2.pop();54 if(!check(x,y,time)) continue;55 for(int i = 0;i < 4;i++){56 int nx = x + dx[i],ny = y + dy[i];57 if(nx < 1 || nx > n || ny < 1 || ny > m || s[nx][ny] == 'X' || vis2[nx][ny]) continue;58 if(vis1[nx][ny])return time;59 vis2[nx][ny] = 1;60 q2.push({nx,ny});61 }62 }63 }64 }65 return -1;66}67
68int main(){69 int t;cin >> t;70 while(t--) cout << sol() << '\n';71}
双向DFS
P10484 送礼物 - 洛谷 (luogu.com.cn)
给定N个物品,每个物品体积为a[i],和一个容量为M背包,请问最多能装多少体积的物品? 其中
本题使用背包求解的话体积过大,指数型枚举的话复杂度过高
我们可以把礼物分成两半,对每一半分别搜索,将其能达到的体积分别存放于两个数组A,B中,对B数组进行排序,然后遍历A数组中的每一个元素x1,在B数组中二分找到x2,使得x1+x2 <= m,用二者之和更新答案。时间复杂度为
xxxxxxxxxx4012345using namespace std;6int n,m;7
8void dfs(vector<int>&v,vector<long long>&w,int id,long long sum){9 if(sum > m) return;10 if(id == v.size()){11 w.emplace_back(sum);12 return;13 }14 dfs(v,w,id+1,sum+v[id]);15 dfs(v,w,id+1,sum);16}17
18int main(){19 cin >> m >> n;20 vector<int>v1,v2;21 vector<long long>w1,w2;22 int mid = (n+1)/2;23 for(int i = 1;i <= mid;i++){24 int x;cin >> x;25 v1.emplace_back(x);26 }27 for(int i = mid+1;i <= n;i++){28 int x;cin >> x;29 v2.emplace_back(x);30 }31 dfs(v1,w1,0,0);32 dfs(v2,w2,0,0);33 sort(w2.begin(),w2.end());34 long long ans = 0;35 for(auto x1:w1){36 auto x2 = --upper_bound(w2.begin(),w2.end(),m-x1);37 ans = max(ans,x1+*x2);38 }39 cout << ans;40}
A*
为了提高搜索效率,我们设计一个“估价函数”,将“当前代价+未来估价”的最小状态作为堆顶进行拓展。 为了保证第一次从堆中取出目标状态时得到的就是最优解,我们设计的估价函数f(state)不能大于未来的实际代价g(state)。 这种带有估价函数的优先队列BFS就称为A*算法。
给定N个点,M条边的有向图,求从起点st到终点ed的第k短路(允许经过重复点或边)。
在优先队列BFS中,当节点x第k次被取出时,当前代价即为第k小代价。考虑A*优化。
我们以点x到终点ed的最短距离作为估价函数f(x),建反图以ed为起点求单源最短路即可求得f(x)。
建立二叉堆,存储二元组
(x,dist + f(x)),x表示当前节点,dist代表当前实际代价,f(x)表示预估代价。最初堆中只有(st,0+f(st))从堆顶取出
dist + f(x)的最小二元组进行拓展,如果节点y被取出的次数尚未达到k次,就把新的二元组(y,(dist+w[i])+f(y))插入堆中。重复3操作,直到终点ed被取出了k次,此时的dist即为从st到ed的第k短路。
A*算法的时间复杂度上界仍与优先队列BFS相同
xxxxxxxxxx841234using namespace std;5const int N = 1003,M = 10004;6int n,m,st,ed,k;7int h[N],e[M],ne[M],w[M],idx;8int f[N];9int vis[N];10
11struct edge{12 int a,b,c;13}in[M];14
15void add(int a,int b,int c){16 w[idx] = c,e[idx] = b,ne[idx] = h[a],h[a] = idx++;17}18
19void dijkstra(){20 memset(f,0x3f,sizeof f);21 priority_queue<pair<int,int>,vector<pair<int,int>>,greater<pair<int,int>>>pq;22 pq.push({0,ed});23 f[ed] = 0;24 while(pq.size()){25 int x = pq.top().second;26 pq.pop();27 if(vis[x]) continue;28 else vis[x] = 1;29 for(int i = h[x];~i;i = ne[i]){30 int y = e[i];31 if(f[y] > f[x] + w[i]){32 f[y] = f[x] + w[i];33 pq.push({f[y],y});34 }35 }36 }37}38
39struct node{40 int x,dist;41 bool operator < (const node &e2)const{42 return dist + f[x] > e2.dist + f[e2.x];43 }44};45
46int astar(){47 memset(vis,0,sizeof vis);48 priority_queue<node>pq;49 pq.push({st,0});50 while(pq.size()){51 auto [x,dist] = pq.top();52 pq.pop();53 vis[x]++;54 if(vis[ed] == k) return dist;55
56 for(int i = h[x];~i;i = ne[i]){57 int y = e[i];58 if(vis[y] <= k) pq.push({y,dist+w[i]});59 }60 }61 return -1;62}63
64int main(){65 memset(h,-1,sizeof h);66 cin >> n >> m;67 for(int i = 1;i <= m;i++){68 auto &[a,b,c] = in[i]; cin >> a >> b >> c;69 add(b,a,c);70 }71 cin >> st >> ed >> k;72 if(st == ed) k++;73
74 dijkstra();75
76 idx = 0;77 memset(h,-1,sizeof h);78 for(int i = 1;i <= m;i++){79 auto [a,b,c] = in[i];80 add(a,b,c);81 }82
83 cout << astar();84}
IDA*
IDA* 为采用了迭代加深算法的 A* 算法。
Dancing Links
时间复杂度与矩阵中1的个数有关,与矩阵r,c等参数无关,时间复杂度是指数级的
精确覆盖
P4929 【模板】舞蹈链(DLX) - 洛谷 (luogu.com.cn)
给定N行M列的矩阵,每个元素要么是1,要么是0。请从中挑选诺干行,使得每列有且仅有一行有1。输出任意一个方案即可,顺序随意。
,保证1的个数不超过5000。
xxxxxxxxxx163123
4std::vector<int>res;5
6struct DLX{//1_idx7 int n,m,idx;8 std::vector<int>first,siz,stk;//siz[c]存第c列的元素个数,stk存答案,first[r]指向第r行的第一个元素9 std::vector<int>L,R,U,D;//十字循环链表10 std::vector<int>col,row;//存放第idx个元素的列左边和行坐标11
12 int ans;13
14 void init(const int &r,const int &c){15 ans = 1e9;16 n = r,m = c;17 first.resize(n+1),siz.resize(m+1);18 L.resize(m+1),R.resize(m+1),U.resize(m+1),D.resize(m+1);19 col.resize(m+1),row.resize(m+1);20 for(int i = 0;i <= m;i++){21 L[i] = i-1,R[i] = i+1;22 U[i] = D[i] = i;23 }24 L[0] = m,R[m] = 0;25 idx = m+1;26 }27
28 void remove(const int &c){//删除第c列,以及其它与它相关的行29 L[R[c]] = L[c],R[L[c]] = R[c];30 for(int i = D[c];i != c;i = D[i]){31 for(int j = R[i];j != i;j = R[j]){32 U[D[j]] = U[j];33 D[U[j]] = D[j];34 siz[col[j]]--;35 }36 }37 }38
39 void recover(const int &c){//还原第c列,以及其它与它相关的行,即remove的逆操作40 for(int i = U[c];i != c;i = U[i]){41 for(int j = L[i];j != i;j = L[j]){42 U[D[j]] = D[U[j]] = j;43 siz[col[j]]++;44 }45 }46 L[R[c]] = R[L[c]] = c;47 }48
49 bool dance(){//精确覆盖50 if(stk.size() >= ans) return 0;//剪枝:如果当前已经超过最优解,直接返回51 if(!R[0]){//如果0号节点没有右节点,那么矩阵为空,记录答案并返回52 ans = stk.size(); //当前stk[]即为一组解53 res = stk;54 return 1;55 }56 int c = R[0];57 for(int i = R[0];i != 0;i = R[i]){58 if(siz[i] < siz[c]) c = i;59 }60 remove(c);//优先选择元素个数最少得一列c,并删除这一列61 for(int i = D[c];i != c;i = D[i]){//遍历这一列其它所有有1的行,递归枚举是否选择它62 stk.push_back(row[i]);63 for(int j = R[i];j != i;j = R[j]) remove(col[j]);64 if(dance()) return 1; //任意解65// dance();//最小解66 for(int j = L[i];j != i;j = L[j]) recover(col[j]);67 stk.pop_back();68 }69 recover(c);70 return 0;71 }72 73 void remove2(const int &c){//重复覆盖问题中只需要删除当前列74 for(int i = D[c];i != c;i = D[i]){ 75 L[R[i]] = L[i]; R[L[i]] = R[i]; 76 }77 }78
79 void recover2(const int &c){80 for(int i = U[c];i != c;i = U[i]){ 81 L[R[i]] = R[L[i]] = i; 82 }83 }84
85 int f(){//估价函数86 int res = 0;87 std::vector<int>vis(m+1);88 for(int i = R[0];i != 0;i = R[i]){89 if(vis[i]) continue;90 vis[i] = 1;91 res++;92 for(int j = D[i];j != i;j = D[j]){93 for(int k = R[j];k != j;k = R[k]){94 vis[col[k]] = 1;95 }96 }97 }98 return res;99 }100
101 bool dance2(){//重复覆盖102 if(stk.size() + f() >= ans) return 0;103 if(!R[0]) {104 ans = stk.size();105 return 1;106 }107 int c = R[0];108 for(int i = R[0];i != 0;i = R[i]){109 if(siz[i] < siz[c]) c = i;110 }111 for(int i = D[c];i != c;i = D[i]){112 for(int j = R[i];j != i;j = R[j]) remove2(j);113 remove2(i);114 stk.push_back(row[i]);115// if(dance2()) return 1; 任意解116 dance2();//最小解117 stk.pop_back();118 recover2(i);119 for(int j = L[i];j != i;j = L[j]) recover2(j);120 }121 return 0;122 }123
124 void insert(const int &r,const int &c){//在第r行,第c列插入一个节点125 col.push_back(c),row.push_back(r);126 siz[c]++;127 U.push_back(c),D.push_back(D[c]);128 U[D[c]] = idx; D[c] = idx;129 if(first[r] == 0){//如果第r行没有元素,那么直接插入一个元素,并使first[r]指向该元素130 first[r] = idx;131 L.push_back(idx),R.push_back(idx);132 }133 else{//否则把idx插入到c的正下方,把idx插入到first(r)的正右方134 R.push_back(R[first[r]]);135 L[R[first[r]]] = idx;136 L.push_back(first[r]);137 R[first[r]] = idx;138 }139 idx++;140 }141};142
143
144int main() {145 int n,m; std::cin >> n >> m;146 DLX dlx;147 dlx.init(n,m);148 for(int i = 1;i <= n;i++){149 for(int j = 1;j <= m;j++){150 int x;std::cin >> x;151 if(x) dlx.insert(i,j);152 }153 }154 dlx.dance();155 if(dlx.ans == 1e9) {156 std::cout << "No Solution!";157 }158 else{159 for(auto x:res){160 std::cout << x << ' ';161 }162 }163}
求解大小为w*w的数独
xxxxxxxxxx34123
4const int N = 10;5int w = 9;6int sw = 3;//sqrt(w)7int a[N][N];8
9int main(){10 DLX dlx;11 dlx.init(w*w*w,w*w*4);12 for(int i = 0;i < w;i++){13 for(int j = 0;j < w;j++){14 std::cin >> a[i][j];15 for(int k = 1;k <= w;k++){16 if(a[i][j] != 0 && a[i][j] != k) continue;17 int id = i*w*w + j*w + k;18 dlx.insert(id,i*w + j + 1);19 dlx.insert(id,i*w + w*w + k);20 dlx.insert(id,j*w + w*w*2 + k);21 dlx.insert(id,w*w*3 + (i/sw*sw + j/sw)*w + k);22 }23 }24 }25
26 dlx.dance();27
28 for(int i = 0;i < w;i++){29 for(int j = 0;j < w;j++){30 std::cout << a[i][j] << ' ';31 }32 std::cout << '\n';33 }34}
重复覆盖
remove、recover、dance操作略有不同。 调用dlx.dance2()即可。
因为重复覆盖问题方案一般比精确覆盖多,搜索基于IDA*优化,加入了估价函数f()。
倍增
倍增法(英语:binary lifting),顾名思义就是翻倍。它能够使线性的处理转化为对数级的处理,大大地优化时间复杂度。
这个方法在很多算法中均有应用,其中最常用的是 RMQ 问题和求 LCA(最近公共祖先)。
ST表
ST表(Sparse Table,稀疏表)基于倍增思想,用于解决[可重复贡献问题][区间重合区域不影响结果,如「RMQ 」、「区间按位与」、「区间按位或」、「区间 GCD」]的数据结构,不支持修改
O(
令
对于每个询问
xxxxxxxxxx741//https://www.luogu.com.cn/problem/P288023using namespace std;4
5namespace S_T{ //1_idx6 const int N = 1000006;7 int lg2[N];8
9 struct info{10 long long mx,mn; //mn,gcd,and,or;11
12 info(){}13 info(const int &x) { //init_info14 mx = mn = x;15 }16
17 info friend operator + (const info &e1,const info &e2) { //updaet_info18 info ans;19 ans.mx = std::max(e1.mx,e2.mx);20 ans.mn = std::min(e1.mn,e2.mn);21 return ans;22 }23 };24
25
26 void init_lg2(){ //下取整27 lg2[0] = -1;28 for(int i = 1;i < N;i++){29 lg2[i] = lg2[i>>1]+1;30 }31 }32
33 template<typename T>34 struct ST{35 int n,m;36 std::vector<std::vector<info>>st;37
38 ST(){}39 ST(const T &v) {40 if(~lg2[0]) init_lg2();41 n = v.size()-1,m = lg2[v.size()];42 st = std::vector<std::vector<info>>(n+1,std::vector<info>(m+1));43
44 for(int i = 1;i < v.size();i++){ st[i][0] = v[i]; }45 for(int j = 1;(1<<j) < v.size();j++){46 int pj = 1 << (j-1);47 for(int i = 1;i+(1<<j)-1 < v.size();i++){48 st[i][j] = st[i][j-1] + st[i+pj][j-1];49 }50 }51 }52
53 info query(int l,int r) {54 int x = lg2[r-l+1];55 info ans = st[l][x] + st[r-(1<<x)+1][x];56 return ans;57 }58 };59}60using S_T::ST,S_T::info;61
62int main() {63 int n,q; std::cin >> n >> q;64 vector<int> a(n+1);65 for(int i = 1;i <= n;i++){66 std::cin >> a[i];67 }68 ST t(a);69 while(q--){70 int l,r; std::cin >> l >> r;71 auto ans = t.query(l,r);72 std::cout << ans.mx - ans.mn << '\n';73 }74}
根号分治
根号分治,是一种对数据进行点分治的分治方式,它的作用是优化暴力算法,类似与分块,但应用范围比分块更广。
具体来说,对于所进行的操作,按照某个点B划分,分为大于B以及小于B两个部分,两部分使用不同的方式处理。(一般以根号为分界
Colorful Graph(★6) - AtCoder typical90_ce - Virtual Judge (vjudge.net)
给定N个点M条边的无向图,初始时每个点颜色为1。再给定Q次查询,每次查询给定两个整数{x,c},输出当前点x的颜色,然后将点x及其所有相邻的点颜色改为c。
ans[x]={time,color}表示当前点最后被更新时的时间戳和颜色,flag[x]={time,color}flag状态标记,表示当前节点在time时更新应周围节点为color。
以度数是否大于
xxxxxxxxxx3712
3int main(){4 int n,m;std::cin >> n >> m;5 int sn = sqrt(m<<1);6
7 std::vector<std::vector<int>>e(n+1);8 std::vector<int>du(n+1);9 for(int i = 1;i <= m;i++){10 int x,y;std::cin >> x >> y;11 e[x].emplace_back(y);12 e[y].emplace_back(x);13 du[x]++; du[y]++;14 }15
16 for(int i = 1;i <= n;i++){17 std::sort(e[i].begin(),e[i].end(),[&](int x,int y){return du[x] > du[y];});18 }19
20 std::vector<std::pair<int,int>>ans(n+1,{0,1}),flag(n+1,{0,1});21
22 int q;std::cin >> q;23 for(int i = 1;i <= q;i++){24 int x,c;std::cin >> x >> c;25 if(du[x] <= sn){26 for(auto& y:e[x]){27 ans[x] = std::max(ans[x],flag[y]);28 }29 }30 std::cout << ans[x].second << '\n';31 ans[x] = flag[x] = {i,c};32 for(auto& y:e[x]){33 if(du[y] <= sn) break;34 ans[y] = flag[x];35 }36 }37}
数据结构
基本数据结构
链表
单链表
xxxxxxxxxx111const int N = 100010;2//head 表示头节点的下标3//e[i] 表示节点i的值4//ne[i] 表示节点i的next指针是多少5//idx 储存当前已经用到了哪个点6int head, e[N], ne[N], idx; 7//初始化8void init() {9 head = -1;10 idx = 0;11}xxxxxxxxxx71//将x插到头节点2void add_to_head(int x) {3 e[idx] = x;4 ne[idx] = head; //x指向开头5 head = idx; //x的位置变为新的开头6 idx++;7}xxxxxxxxxx71//将x插到下标是k的点的后面(注意下标从0开始)2void add(int k, int x) {3 e[idx] = x;4 ne[idx] = ne[k]; //x指向k的后一位5 ne[k] = idx; //k再指向x6 idx++;7}xxxxxxxxxx41//将下标是k的点的后面一个点删掉2void remove(int k) {3 ne[k] = ne[ne[k]];//原来指向k的后位改为指向k后位的后位4}xxxxxxxxxx41//遍历链表2for(int i = head;i != -1;i = ne[i]){3 cout << e[i] << " ";4}
xxxxxxxxxx461//https://www.acwing.com/problem/content/828/23using namespace std;4const int N = 100005;5int e[N], ne[N], idx,h = -1;6
7void add_to_head(int x) {8 e[idx] = x;9 ne[idx] = h;10 h = idx++;11}12
13void add(int k, int x) {14 e[idx] = x;15 ne[idx] = ne[k];16 ne[k] = idx++;17}18
19void del(int k) {20 ne[k] = ne[ne[k]];21}22
23int main() {24
25 int t; cin >> t;26 for (int i = 1; i <= t;i++) {27 char op; cin >> op;28 if (op == 'I') {29 int k, x; cin >> k >> x;30 add(k - 1, x);//第k个数,下标为k-131 }32 if (op == 'H') {33 int x; cin >> x;34 add_to_head(x);35 }36 if (op == 'D') {37 int k; cin >> k;38 if (k == 0) h = ne[h];//k == 0 表示删除头结点39 else del(k-1);40 }41 }42
43 for (int i = h; i != -1; i = ne[i]) cout << e[i] << " ";44 45 return 0;46}
双链表
xxxxxxxxxx91const int N = 100010;2int e[N], l[N], r[N], idx;3
4//初始化5void init() {6 //0表示左端点,1表示右端点7 r[0] = 1, l[1] = 0;8 idx = 2; //从2开始9}xxxxxxxxxx81//在下标为k的右边插入值x2void addr(int k, int x) {3 e[idx] = x; 4 r[idx] = r[k]; //x的右边指向k的下一位5 l[idx] = k; //x的左边指向k6 l[r[k]] = idx; //k的下一位的左边指向x (注意与下一条语句顺序不能写反)7 r[k] = idx; //k的右边指向x8}xxxxxxxxxx41//在下标为k的左边插入值x2void addl(int k, int x) {3 addr(l[k], x); //相当于在k左边一个数的右边插入x4}xxxxxxxxxx51//删除下标为k的点2void remove(int k) {3 r[l[k]] = r[k]; //让原来右边指向k的指针 指向k的右边4 l[r[k]] = l[k]; //让原来左边指向k的指针 指向k的左边5}
问题转化为差分数组
xxxxxxxxxx54123using ll = long long;4using namespace std;5const ll inf = 1e18;6const int N = 100005;7int n,k;8ll a[N];9ll ne[N],la[N],s[N];//双链表10bool del[N];//标记当前节点是否已经删除11ll ans;12
13struct Edge{14 ll id,x;15 bool operator < (const Edge &e) const {16 return x > e.x;//优先队列小根堆17 }18};19
20int main(){21 cin >> n >> k;22 priority_queue<Edge>pq;23 for(int i = 1;i <= n;i++){24 cin >> a[i];25 if(i > 1) s[i-1] = a[i] - a[i-1];26 }27
28 s[0] = s[n] = inf;//左右两边设置哨兵29 for(int i = 1;i < n;i++){30 pq.push({i,s[i]});31 ne[i] = i+1;32 la[i] = i-1;33 }34 35 while(k--){36 while(del[pq.top().id]) pq.pop();//诺该点已经被删除,则直接弹出37 auto [id,x] = pq.top();38 pq.pop();39 ans += x;//选择当前节点40
41 //删除当前节点和左右两边的节点,在当前节点位置新建一个节点42 //诺未来选了该新节点,则相当于不选当前节点,选择当前节点左右两边的节点43 s[id] = s[la[id]] + s[ne[id]] - s[id];44 Edge e = {id,s[id]};45 pq.push(e);46
47 del[ne[id]] = del[la[id]] = 1;//删除左右两边的节点48 la[id] = la[la[id]];49 ne[id] = ne[ne[id]];50 ne[la[id]] = id;51 la[ne[id]] = id;52 }53 cout << ans;54}
栈
先进后出
xxxxxxxxxx1812using namespace std;3const int N = 100010;4//tt表示栈尾5int stk[N], tt;6
7//插入8stk[++tt] = x;9
10//弹出11tt--;12
13//判断栈是否为空14if (tt > 0) not empty15else empty16
17//栈顶18stk[tt];xxxxxxxxxx221//带最小值的栈2//新建一个栈B,每次A入栈x时,B入栈min(B.top(),x);3stack<int>A,B;4
5void push(int x){6 A.push(x);7 if(B.empty()) B.push(x);8 else B.push(min(x,B.top()));9}10
11void pop(){12 A.pop();13 B.pop();14}15
16int top(){17 return A.top();18}19
20int getmin(){21 return B.top();22}
xxxxxxxxxx371//火车进站方案 https://www.acwing.com/problem/content/131/2345using namespace std;6const int N = 30;7int n,rest;8vector<int>p;//p存已经出站的序列9stack<int>st;//栈10
11void dfs(int u){12 if(rest >= 20) return;13 if(p.size() == n){//边界条件,如果全部出站则输出序列14 rest++;15 for(auto &x:p) cout << x;16 cout << endl;17 return;18 }19 if(st.size()){//情况一:栈中元素出站20 p.push_back(st.top());21 st.pop();22 dfs(u);23 st.push(p.back());//还原现场24 p.pop_back();25 }26 if(u <= n){//情况二:火车入栈27 st.push(u);28 dfs(u+1);29 st.pop();//还原现场30 }31}32
33int main(){34 cin >> n;35 dfs(1);36 return 0;37}
表达式计算
中缀表达式:A op B 前缀表达式:op A B (波兰式) 后缀表达式:A B op (逆波兰式)
后缀表达式求值
后缀表达式对于计算机来讲容易实现
建立一个用于存储数的栈,逐一扫描该后缀表达式中的元素
如果遇到一个数,则把该数入栈。
如果遇到运算符,则取出栈顶两个数进行计算,把结果入栈。
扫描完成后,栈中恰好剩下一个数,即为该后缀表达式的答案
xxxxxxxxxx51if(c[k]>='0' && c[k]<='9') q.push(c[k]-'0'); //假设表达式都是1位数2if(c[k]=='-') i=q.top(),q.pop(),j=q.top(),q.pop(), q.push(j-i);3if(c[k]=='+') i=q.top(),q.pop(),j=q.top(),q.pop(), q.push(j+i);4if(c[k]=='*') i=q.top(),q.pop(),j=q.top(),q.pop(), q.push(j*i);5if(c[k]=='/') i=q.top(),q.pop(),j=q.top(),q.pop(), q.push(j/i);
单调栈
xxxxxxxxxx231//输出每个数左边第一个比它小的数,如果不存在则输出-1;234using namespace std;5const int N = 100005;6int n,a[N];7stack<int>sk;8int ans[N];9
10int main(){11 cin >> n;12 for(int i = 1;i <= n;i++) { cin >> a[i]; }13 14 for(int i = 1;i <= n;i++){15 while(sk.size() && sk.top() >= a[i]) sk.pop();16 if(sk.size()) ans[i] = sk.top();17 else ans[i] = -1;18
19 sk.push(a[i]);20 }21
22 for(int i = 1;i <= n;i++){ cout << ans[i] << ' '; }23}
xxxxxxxxxx401//https://www.acwing.com/problem/content/133/234using namespace std;5using ll = long long;6const int N = 100005;7ll a[N];8ll l[N],r[N];//l[i]记录左边最后一个大于等于本身的下标,r[i]记录右边最后一个大于等于本身的下标9//对于每一个高度a[i],能组成的最大面积为a[i]*(r[i]-l[i]+1)10
11int main(){12 int n;13 while(cin >> n,n){14 a[0] = a[n+1] = -1;//在边界设置哨兵15 for(int i = 1;i <= n;i++) cin >> a[i];16
17 stack<ll>sk;18 for(int i = 0;i <= n+1;i++){19 while(sk.size() && a[sk.top()] >= a[i]) sk.pop();20 if(sk.size()) l[i] = sk.top()+1;21 else l[i] = i;22 sk.push(i);23 }24
25 sk = stack<ll>();26 for(int i = n+1;i >= 0;i--){27 while(sk.size() && a[sk.top()] >= a[i]) sk.pop();28 if(sk.size()) r[i] = sk.top()-1;29 else r[i] = i;30 sk.push(i);31 }32
33 ll ans = 0;34 for(int i = 1;i <= n;i++){35 ans = max(ans,a[i]*(r[i] - l[i] + 1));36 }37 cout << ans << endl;38 }39 return 0;40}
队列
| ql | ← | ← | ← | ← | qr |
|---|---|---|---|---|---|
| hh | tt |
先进先出
xxxxxxxxxx161//双端队列2//hh表示队首front,tt表示队尾back3int q[N], hh = 0, tt = -1;4
5//入队(队尾)6q[++tt] = x;7
8//弹出(队首)9hh++;10
11//判断队列是否非空12if (hh <= tt) return 1;13
14
15q[hh]//队首元素16q[tt]//队尾元素
单调队列
滑动窗口
| 窗口位置 | 最小值 | 最大值 |
|---|---|---|
| [1 3 -1] -3 5 3 6 7 | -1 | 3 |
| 1 [3 -1 -3] 5 3 6 7 | -3 | 3 |
| 1 3 [-1 -3 5] 3 6 7 | -3 | 5 |
| 1 3 -1 [-3 5 3] 6 7 | -3 | 5 |
| 1 3 -1 -3 [5 3 6] 7 | 3 | 6 |
| 1 3 -1 -3 5 [3 6 7] | 3 | 7 |
此处的 "队列" 跟普通队列的一大不同就在于可以从队尾进行操作,STL 中有类似的数据结构 deque
xxxxxxxxxx281//滑动窗口之最值23using namespace std;4const int N = 1000010;5int a[N], q[N]; //a[N]为数组,q[N]队列存下标6int main() {7 int n, k;8 cin >> n >> k;9 for (int i = 1; i <= n; i++) scanf("%d", &a[i]);10
11 int hh = 0, tt = -1;12 for (int i = 1;i <= n;i++){//hh<=tt判断队列是否为空,相当于!q.empty()或q.size()13 if (hh <= tt && i-k >= q[hh]) hh++;//i-k>=q[hh]窗口已经形成,则每次循环队首右移14 while (hh <= tt && a[q[tt]] > a[i]) tt--;//求最小,把队列中>a[i]的数都弹出15 q[++tt] = i;16 if (i >= k) printf("%d ", a[q[hh]]);//队首即为最小值17 }18 puts("");19
20 hh = 0, tt = -1;21 for (int i = 1; i <= n; i++){22 if (hh <= tt && i-k >= q[hh]) hh++;23 while (hh <= tt && a[q[tt]] < a[i]) tt--;//求最大类似24 q[++tt] = i;25 if (i >= k) printf("%d ", a[q[hh]]);26 }27 return 0;28}xxxxxxxxxx281//STL的deque实现,比数组模拟要慢234using namespace std;5const int N = 1000006;6int a[N];7
8int main(){9 int n,k;cin >> n >> k;10 for(int i = 1;i <= n;i++) {cin >> a[i];}11
12 deque<int>dq;13 for(int i = 1;i <= n;i++){14 if(dq.size() && i - k >= dq.front()) dq.pop_front();15 while(dq.size() && a[dq.back()] >= a[i]) dq.pop_back();16 dq.push_back(i);17 if(i >= k) cout << a[dq.front()] << ' ';18 }19 cout << '\n';20
21 dq.clear();22 for(int i = 1;i <= n;i++){23 if(dq.size() && i - k >= dq.front()) dq.pop_front();24 while(dq.size() && a[dq.back()] <= a[i]) dq.pop_back();25 dq.push_back(i);26 if(i >= k) cout << a[dq.front()] << ' ';27 }28}
最大子段和
给定一个长度为 n 的整数序列,从中找出一段长度不超过 m 的连续子序列,使得子序列中所有数的和最大。
最优策略是,下标递增,对应前缀和也递增
xxxxxxxxxx25123using namespace std;4const int N = 300005;5int n,m,a[N];6int q[N],hh,tt;//tt = -1;q[++tt] = 0;7
8int main(){9 cin >> n >> m;10 for(int i = 1;i <= n;i++){11 cin >> a[i];12 a[i] += a[i-1];13 }14
15 int ans = 0x80000000; //初始化为负无穷16 for(int i = 1;i <= n;i++){17 ans = max(ans,a[i]-a[q[hh]]);18 if(hh <= tt && i - q[hh] >= m) hh++;19 while(hh <= tt && a[q[tt]] >= a[i]) tt--;//单调递增队列,队首a[q[hh]]为最小值20 q[++tt] = i;21 }22 cout << ans;23
24 return 0;25}
堆
堆是一棵树,其每个节点都有一个键值,且每个节点的键值都大于等于/小于等于其父亲的键值。
堆主要支持的操作有:插入一个数、查询最小值、删除最小值、合并两个堆、减小一个元素的值。
一些功能强大的堆(可并堆)还能(高效地)支持 merge 等操作。
一些功能更强大的堆还支持可持久化,也就是对任意历史版本进行查询或者操作,产生新的版本。
二叉堆
//下标从1开始比较方便:第k个数,左儿子为2k,右儿子为2k+1
| 操作 | 实现 |
|---|---|
| 1.插入一个数 | heap[++size] = x; up(size); |
| 2.求集合当中最小值 | heap[1]; |
| 3.[删除最小值][将第一个数等于最后一个数,删掉最后一个数,再下沉第一个数] | heap[1] = heap[size]; size--; down[1]; |
| 4.[删除任意一个元素][将第k个数等于最后一个数,删掉最后一个数,再上下这个数] | heap[k] = heap[size]; size--; down[k]; up[k]; |
| 5.修改任意一个元素 | heap[k] = x; down[k]; up[k]; |
xxxxxxxxxx101int h[100010],cnt;2void down(int u) {3 int t = u;4 if (u * 2 <= cnt && h[u * 2] < h[t]) t = u * 2;5 if (u * 2 + 1 <= cnt && h[u * 2 + 1] < h[t]) t = u * 2 + 1;//令t为u及其子节点中的最小节点6 if (u != t) {//判断u是不是最小节点,不是则与最小节点t交换,继续下沉(递归处理)7 swap(h[u], h[t]);8 down(t);9 }10}xxxxxxxxxx61void up(int u) {2 if (u / 2 && h[u / 2] > h[u]) {//诺比父节点小,则二者交换3 swap(h[u / 2], h[u]);4 u /= 2;5 }6}
给定 m 个序列,每个包含 n 个非负整数。 现在我们可以从每个序列中选择一个数字以形成具有 m 个整数的序列。 很明显,我们一共可以得到 n^m 个这种序列,然后我们可以计算每个序列中的数字之和,并得到 n^m 个值。 求出这些序列和之中最小的 n 个值。
思路:第一个数组a[]依次与其他数组合并,每次将a[]更新最小的n个数
xxxxxxxxxx521234using namespace std;5using pii = pair<int,pair<int,int>>;6const int N = 2003;7int n,m;8int a[N],b[N],c[N];9
10struct Edge{11 int s,x,y;//s为a[x]+b[y]的值12 bool operator < (const Edge &e)const {13 return s > e.s;//运算符重载,小根堆14 }15};16
17void merge(){//数组a[]与数组b[]合并18 priority_queue<Edge>pq;//a[]为有序数组,a[x],b[y]为n*n的组合,y一定时a[x]+b[y]有序19 for(int i = 1;i <= n;i++){20 pq.push({a[1]+b[i],1,i});21 }22 vector<int>ans;23 for(int i = 1;i <= n;i++){24 auto [s,x,y] = pq.top();//a[x]+b[y]出队,a[x+1]+b[y]入队25 pq.pop();26 c[i] = s;27 x++;28 pq.push({a[x]+b[y],x,y});29 }30 for(int i = 1;i <= n;i++){31 a[i] = c[i];32 }33}34
35void sol(){36 cin >> m >> n;37 for(int i = 1;i <= n;i++){ cin >> a[i]; }38 sort(a+1,a+n+1);39 40 m--;41 while(m--){42 for(int i = 1;i <= n;i++){ cin >> b[i]; }43 merge();44 }45 for(int i = 1;i <= n;i++){ cout << a[i] << ' '; }46 cout << endl;47}48
49int main(){50 int T;cin >> T;51 while(T--){ sol(); }52}
左偏树
左偏树(leftist tree)是一种可并堆,具有堆的性质,可以快速合并,支持可持久化。
| 插入 | 查询最小值 | 删除最小值 | 合并 | |
|---|---|---|---|---|
| O(logN) | O(1) | O(logN) | O(logN) |
配对堆
配对堆是一个支持插入,查询/删除最小值,合并,修改元素等操作的数据结构,是一种可并堆。有速度快和结构简单的优势,但由于其为基于势能分析的均摊复杂度,不支持可持久化。
| 插入 | 查询最小值 | 删除最小值 | 合并 | |
|---|---|---|---|---|
| O(1) | O(1) | O(logN) | O(1) |
配对堆是一棵满足堆性质的带权多叉树,即每个节点的权值都小于或等于他的所有儿子。每个节点储存第一个儿子的指针,即链表的头节点;和他的右兄弟的指针。
查询最小值:根节点即为最小值。
合并:令两个根节点较小的作为一个新的根节点,然后将较大的作为它的儿子插进去。
插入:新建一个节点然后与原堆合并即可。
删除:删除操作较为麻烦,为了保证总的均摊复杂度,需要使用一个两步走的合并方法
把儿子们两两配成一对,在把配成一对的儿子合并到一起。
将新产生的堆从右往左(即老儿子到新儿子的方向)挨个合并到一起。
xxxxxxxxxx681//封装实现,堆顶为最小值. 未经过严谨测试,可能尚存bug2template<typename type>3class heap{4 size_t heap_size;5 struct node{6 node *s,*t;//s指向该节点的第一个儿子,t指向该节点的下一个兄弟7 type val;8 }*root;9
10 node *merge_bros(node *p){//辅助函数,合并一个节点的所有兄弟11 if(p==nullptr || p->t==nullptr) return p;//如果该节点为空或它没有下一个兄弟,就不需要合并了12 node *q=p->t;node *d=q->t;p->t=q->t=nullptr;13 return merge(merge_bros(d),merge(p,q));14 }15
16 inline node *merge(node *p,node *q){//合并两个节点17 if(p==nullptr) return q;18 if(q==nullptr) return p;19 if(p->val > q->val) swap(p,q);20 q->t=p->s,p->s=q;21 return p;22 }23 24 public:25 inline heap<type>(){root=nullptr;}26
27 inline void merge(node *p){28 if(root==nullptr){root=p;return;}29 if(p==nullptr) return;30 if(root->val > p->val) swap(p,root);31 p->t=root->s,root->s=p;32 }33
34 inline void merge(heap x){//合并两个堆,将堆x并入当前堆(不会清空x)35 node *p=x.root;36 heap_size += x.size();37 if(root==nullptr){root=p;return;}38 if(p==nullptr) return;39 if(root->val > p->val) swap(p,root);40 p->t=root->s,root->s=p;41 }42
43 inline void push(type x){44 heap_size++;45 node *p=new node;46 p->val=x;p->s=nullptr,p->t=nullptr;47 merge(p);48 }49
50 inline void pop(){51 heap_size--;52 node *T=root;53 root=merge_bros(root->s);54 delete T;55 }56
57 inline type top(){return root->val;}58 inline bool empty(){return !heap_size;}59 inline size_t size(){return heap_size;}60 inline void clear(){root=nullptr;}61};62
63int main(){64 heap<int> pq1,pq2;65 pq1.push(2);pq2.push(1);66 pq1.merge(pq2);67 std::cout << pq1.top() << ' ' << pq1.size() << '\n';68}
哈希
开放寻址法
xxxxxxxxxx2612using namespace std;3const int N = 2000003, null = 0x3f3f3f3f;//N取大于所给范围两~三倍的一个质数,null取一个取不到的数4int h[N];5int find(int x) {6 int k = (x % N + N) % N;7 //如果x存在则返回x在哈希表中的位置,否则返回x应该在哈希表中插入的位置8 while (h[k] != null && h[k] != x) {//如果这个位置有值且不是x,则k++,直到找到一个空位(null)9 k++;10 if (k == N) k = 0;//诺k为N,则重新从0开始找11 }12 return k;13}14int main() {15 memset(h, 0x3f, sizeof h);//初始化,memset按字节初始化,int类型只要填一个0x3f16 int x; cin >> x;17 int k = find(x);18 //插入x19 h[k] = x;20
21 //查询x是否存在22 if (h[k] == x) cout << "YES";23 else cout << "NO";24
25 return 0;;26}
拉链法
xxxxxxxxxx24123using namespace std;4const int N = 100003;//N取大于所给范围的第一个质数5int h[N], e[N], ne[N],idx;//e,ne为单链表用法,h为头结点6
7void insert(int x) {8 int k = (x % N + N) % N;//k为哈希值9 e[idx] = x;10 ne[idx] = h[k]; 11 h[k] = idx++;12}13bool find(int x) {14 int k = (x % N + N) % N;15 for (int i = h[k]; i != -1;i = ne[i]) {16 if (e[i] == x) return true;17 }18 return false;19}20
21int main() {22 memset(h, -1, sizeof h);//初始化为-1(空指针一般用-1表示)23
24}
定义Hash函数
xxxxxxxxxx62123using namespace std;4using ll = long long;5const int N = 100005,mod = 100003;//取接近N的质数6int h[N],ne[N],idx;7int snow[N][6];//第idx个位置对应的雪花snow[idx][6]8int n;9
10int hs(int a[]){//哈希函数11 ll sum = 0,mul = 1;12 for(int i = 0;i < 6;i++){13 sum = (sum+a[i])%mod;14 mul = mul*a[i]%mod;15 }16 return (sum+mul)%mod;17}18
19bool equal(int a[],int b[]){//判断两片雪花是否相等20 for(int i = 0;i < 6;i++){21 for(int j = 0;j < 6;j++){22 bool eq = 1;23 for(int k = 0;k < 6;k++){24 if(a[(i+k)%6] != b[(j+k)%6]) eq = 0;25 }26 if(eq) return 1;27 eq = 1;28 for(int k = 0;k < 6;k++){29 if(a[(i+k)%6] != b[(j-k+6)%6]) eq = 0;30 }31 if(eq) return 1;32 }33 }34 return 0;35}36
37bool insert(int a[]){38 int key = hs(a);39
40 for(int i = h[key];i != -1;i = ne[i]){//诺发生哈希冲突41 if(equal(snow[i],a)) return 1;//且出现两片雪花相等,返回142 }43 //否则在哈希值对应链表位置接上该雪花44 memcpy(snow[idx],a,6*sizeof(int));45 ne[idx] = h[key];46 h[key] = idx++;47 return 0;48}49
50int main(){51 cin >> n;52 memset(h,-1,sizeof h);53 for(int i = 1;i <= n;i++){54 int a[10];55 for(int j = 0;j < 6;j++) cin >> a[j];56 if(insert(a)){57 cout << "Twin snowflakes found.";58 return 0;59 }60 }61 cout << "No two snowflakes are alike.";62}
并查集
并查集(DSU)是一种用于管理元素所属集合的数据结构,实现为一个森林,其中每棵树表示一个集合,树中的节点表示对应集合中的元素
xxxxxxxxxx171int p[N],siz[N];//p[i]存节点i的根节点,siz[i]存以i为根的集合大小2
3int find(int x){//查找x的根节点+路径压缩4 if(p[x] != x) p[x] = find(p[x]);5 return p[x];6}7
8void merge(int a,int b){//将a所在集合并入b所在集合9 int pa = find(a),pb = find(b);10 if(pa == pb) return;11 p[pa] = pb;12 siz[pb] += siz[pa];13}14
15int main(){16 for(int i = 1;i <= n;i++) p[i] = i,siz[i] = 1;//初始化每个节点的父节点为自身17}i == p[i] 父节点等于自身的数量即为连通块的数量How Many Tables - HDU 1213 - Virtual Judge (vjudge.net)
xxxxxxxxxx211//二维矩阵类型并查集(也可以改用pair<int,int>,或者将二维坐标[x,y]映射为一维)2struct node {3 int x,y;4 bool operator == (const node&e2)const{5 return x == e2.x && y == e2.y;6 }7 bool operator != (const node&e2)const{8 return x != e2.x || y != e2.y;9 }10}p[N][N];11
12node find(node x){13 if(p[x.x][x.y] != x) p[x.x][x.y] = find(p[x.x][x.y]);14 return p[x.x][x.y];15}16
17void merge(node x1,node x2){18 node pa = find(x1),pb = find(x2);19 if(pa == pb) return;20 else p[pa.x][pa.y] = pb;21}
边带权并查集
并查集实际上是由诺干棵树构成的森林,我们可以在树中的每条边上记录一个权值,即维护一个数组d[ ],用d[x]保存节点x到父亲节点p[x]之间的边权。在每次路径压缩后,每个访问过的节点都会直接指向树根,如果我们同时更新这些节点的d值,就可以利用路径压缩过程来统计每个节点到树根之间的路径上的一些信息。
关系的传递可以看做向量。
如果pa == pb那么我们可以检查d[a] + s是否等于d[b]来判断是否矛盾,或者求出相对关系大小s。
否则可以合并两者:p[pb] = pb, d[pb] = d[a] + s - d[b]。
[P1196 NOI2002] 银河英雄传说 - 洛谷 (luogu.com.cn)
有N只飞船,M条指令,每条指令为以下之一:
1.
M i j,表示让i号飞船所在的队列全部飞船保持原有顺序,接在第j号飞船所在队列的尾部 2.C i j,表示询问第i号飞船与第j号飞船当前是否处于同一列中,如果在,输出他们之间间隔了多少飞船
xxxxxxxxxx3612
3const int N = 30004;4int n = 30000,q;5int p[N],d[N],siz[N];//d[x]代表x与其父节点(路径压缩后即根结点)之间的边的权值,即位于x之前的飞船数量6
7int find(int x){//带权并查集模版,路径压缩时维护d[]8 if(p[x] != x) {9 int root = find(p[x]);10 d[x] += d[p[x]];11 p[x] = root;12 }13 return p[x];14}15
16int main(){17 for(int i = 1;i <= n;i++) p[i] = i,siz[i] = 1;18 std::cin >> q;19 while(q--){20 char op;int a,b;std::cin >> op >> a >> b;21 int pa = find(a),pb = find(b);22 if(op == 'M'){23 if(pa == pb) continue;24 else{25 p[pa] = pb;//将a所在队列并入b所在队列26 int s = siz[pb]-d[b] + d[a];//s即为(b到队列pb尾部的距离)+(队列pa头部到a的距离)27 d[pa] = d[b] + s - d[a];28 siz[pb] += siz[pa];29 }30 }31 if(op == 'C'){32 if(pa == pb) std::cout << std::max(0,std::abs(d[b] - d[a])-1) << '\n'; 33 else std::cout << -1 << '\n'; 34 }35 }36}
拓展域并查集
用于解决具有多个相互关系集合的问题。它是传统并查集的扩展,能够处理集合间的不同关系,如相互排斥或相互独立的关系。
[P1525 NOIP 2010 提高组] 关押罪犯 - 洛谷 (luogu.com.cn)
n 个点有 m 对关系,把 n 个节点放入两个集合里,要求每对存在关系的两个节点不能放在同一个集合。问能否成功完成?
我们使用拓展域并查集,将u和u+n分别表示u的两个相反状态,将(u,v+n)连边表示(u,v)不应在一个集合里(也可以看作u和v的反状态在一个集合里)。相应的(v,u+n)也应该连边。诺(u,u+n)在同一个集合里则说明发生了矛盾。
xxxxxxxxxx43123using namespace std;4const int N = 40004,M = 100005;5int n,m;6int p[N];7
8struct node{9 int a,b,c;10 bool operator < (const node &e2)const{11 return c > e2.c;12 }13}e[M];14
15int find(int x){16 if(p[x] != x) p[x] = find(p[x]);17 return p[x];18}19
20void merge(int x,int y){21 int px = find(x),py = find(y);22 if(px != py) p[px] = py;23}24
25int main(){26 cin >> n >> m;27 for(int i = 1;i <= n << 1;i++) p[i] = i;28
29 for(int i = 1;i <= m;i++){30 cin >> e[i].a >> e[i].b >> e[i].c;31 }32 sort(e+1,e+m+1);33
34 for(int i = 1;i <= m;i++){35 auto [a,b,c] = e[i];36 merge(a,b+n); merge(a+n,b);37 if(find(a) == find(a+n) || find(b) == find(b+n)) {//发生矛盾38 cout << c;39 return 0;40 }41 }42 cout << 0;43}
[P2024 NOI2001] 食物链 - 洛谷 (luogu.com.cn)
每个动物都是A,B,C的一种,三者形成环形食物链,依次给出m个句话,为以下两种形式之一
1 x y表示x和y是同类2 x y表示x吃y诺当前话与前面的话产生矛盾,则当前话为假话。求有多少假话。
我们用(x)表示同类域,(x+n)表示捕食域,(x+2n)表示天敌域
xxxxxxxxxx4712
3const int N = 50004*3;4int n,q;5int p[N],d[N];6
7int find(int x){8 if(p[x] != x) {9 int root = find(p[x]);10 d[x] += d[p[x]];11 p[x] = root;12 }13 return p[x];14}15
16void merge(int x,int y){17 int px = find(x),py = find(y);18 if(px == py) return;19 p[px] = py;20}21
22int main(){23 int ans = 0;24 std::cin >> n >> q;25 for(int i = 1;i <= n*3;i++) p[i] = i;26 while(q--){27 int op,x,y;std::cin >> op >> x >> y;28 if(x > n || y > n) {ans++;continue;}29 if(op == 1){//x与y是同类30 if(find(x) == find(y+n) || find(x) == find(y+n+n)) ans++;//如果y是x的食物或y是x的天敌,矛盾31 else{32 merge(x,y);33 merge(x+n,y+n);34 merge(x+n+n,y+n+n);35 }36 }37 else{//x吃y38 if(find(x) == find(y+n+n) || find(x) == find(y)) ans++;//如果y是x的天敌或y是x的同类,矛盾39 else{40 merge(x,y+n);41 merge(x+n,y+n+n);42 merge(x+n+n,y);43 }44 }45 }46 std::cout << ans;47}
分块
O(
| 元素个数n (1~n) | |
|---|---|
| 每一块的最长长度m | m = sqrt(n) |
| 块的个数num | num = (n/m)+bool(n%m) |
| 第i个元素所在的块id[i] | id[i] = (i-1)/m + 1 |
| 第i个元素所在块的左端点b[id[i]].l | if(b[id[i]].l == 0) b[id[i]].l = i |
| 第i个元素所在块的右端点b[id[i]].r | b[id[i]].r = max( b[id[i]].r , i ) |
xxxxxxxxxx131//数据定义2int id[N];//第i个元素在哪个块中3struct Block{//存每一块的属性4 int l,r;//该块的左右端点5 ll lazy;//lazy标记6 ll sum;//区间和7 ll add;//区间加标记8 ll mul;//区间乘标记9 ll cnt;//块中元素个数标记10 //...11}b[N];12
13vector<int>v[N]; //如果需要对块进行排序、拷贝等操作,可以开vector数组将每一个元素放入对应块中
xxxxxxxxxx101//初始化2void initi(){3 m = sqrt(n);4 for(int i = 1;i <= n;i++){5 id[i] = (i-1)/m + 1;//第i个元素在第id[i]块6 if(b[id[i]].l == 0) b[id[i]].l = i;//第id[i]块的左右端点7 b[id[i]].r = max(b[id[i]].r,(ll)i);8 //v[id[i]].emplace_back(a[i]);9 }10}
xxxxxxxxxx211//修改/查询2
3//如果l和r在同一块4if(id[l] == id[r]){5 for(int i = l;i <= r;i++){6 //直接枚举l~r所有元素7 }8}9
10//如果l和r不在同一块11else{12 for(int i = l;id[i] == id[l];i++){13 //枚举l所在块14 }15 for(int i = r;id[i] == id[r];i--){16 //枚举r所在块17 } 18 for(int i = id[l]+1;i < id[r];i++){19 //l~r之间所有完整块利用区间属性快速操作20 }21}
xxxxxxxxxx921//区间修改,区间求小于x的数的个数:https://loj.ac/p/62782//区间修改,区间求大于等于x的数的个数:https://www.luogu.com.cn/problem/P280134567using namespace std;8using ll = long long;9const ll N = 100005;10ll n,m,cnt;11ll a[N];12
13ll id[N];14vector<ll>v[N];//v[i]存第i块排序后的序列15struct Bolck{16 ll l,r,lazy;17}b[N];18
19void initi(){20 m = sqrt(n);21 for(int i = 1;i <= n;i++){22 id[i] = (i-1)/m + 1;23 v[id[i]].emplace_back(a[i]);24 if(b[id[i]].l == 0) b[id[i]].l = i;25 b[id[i]].r = max(b[id[i]].r,(ll)i);26 }27 for(int i = 1;i <= id[n];i++){28 sort(v[i].begin(),v[i].end());29 }30}31
32void bsort(int i){//块排序33 v[i].clear();34 for(int j = b[i].l;j <= b[i].r;j++){35 v[i].emplace_back(a[j]);36 }37 sort(v[i].begin(),v[i].end());38}39
40void add(ll l,ll r,ll c){41 if(id[l] == id[r]){42 for(int i = l;i <= r;i++){43 a[i]+=c;44 }45 bsort(id[l]);46 }47 else{48 for(int i = l;id[i] == id[l];i++){49 a[i]+=c;50 }51 for(int i = r;id[i] == id[r];i--){52 a[i]+=c;53 }54 for(int i = id[l]+1;i < id[r];i++){55 b[i].lazy += c;56 }57 bsort(id[l]);58 bsort(id[r]);59 }60}61
62ll query(int l,int r,int c){63 ll ans = 0;64 if(id[l] == id[r]){65 for(int i = l;i <= r;i++){66 ans+=(a[i] < c-b[id[i]].lazy);67 }68 }69 else{70 for(int i = l;id[i] == id[l];i++){71 ans+=(a[i] < c-b[id[i]].lazy);72 }73 for(int i = r;id[i] == id[r];i--){74 ans+=(a[i] < c-b[id[i]].lazy);75 }76 for(int i = id[l]+1;i < id[r];i++){//l~r之间每个完整块v[i]二分查找77 ans+=lower_bound(v[i].begin(),v[i].end(),c-b[i].lazy)-v[i].begin();78 }79 }80 return ans;81}82
83int main(){84 cin >> n;85 for(int i = 1;i <= n;i++){ cin >> a[i]; }86 initi();87 for(int i = 1;i <= n;i++){88 ll op,l,r,c;cin >> op >> l >> r >> c;89 if(op == 0){ add(l,r,c);}90 else{ cout << query(l,r,c*c) <<endl; }91 }92}xxxxxxxxxx681//单点插入,单点查询2345using namespace std;6const int N = 100005<<1;7int n,m,all,num;//n:初始元素数量,all:插入后元素数量,m:块长度,num:块数量8int a[N];9
10vector<int>v[N];11int id[N];12
13void initi(){14 m = sqrt(all);15 num = all/m + bool (all%m);16 for(int i = 1;i <= all;i++){17 id[i] = (i-1)/m + 1;18 v[id[i]].emplace_back(a[i]);19 }20}21
22void reint(){//重新分块,保证每块都接近sqrt(all),否则可能退化为普通数组23 int cnt = 1;24 for(int i = 1;i <= num;i++){25 for(int j = 0;j < v[i].size();j++){26 a[cnt++] = v[i][j];27 }28 v[i].clear();29 }30}31
32void ins(int l,int x){33 int cnt = 1;34 while(l > v[cnt].size()){//根据每个块的长度推出第l个元素的位置35 l -= v[cnt].size();36 cnt++;37 }38 v[cnt].insert(v[cnt].begin()+ l-1,x);39 all++;40
41 if(v[id[l]].size() > 2*m){//如果插入后,块的长度>2*m,则清空v[]还原到a[],重新初始化分块42 reint();43 initi();44 }45}46
47int query(int r){48 int cnt = 1;49 while(r > v[cnt].size()){50 r -= v[cnt].size();51 cnt++;52 }53 return v[cnt][r-1];54}55
56int main(){57 cin >> n;58 all = n;59 for(int i = 1;i <= n;i++){60 cin >> a[i];61 }62 initi();63 for(int i = 1;i <= n;i++){64 int op,l,r,c;cin >> op >> l >> r >> c;65 if(op == 0){ ins(l,r); }66 else{ cout << query(r) << endl; }67 }68}
树状数组
树状数组是(Fenwick Tree)一种支持单点修改和区间查询,代码量小的数据结构,树状数组能解决的问题是线段树能解决的问题的子集
普通树状数组维护的信息及运算要满足 结合律 且 可差分,如加法、乘法、异或等。
一维树状数组
xxxxxxxxxx481template<typename T>2struct Fenwick{3 int n;4 vector<T>t;5
6 Fenwick(int n_ = 0){7 init(n_);8 }9
10 void init(int n_){11 n = n_ + 1;12 t.assign(n,T{});13 }14
15 void add(int i,const T &x){16 while(i <= n){17 t[i] += x;18 i += i&-i;19 }20 }21
22 T sum(int i){23 T ans = 0;24 while(i){25 ans += t[i];26 i -= i&-i;27 }28 return ans;29 }30
31 T sum(int l,int r){32 return sum(r) - sum(l-1);33 }34
35 int kth(int k){36 int sum = 0,x = 0;37 for(int i = log2(n);i >= 0;i--){38 int nx = x + (1 << i);39 if(nx < n && sum + t[nx] < k){40 x = nx;41 sum += t[x];42 }43 }44 return x + 1;45 }46};47
48Fenwick<int>t(n);
区间修改,单点查询
xxxxxxxxxx441//利用差分及前缀和数组,t[]建树为差分数组23using namespace std;4using ll = long long;5const int N = 1000006;6int n,q;7ll a[N],t[N];8
9ll lowbit(ll x){return x&-x;}10
11void add(ll i,ll x){12 while(i <= n){13 t[i] += x;14 i += lowbit(i);15 }16}17
18ll getsum(int i){19 ll ans = 0;20 while(i > 0){21 ans += t[i];22 i -= lowbit(i);23 }24 return ans;25}26
27int main(){28 cin >> n >> q;29 for(int i = 1;i <= n;i++){30 cin >> a[i];31 }32
33 while(q--){34 int op;cin >> op;35 if(op == 1){//区间l~r加上x36 int l,r,x;cin >> l >> r >> x;37 add(l,x);add(r+1,-x);38 }39 else{//查询下标为x的数所在的值40 int x;cin >> x;41 cout << a[x] + getsum(x) << '\n';42 }43 }44}
权值树状数组
诺题目允许离线,则离散化后空间复杂度为O(N),否则需要开到O(值域)的大小。
单点修改,全局第k小
xxxxxxxxxx121// 权值树状数组查询全局第 k 小,倍增代替二分,O(logN)2int kth(int k){3 int sum = 0,x = 0;4 for(int i = log2(n);i >= 0;i--){//要注意n应为b[]的值域而非数组大小5 int nx = x + (1 << i);6 if(nx < n && sum + t[nx] < k){//诺成功扩展7 x = nx;8 sum += t[x];9 }10 }11 return x + 1;12}
P3369 【模板】普通平衡树 - 洛谷 (luogu.com.cn)
您需要动态地维护一个可重集合
,并且提供以下操作:
向
中插入一个数 。 从
中删除一个数 (若有多个相同的数,应只删除一个)。 查询
中有多少个数比 小,并且将得到的答案加一。 查询如果将
从小到大排列后,排名位于第 位的数。 查询
中 的前驱(前驱定义为小于 ,且最大的数)。 查询
中 的后继(后继定义为大于 ,且最小的数)。 对于操作 3,5,6,不保证当前可重集中存在数
。
本题中我们只需要关心的是每个数之间的相对关系,且允许离线,因此可以离散化处理。
xxxxxxxxxx8212345using namespace std;6const int N = 100005;7int op[N],a[N];8int n;9vector<int>hs;10
11template<typename T>12struct Fenwick{13 int n;14 vector<T>t;15
16 Fenwick(int n_ = 0){17 init(n_);18 }19
20 void init(int n_){21 n = n_ + 1;22 t.assign(n,T{});23 }24
25 void add(int i,const T &x){26 while(i <= n){27 t[i] += x;28 i += i&-i;29 }30 }31
32 T sum(int i){33 T ans = 0;34 while(i){35 ans += t[i];36 i -= i&-i;37 }38 return ans;39 }40
41 T sum(int l,int r){42 return sum(r) - sum(l-1);43 }44
45 int kth(int k){46 int sum = 0,x = 0;47 for(int i = log2(n);i >= 0;i--){48 int nx = x + (1 << i);49 if(nx < n && sum + t[nx] < k){50 x = nx;51 sum += t[x];52 }53 }54 return x + 1;55 }56};57
58int main(){59 int q;cin >> q;60 for(int i = 1;i <= q;i++){61 cin >> op[i] >> a[i];62 if(op[i] != 4) hs.emplace_back(a[i]);63 }64
65 hs.emplace_back(-1e9);66 sort(hs.begin(),hs.end());67 hs.erase(unique(hs.begin(),hs.end()),hs.end());68 n = hs.size()-1;69 for(int i = 1;i <= q;i++){70 if(op[i] != 4) a[i] = lower_bound(hs.begin(),hs.end(),a[i])-hs.begin();71 }72
73 Fenwick<int>t(n+5);74 for(int i = 1;i <= q;i++){75 if(op[i] == 1) t.add(a[i],1);//插入一个数x76 if(op[i] == 2) t.add(a[i],-1);//删除一个数x77 if(op[i] == 3) cout << t.sum(a[i]-1)+1 << '\n';//a[i]的排名,即区间[1,q[i]-1]的数的个数再加178 if(op[i] == 4) cout << hs[t.kth(a[i])] << '\n';//查询全局第k小的数(下标从1开始)79 if(op[i] == 5) cout << hs[t.kth(t.sum(a[i]-1))] << '\n';//前驱(诺不存在则返回x)80 if(op[i] == 6) cout << hs[t.kth(t.sum(a[i])+1)] << '\n';//后继(诺不存在则返回x)81 }82}
静态区间颜色数
给点一个长度为n的数组a[ ],q个询问:区间[l,r]不同元素的个数。
思路:对于询问多个区间 [L,R] 中出现不同数字个数,当 R 相同时,如果区间出现了重复数字,那么我们只需要关心最右端的这个数字就可以了。换言之,重复出现的数字在左端将无任何贡献。例如 {1 2 3 1 4},在询问 区间[1,4] 时,第一个 1 无任何贡献,并且当询问的 R 不断右移的过程中,第一个 1 都无任何意义,被第四位置的替代掉。
于是把询问离线下来,按照询问的右端点排序。
当出现一个数字时,如果这个数字曾经出现,则 add(lst[pos[i]],-1),把这个数在上一个位置的标记清掉,add(pos[i],1) 来更新这个数字的新位置。只需要维护第i个位置上有没有数,求区间[l,r]内有多少个数。
用树状数组单点修改、区间查询。用前缀和 query(R)-query(L-1) 把区间[L,R]的数的个数出来。
xxxxxxxxxx571//HH的项链 https://www.luogu.com.cn/problem/P1972234using namespace std;5const int N = 1000006;6int n,q;7int a[N];//本题a[i] <= 1e6,不用离散化8int pos[N];9int ans[N];10
11struct Ask{12 int l,r,id;13 bool operator < (Ask &e2){14 return r < e2.r;15 }16}ask[N];17
18int t[N];19
20void add(int i,int x){21 while(i <= n){22 t[i] += x;23 i += i&-i;24 }25}26
27int query(int i){28 int ans = 0;29 while(i > 0){30 ans += t[i];31 i -= i&-i;32 }33 return ans;34}35
36int main(){37 cin >> n;38 for(int i = 1;i <= n;i++) cin >> a[i];39 cin >> q;40 for(int i = 1;i <= q;i++){41 cin >> ask[i].l >> ask[i].r;42 ask[i].id = i;43 }44 sort(ask+1,ask+q+1);//按右端点升序排序45
46 for(int i = 1,j = 1;i <= q;i++){//处理每个询问ask[i],j为当前右端点。47 auto &[l,r,id] = ask[i];48 while(j <= r){//更新j到ask[i].r的值49 if(pos[a[j]]) add(pos[a[j]],-1);//诺a[j]之前已经出现过则删去50 pos[a[j]] = j;51 add(j,1);52 j++;53 }54 ans[id] = query(r) - query(l-1);55 }56 for(int i = 1;i <= q;i++) cout << ans[i] << '\n';57}
线段树
线段树是常用的用来维护 区间信息 的数据结构。
可以在
建树
xxxxxxxxxx121struct ST{2 int l,r;3 int dat;//lazy,mx,mn,sum,gcd...4}t[N<<2];//至少要开 4*N 空间5
6void build(int p,int l,int r){//build(1,1,n);7 t[p] = {l,r,0};8 if(l == r){t[p].dat = a[l];return;}9 int mid = l + r >> 1;10 build(p+p,l,mid);build(p+p+1,mid+1,r);//递归建树11 //t[p].dat = f(t[p+p].dat,t[p+p+1].dat);12}
单点修改,区间查询
单点修改:从根节点开始遍历,递归找到需要修改的叶子节点,然后修改,然后向上传递信息。
区间查询: 1.若当前节点所表示的区间已经被询问区间所完全覆盖,则立即回溯,并传回该点的信息。 2.若当前节点的左儿子所表示的区间已经被询问区间所完全覆盖,就递归访问它的左儿子。 3.若当前节点的右儿子所表示的区间已经被询问区间所完全覆盖,就递归访问它的右儿子。
xxxxxxxxxx241//单点修改,区间最值 http://acm.hdu.edu.cn/showproblem.php?pid=17542void change(int p,int i,int x){//change(1,i,x);3 if(t[p].l == t[p].r) {t[p].mx = x;return;}4 int mid = t[p].l + t[p].r >> 1;5 if(i <= mid) change(p+p,i,x);6 else change(p+p+1,i,x);7 t[p].mx = max(t[p+p].mx,t[p+p+1].mx);8}9
10int query_max(int p,int l,int r){//query(1,l,r);11 if(l <= t[p].l && r >= t[p].r) return t[p].mx;12 int mid = t[p].l + t[p].r >> 1;13 int ans = -1e9;14 if(l <= mid) ans = max(ans,query(p+p,l,r));15 if(r > mid) ans = max(ans,query(p+p+1,l,r));16 return ans;17}18
19int query(int p,int i){//单点查询20 if(t[p].l == t[p].r) return t[p].mx;21 int mid = t[p].l + t[p].r >> 1;22 if(i <= mid) return query(p<<1,i);23 else return query(p<<1|1,i);24}
xxxxxxxxxx751//单点修改,求区间最大子段和 https://www.luogu.com.cn/problem/SP17162//单点修改,求区间最大值出现次数 https://ac.nowcoder.com/acm/contest/90296/E345using namespace std;6const int N = 50004;7int n,q;8int a[N];9
10struct ST{11 int l,r;12 int ls,rs;//紧靠左端点和紧靠右端点的最大子段和13 int ans,sum;//sum为区间和,ans为最长子段和14}t[N<<2];15
16void pushup(ST &p,ST &pl,ST &pr){17 p.sum = pl.sum + pr.sum;18 p.ls = max(pl.ls,pl.sum+pr.ls);//紧靠左端点不跨越中间和跨越中间取最大值19 p.rs = max(pr.rs,pr.sum+pl.rs);20 p.ans = max({pl.ans,pr.ans,pl.rs+pr.ls});//答案在{左区间答案,右区间答案,跨中间的最长子段}取最值21}22
23void build(int p,int l,int r){24 t[p] = {l,r};25 if(l == r){26 t[p].ls = t[p].rs = t[p].ans = t[p].sum = a[l];27 return;28 }29 int mid = l + r >> 1;30 build(p<<1,l,mid);build(p<<1|1,mid+1,r);31 pushup(t[p],t[p<<1],t[p<<1|1]);32}33
34void change(int p,int i,int x){35 if(t[p].l == t[p].r){36 t[p].ls = t[p].rs = t[p].ans = t[p].sum = x;37 return;38 }39 int mid = t[p].l + t[p].r >> 1;40 if(i <= mid) change(p<<1,i,x);41 else change(p<<1|1,i,x);42 pushup(t[p],t[p<<1],t[p<<1|1]);43}44
45ST query(int p,int l,int r){//更加通用的query写法,返回的是一个节点包含了区间[l,r]的ans、sum等信息46 if(l <= t[p].l && r >= t[p].r){47 return t[p];48 }49 int mid = t[p].l + t[p].r >> 1;50 if(r <= mid) return query(p<<1,l,r);//要查询的区间在mid左边,答案在左区间,注意l,r位置51 if(l > mid) return query(p<<1|1,l,r);//要查询的区间在mid右边,答案在右区间52 ST pl = query(p<<1,l,r);53 ST pr = query(p<<1|1,l,r);54 ST ans;55 pushup(ans,pl,pr);56 return ans;57}58
59int main(){60 cin >> n;61 for(int i = 1;i <= n;i++){62 cin >> a[i];63 }64 build(1,1,n);65 cin >> q;66 while(q--){67 int op,l,r;cin >> op >> l >> r;68 if(op == 0){69 change(1,l,r);70 }71 else{72 cout << query(1,l,r).ans << '\n';73 }74 }75}
F - Palindrome Query (atcoder.jp)
单点修改,查询区间是否为回文串(字符串哈希)
我们只需要单点修改,求区间和即可得到子串哈希值。比较正反串的哈希值是否一致判断回文串。
xxxxxxxxxx9412using namespace std;3using ull = unsigned long long;4const ull P = 131;5const int N = 1000006;6int n,q;7ull f[N];8string s;9
10struct ST{11 int l,r;12 ull kl,kr;13}t[N<<2];14
15void pushup(ST &p,ST &pl,ST &pr){16 p.kl = pl.kl + pr.kl;17 p.kr = pl.kr + pr.kr;18}19
20void pushup(int p){21 pushup(t[p],t[p<<1],t[p<<1|1]);22}23
24void update(ST &p,char x){25 p.kl = f[n-p.l]*x;26 p.kr = f[p.l-1]*x;27}28
29void build(int p,int l,int r){30 t[p] = {l,r};31 if(l == r){32 update(t[p],s[l-1]);33 return;34 }35 int mid = l + r >> 1;36 build(p<<1,l,mid);build(p<<1|1,mid+1,r);37 pushup(p);38}39
40void modify(int p,int i,char x){41 if(t[p].l == t[p].r){42 update(t[p],x);43 return;44 }45 int mid = t[p].l + t[p].r >> 1;46 if(i <= mid) modify(p<<1,i,x);47 else modify(p<<1|1,i,x);48 pushup(p);49}50
51ST query(int p,int l,int r){52 if(l <= t[p].l && r >= t[p].r){53 return t[p];54 }55 int mid = t[p].l + t[p].r >> 1;56 if(r <= mid) return query(p<<1,l,r);57 if(l > mid) return query(p<<1|1,l,r);58 ST pl = query(p<<1,l,r);59 ST pr = query(p<<1|1,l,r);60 ST ans;61 pushup(ans,pl,pr);62 return ans;63}64
65int main(){66 f[0] = 1;67 for(int i = 1;i < N;i++){68 f[i] = f[i-1]*P;69 }70
71 cin >> n >> q;72 cin >> s;73 build(1,1,n);74
75 while(q--){76 int op;cin >> op;77 if(op == 1){78 int x; char op; cin >> x >> op;79 modify(1,x,op);80 }81 else{82 int l,r;cin >> l >> r;83 auto ans = query(1,l,r);84 ull kl = ans.kl,kr = ans.kr;85
86 int k1 = l-1,k2 = n-r;87 if(k1 > k2) kl *= f[k1-k2];88 else kr *= f[k2-k1];89
90 if(kl == kr){ cout << "Yes\n"; }91 else{ cout << "No\n"; }92 }93 }94}
xxxxxxxxxx861//区间修改,区间公约数 https://www.luogu.com.cn/problem/P104632//利用gcd辗转相减法性质,将a[]数组变为差分数组d[],区间修改(a[l~r]+x)变为单点修改(a[l]+x,d[r-1]-x)3//答案为 gcd(a[l],d[l+1],...,d[r]) 的绝对值45using namespace std;6using ll = long long;7const int N = 500005;8int n,q;9ll a[N];10
11ll gcd(ll a,ll b){return b?gcd(b,a%b):a;}12
13struct ST{14 int l,r;15 ll dat,sum;16}t[N<<2];17
18void pushup(ST &p,ST &pl,ST &pr){19 p.dat = gcd(pl.dat,pr.dat);20 p.sum = pl.sum + pr.sum;21}22
23void build(int p,int l,int r){24 t[p] = {l,r};25 if(l == r){26 t[p].dat = t[p].sum = a[l] - a[l-1];27 return;28 }29 int mid = l + r >> 1;30 build(p<<1,l,mid);build(p<<1|1,mid+1,r);31 pushup(t[p],t[p<<1],t[p<<1|1]);32}33
34void add(int p,int i,ll x){35 if(t[p].l == t[p].r){36 t[p].dat += x;37 t[p].sum += x;38 return;39 }40 int mid = t[p].l + t[p].r >> 1;41 if(i <= mid) add(p<<1,i,x);42 else add(p<<1|1,i,x);43 pushup(t[p],t[p<<1],t[p<<1|1]);44}45
46ll query_sum(int p,int l,int r){47 if(l <= t[p].l && r >= t[p].r){48 return t[p].sum;49 }50 ll ans = 0;51 int mid = t[p].l + t[p].r >> 1;52 if(l <= mid) ans += query_sum(p<<1,l,r);53 if(r > mid) ans += query_sum(p<<1|1,l,r);54 return ans;55}56
57ll query_gcd(int p,int l,int r){58 if(l <= t[p].l && r >= t[p].r){59 return t[p].dat;60 }61 ll ans = 0;62 int mid = t[p].l + t[p].r >> 1;63 if(l <= mid) ans = gcd(ans,query_gcd(p<<1,l,r));64 if(r > mid) ans = gcd(ans,query_gcd(p<<1|1,l,r));65 return ans;66}67
68int main(){69 cin >> n >> q;70 for(int i = 1;i <= n;i++){71 cin >> a[i];72 }73 build(1,1,n);74 while(q--){75 char op;cin >> op;76 if(op == 'C'){77 ll l,r,x;cin >> l >> r >> x;78 add(1,l,x);79 if(r+1 <= n) add(1,r+1,-x);80 }81 else{82 int l,r;cin >> l >> r;83 cout << abs(gcd(query_gcd(1,l+1,r),query_sum(1,1,l))) << '\n';84 }85 }86}
区间修改,区间查询,lazy标记
通过延迟对节点信息的更改,从而减少可能不必要的操作次数。每次执行修改时,我们通过打标记的方法表明该节点对应的区间在某一次操作中被更改,但不更新该节点的子节点的信息。实质性的修改则在下一次访问带有标记的节点时才进行。叶子结点无需下放标记。
Transformation - HDU 4578 - Virtual Judge (vjudge.net)
对数组
a[]实现以下操作1 x y z将区间[x,y]之间的每个元素增加z。2 x y z将区间[x,y]之间的每个元素乘以z。3 x y z将区间[x,y]之间的每个元素变为z。4 x y z输出对10007取模后的结果。
xxxxxxxxxx9712
3const int mod = 10007;4const int N = 100005;5int n,m;6
7struct ST{8 int l,r;9 long long d[4];10 long long add,mul;11}t[N<<2];12
13void build(int p,int l,int r){14 t[p] = {l,r};15 if(l == r){16 t[p].add = 0;17 t[p].mul = 1;18 return;19 }20 int mid = l + r >> 1;21 build(p<<1,l,mid);build(p<<1|1,mid+1,r);22}23
24void pushup(ST &p,ST &pl,ST &pr){25 p.d[1] = (pl.d[1] + pr.d[1]) % mod;26 p.d[2] = (pl.d[2] + pr.d[2]) % mod;27 p.d[3] = (pl.d[3] + pr.d[3]) % mod;28}29
30void pushup(int p){31 pushup(t[p],t[p<<1],t[p<<1|1]);32}33
34void update(ST &p,long long add,long long mul){//注意答案、数据的更新顺序35 int len = p.r - p.l + 1;36 p.add = (p.add * mul + add) % mod;37 p.mul = (p.mul * mul) % mod;38 p.d[3] = (mul*mul%mod*mul%mod*p.d[3]%mod + 3*mul*mul%mod*add%mod*p.d[2]%mod + 3*add*add%mod*mul%mod*p.d[1]%mod + add*add%mod*add%mod*len%mod) % mod;39 p.d[2] = (p.d[2]*mul%mod*mul%mod + 2*mul*add%mod*p.d[1]%mod + add*add%mod*len%mod) % mod;40 p.d[1] = (p.d[1]*mul + len*add) % mod;41}42
43void pushdown(int p){44 auto &add = t[p].add,&mul = t[p].mul;45 if(add || mul != 1){//同时下传所有标记46 update(t[p<<1],add,mul);47 update(t[p<<1|1],add,mul);48 add = 0,mul = 1;49 }50}51
52void modify(int p,int l,int r,int x,int op){//用当前标记更新答案(和更新其它标记(如果有且需要更新))53 if(l <= t[p].l && r >= t[p].r){54 if(op == 1){//add55 update(t[p],x,1);56 }57 if(op == 2){//mul58 update(t[p],0,x);59 }60 if(op == 3){//same 相当于区间乘0再加x61 update(t[p],x,0);62 }63 return;64 }65 pushdown(p);66 int mid = t[p].l + t[p].r >> 1;67 if(l <= mid) modify(p<<1,l,r,x,op);68 if(r > mid) modify(p<<1|1,l,r,x,op);69 pushup(p);70}71
72ST query(int p,int l,int r){//比较通用的query写法73 if(l <= t[p].l && r >= t[p].r){74 return t[p];75 }76 pushdown(p);77 int mid = t[p].l + t[p].r >> 1;78 if(r <= mid) return query(p<<1,l,r);79 if(l > mid) return query(p<<1|1,l,r);80 ST pl = query(p<<1,l,r),pr = query(p<<1|1,l,r),ans;81 pushup(ans,pl,pr);82 return ans;83}84
85void sol(){86 build(1,1,n);87 while(m--){88 int op,x,y,z;std::cin >> op >> x >> y >> z;89 if(op == 4)std::cout << query(1,x,y).d[z] << '\n'; 90 else modify(1,x,y,z,op);91 }92}93
94int main(){95 std::ios::sync_with_stdio(false);std::cin.tie(0);96 while(std::cin >> n >> m,n|m) sol();97}
SP2713 GSS4 - Can you answer these queries IV - 洛谷 (luogu.com.cn)
区间开平方,求区间和
对于区间开平方,由于我们无法在不更新叶子节点时更新区间,因此无法使用lazy标记优化区间修改。考虑到每个数最多被开平方有限次之后就会变为1,于是我们在修改时,一直修改到 叶子节点 或者 当前区间所有数都为1。
xxxxxxxxxx651const int N = 100005;2int n,q;3long long a[N];4
5struct ST{6 int l,r;7 long long dat;8}t[N<<2];9
10void pushup(ST &p,ST &pl,ST &pr){11 p.dat = pl.dat + pr.dat;12}13
14void pushup(int p){15 pushup(t[p],t[p<<1],t[p<<1|1]);16}17
18void build(int p,int l,int r){19 t[p] = {l,r};20 if(l == r){21 t[p].dat = a[l];22 return;23 }24 int mid = l + r >> 1;25 build(p<<1,l,mid);build(p<<1|1,mid+1,r);26 pushup(p);27}28
29void modify(int p,int l,int r){30 if(t[p].dat == t[p].r - t[p].l + 1) {//当前区间所有数都为1时,直接返回31 return;32 }33 if(t[p].l == t[p].r) {//当前为叶子节点时,直接修改34 t[p].dat = std::sqrt(t[p].dat);35 return;36 }37 int mid = t[p].l + t[p].r >> 1;38 if(l <= mid) modify(p<<1,l,r);39 if(r > mid) modify(p<<1|1,l,r);40 pushup(p);41}42
43ST query(int p,int l,int r){44 if(l <= t[p].l && r >= t[p].r){45 return t[p];46 }47 int mid = t[p].l + t[p].r >> 1;48 if(r <= mid) return query(p<<1,l,r);49 if(l > mid) return query(p<<1|1,l,r);50 ST pl = query(p<<1,l,r),pr = query(p<<1|1,l,r),ans;51 pushup(ans,pl,pr);52 return ans;53}54
55void sol(){56 for(int i = 1;i <= n;i++) std::cin >> a[i];57 build(1,1,n);58 std::cin >> q;59 while(q--){60 int op,l,r;std::cin >> op >> l >> r;61 if(l > r) std::swap(l,r);62 if(op == 0) modify(1,l,r);63 else std::cout << query(1,l,r).dat << '\n';64 }65}
权值线段树
对权值作为维护对象而开的线段树,每个点存的是区间内对应数字的某种值(如出现次数等),操作跟线段树类似。
诺题目允许离线,则离散化后空间复杂度为O(N),否则需要开到O(max_val)的大小。
P3369 【模板】普通平衡树 - 洛谷 (luogu.com.cn)
与权值树状数组实现类似。
xxxxxxxxxx1011234using namespace std;5const int N = 100005;6int op[N],a[N];7vector<int>hs;8
9struct st{10 int l,r;11 int dat;12}t[N<<2];13
14void pushup(int p){15 t[p].dat = t[p<<1].dat + t[p<<1|1].dat;16}17
18void build(int p,int l,int r){19 t[p] = {l,r};20 if(l == r){21 t[p].dat = 0;22 return;23 }24 int mid = l + r >> 1;25 build(p<<1,l,mid);build(p<<1|1,mid+1,r);26 pushup(p);27}28
29void modify(int p,int l,int r,int x){//单点修改30 if(l <= t[p].l && r >= t[p].r){31 if(t[p].dat + x >= 0) t[p].dat += x;32 return;33 }34 int mid = t[p].l + t[p].r >> 1;35 if(l <= mid) modify(p<<1,l,r,x);36 if(r > mid) modify(p<<1|1,l,r,x);37 pushup(p);38}39
40
41int rnk(int p,int l,int r){//查询区间[l,r]的和42 if(l <= t[p].l && r >= t[p].r){43 return t[p].dat;44 }45 int mid = t[p].l + t[p].r >> 1;46 int ans = 0;47 if(l <= mid) ans += rnk(p<<1,l,r);48 if(r > mid) ans += rnk(p<<1|1,l,r);49 return ans;50}51
52int kth(int p,int k){//线段树上二分,查询全局第k小的数的下标(只能查全局)53 if(t[p].l == t[p].r){54 return t[p].l;55 }56 if(k <= t[p<<1].dat) return kth(p<<1,k);57 else return kth(p<<1|1,k-t[p<<1].dat);58}59
60int main(){61 int q;cin >> q;62 for(int i = 1;i <= q;i++){63 cin >> op[i] >> a[i];64 if(op[i] == 4) continue;65 hs.emplace_back(a[i]);66 }67 68 hs.emplace_back(-1e9);69 sort(hs.begin(),hs.end());70 hs.erase(unique(hs.begin(),hs.end()),hs.end());71 int m = hs.size()-1;72 for(int i = 1;i <= q;i++){73 if(op[i] == 4) continue;74 a[i] = lower_bound(hs.begin(),hs.end(),a[i])-hs.begin();75 }76 77 build(1,1,m);78 for(int i = 1;i <= q;i++){79 if(op[i] == 1){//插入一个数x80 modify(1,a[i],a[i],1);81 }82 if(op[i] == 2){//删除一个数x(如有多个,只删除一个)83 modify(1,a[i],a[i],-1);84 }85 if(op[i] == 3){//a[i]的排名,即区间[1,a[i]-1]的数的个数再加186 if(a[i] == 1) {cout << 1 << '\n';continue;}87 cout << rnk(1,1,a[i]-1)+1 << '\n';88 }89 if(op[i] == 4){//查询全局第k小的数(下标从1开始)90 cout << hs[kth(1,a[i])] << '\n';91 }92 if(op[i] == 5){//查找x的前驱(诺不存在则返回x)93 int rk = rnk(1,1,a[i]-1);94 cout << hs[kth(1,rk)] << '\n';95 }96 if(op[i] == 6){//查找x的后继(诺不存在则返回x)97 int rk = rnk(1,1,a[i])+1;98 cout << hs[kth(1,rk)] << '\n';99 }100 }101}
霍夫曼树
从根结点到各叶结点的路径长度与相应叶节点权值的乘积之和称为 树的带权路径长度(Weighted Path Length of Tree,WPL)。
设
(也等于所有非叶子节点之和)对于给定一组具有确定权值的叶结点,可以构造出不同的树,其中,WPL 最小的树 称为 霍夫曼树(Huffman Tree)。霍夫曼树不唯一。
如果每个字符的 使用频率相等,那么等长编码无疑是空间效率最高的编码方法,而如果字符出现的频率不同,则可以让频率高的字符采用尽可能短的编码,频率低的字符采用尽可能长的编码,来构造出一种 不等长编码,从而获得更好的空间效率。
在设计不等长编码时,要考虑解码的唯一性,如果一组编码中任一编码都不是其他任何一个编码的前缀,那么称这组编码为 前缀编码,其保证了编码被解码时的唯一性。
霍夫曼树可用于构造 最短的前缀编码,即 霍夫曼编码(Huffman Code)
不建树直接求n个节点k叉树的最小WPL
让n满足(n-1)%(k-1) == 0,否则不断填0,n++ 建立小根堆,每次取堆顶的k个求和s,再将s入堆,直到小根堆只剩一个元素
xxxxxxxxxx29123using namespace std;4priority_queue<int,vector<int>,greater<int>>pq;5
6int main(){7 int n,k;cin >> n >> k;8 for(int i = 1;i <= n;i++){9 int x;cin >> x;10 pq.push(x);11 }12 while((n-1)%(k-1) != 0){13 pq.push(0);14 n++;15 }16
17 int ans = 0;18 while(pq.size() > 1){19 int sum = 0;20 for(int i = 0;i < k;i++){21 int x = pq.top();22 pq.pop();23 sum += x;24 }25 pq.push(sum);26 ans += sum;27 }28 cout << ans;29}
xxxxxxxxxx521//合并果子加强版:https://www.luogu.com.cn/problem/P60332//简化版为n个节点2叉树的WPL;n<=1e53//加强版n<=1e7,但a[i]比较小,可以用O(N)的桶排4//使用两个queue代替优先队列(保证单调性)567using namespace std;8using ll = long long;9const int N = 100005;10int a[N],idx;11queue<ll>q1,q2;//q1为排序后的原数组,q2为合并的果子数组,可以证明q2为单调递增12
13int read(){14 int x = 0,f = 1;15 char ch = getchar();16 while(!isdigit(ch)){ if(ch == '-') f = -1; ch = getchar(); }17 while(isdigit(ch)){ x = x*10 + ch - '0'; ch = getchar(); }18 return x*f;19}20
21ll get_top(){22 ll x;23 if(q2.empty() || (q1.size() && q1.front() < q2.front())){24 x = q1.front(); q1.pop();25 }26 else{27 x = q2.front(); q2.pop();28 }29 return x;30}31
32int main(){33 int n;cin >> n;34 for(int i = 1;i <= n;i++){35 int x = read();36 a[x]++;//直接使用O(N)的桶排37 }38 for(int i = 1;i < N;i++){39 for(int j = 0;j < a[i];j++){40 q1.emplace(i);41 }42 }43
44 ll ans = 0;45 while(q1.size() + q2.size() > 1){46 ll x = get_top();//每次从q1和q2中选最小的两个取出47 ll y = get_top();48 ans += x+y;49 q2.push(x+y);//累加到答案后,加入q2队尾50 }51 cout << ans;52}
求最短WPL,并且要让最长的霍夫曼编码最短
只需在合并时,对于权值相同的节点,优先考虑当前深度最小(已合并次数最少)的进行合并即可
两种霍夫曼树WPL均为12,但右图最长编码更小
xxxxxxxxxx35123using namespace std;4using ll = long long;5using pii = pair<ll,ll>;6int n,k;7priority_queue<pii,vector<pii>,greater<pii>>pq;8ll ans;9
10int main(){11 cin >> n >> k;12 for(int i = 1;i <= n;i++){13 ll x;cin >> x;14 pq.push({x,0});15 }16
17 while((n-1)%(k-1) != 0){18 pq.push({0,0});19 n++;20 }21
22 while(pq.size() > 1){23 ll sum = 0;24 ll dep = 0;25 for(int i = 0;i < k;i++){26 auto [x,y] = pq.top();27 pq.pop();28 sum += x;29 dep = max(dep,y);30 }31 pq.push({sum,dep+1});32 ans += sum;33 }34 cout << ans << endl << pq.top().second << endl;35}
字符串
string s = “ABCDE”
子串(substring):s[i]~s[j] 连续的一段,如:BCD 子序列(subsequence):s[i]~s[j] 将若干元素提取出来并不改变相对位置形成的序列,如:ACD
前缀s[0~i]:A、AB、ABC、ABCD、ABCDE 真前缀:不包括本身的前缀 A、AB、ABC、ABCD
后缀s[i~n-1]:E、DE、CDE、BCDE、ABCDE 真后缀:不包括本身的后缀 E、DE、CDE、BCDE
回文串 :是正着写和倒着写相同的字符串,如aba、aa、abcba
字典序:以第i个字符作为第i关键字进行大小比较,空字符小于字符集内任何字符
如:abc < adc、a < aa、abcd < adcd
border:字符串中真前缀和真后缀相等的一段: 如f[a] = 0、f[aba] = a、f[abab] = ab、f[ababa] = a,aba、f[aaaa] = a,aa,aaa
字符串哈希
只能匹配子串,不能匹配子序列
全称字符串前缀哈希法,把字符串变成一个p进制数字(哈希值),实现不同的字符串映射到不同的数字。 对形如
的字符串, 采用字符的ascii码乘上 P 的次方来计算哈希值。 映射公式: 注意点: 1.任意字符不可以映射成0,否则会出现不同的字符串都映射成0的情况,比如A, AA, AAA皆为0 2.冲突问题:通过巧妙设置底数P(131或 13331),模数[Q(2^64)][ull,自然溢出]的值,减少冲突
问题是比较不同区间的子串是否相同,就转化为对应的哈希值是否相同。 求一个字符串的哈希值就相当于求前缀和,求一个字符串的子串哈希值就相当于求部分和。
前缀和公式
,h为前缀和数组,s为字符串数组 区间和公式 区间和公式的理解:ABCDE 与 ABC 的前三个字符值是一样,只差两位, 乘上 P² 把 ABC 变为 ABC00,再用 ABCDE-ABC00得到 DE 的哈希值。
xxxxxxxxxx761234
5namespace Hash{6 const unsigned long long base = 131,mod = 1e9+7;7 std::vector<unsigned long long>p(1,1),p2(1,1);//p[]建议开定长数组提前预处理8
9 template<typename T>10 struct hx{11 std::vector<unsigned long long>h;12
13 hx(){}14 hx(const T &s){15 init(s.size());16 h = std::vector<unsigned long long>(s.size()+1);17 for(int i = 0;i < s.size();i++){18 h[i+1] = h[i] * base + s[i];19 }20 }21 unsigned long long query(int l,int r){22 return h[r+1] - h[l]*p[r-l+1];23 }24
25 void init(int id){26 while(p.size() <= id){27 p.emplace_back(p.back()*base);28 }29 }30 };31
32 template<typename T>33 struct hx2{34 std::vector<unsigned long long>h,h2;35
36 hx2(){}37 hx2(const T &s){38 int n = s.size();39 init(s.size());40 h.resize(n+1);h2.resize(n+1);41 for(int i = 0;i < s.size();i++){42 h[i+1] = h[i]*base + s[i];43 h2[i+1] = (h2[i]*base + s[i]) % mod;44 }45 }46
47 std::pair<unsigned long long,unsigned long long> query(int l,int r){48 unsigned long long k1 = h[r+1] - h[l]*p[r-l+1];49 unsigned long long k2 = (h2[r+1] - h2[l]*p2[r-l+1]%mod + mod) % mod;50 return std::pair{k1,k2};51 }52
53 void init(int id){54 while(p.size() <= id){55 p.emplace_back(p.back()*base);56 p2.emplace_back(p2.back()*base%mod);57 }58 }59 };60};61using Hash::hx,Hash::hx2;62
63int main(){64 std::ios::sync_with_stdio(false); std::cin.tie(0);65 int n,q; std::cin >> n >> q;66 std::string s; std::cin >> s;67 s = ' ' + s;68 hx hs(s);69 while(q--){70 int l1,r1,l2,r2; std::cin >> l1 >> r1 >> l2 >> r2;71 if(hs.query(l1,r1) == hs.query(l2,r2)) {72 std::cout << "Yes\n";73 }74 else std::cout << "No\n";75 }76}
xxxxxxxxxx91//简单求整个字符串的哈希值2const ull mod = 212370440130137957;3ull hs(string &s){ // 0_idx4 ull ans = 0;5 for(auto &x:s){6 ans = (ans*131+x)%mod;7 }8 return ans;9}
双哈希
单哈希如大模数哈希/自然溢出哈希仍然可能会被hack 使用两个模数进行哈希得出两个不同哈希值,减少冲突概率
xxxxxxxxxx71//常用模数取值21e9+73ull自然溢出421237044013013795751111111111111111111 //19个16(1LL << 31) - 17998244353
xxxxxxxxxx601//字符串哈希 数据加强版 https://www.luogu.com.cn/problem/U4612112//诺TLE,则尽量开固定数组且预处理p[]和p2[],减小常数3//query(l,r)返回子串s[l~r]的哈希值4namespace Hash{5 const unsigned long long base = 131,mod = 1e9+7;6 std::vector<unsigned long long>p(1,1),p2(1,1);7
8 template<typename T>9 struct hx{ //单哈希 hx hs(s);10 std::vector<unsigned long long>h;11
12 hx(){}13 hx(const T &s){14 init(s.size());15 h = std::vector<unsigned long long>(s.size()+1);16 for(int i = 0;i < s.size();i++){17 h[i+1] = h[i] * base + s[i];18 }19 }20 unsigned long long query(int l,int r){21 return h[r+1] - h[l]*p[r-l+1];22 }23
24 void init(int id){25 while(p.size() <= id){26 p.emplace_back(p.back()*base);27 }28 }29 };30
31 template<typename T>32 struct hx2{//双哈希 hx2 hs(s);33 std::vector<unsigned long long>h,h2;34
35 hx2(){}36 hx2(const T &s){37 int n = s.size();38 init(s.size());39 h.resize(n+1);h2.resize(n+1);40 for(int i = 0;i < s.size();i++){41 h[i+1] = h[i]*base + s[i];42 h2[i+1] = (h2[i]*base + s[i]) % mod;43 }44 }45
46 std::pair<unsigned long long,unsigned long long> query(int l,int r){47 unsigned long long k1 = h[r+1] - h[l]*p[r-l+1];48 unsigned long long k2 = (h2[r+1] - h2[l]*p2[r-l+1]%mod + mod) % mod;49 return std::pair{k1,k2};50 }51
52 void init(int id){53 while(p.size() <= id){54 p.emplace_back(p.back()*base);55 p2.emplace_back(p2.back()*base%mod);56 }57 }58 };59};60using Hash::hx,Hash::hx2;
KMP
一个模式串匹配一个/多个文本串
能够在线性时间内判定字符串 A[1~N] 是否为字符串 B[1~M] 的子串,并求出字符串A在字符串B中各次出现的位置。能比字符串哈希更高效准确地处理这个问题,并且能提供一些额外的信息
next[i]数组求的是子串s[1~i]的最长border(真前缀==真后缀)
xxxxxxxxxx261//下标从1开始23using namespace std;4const int N = 1000006;5int ne[N],f[N];6
7int main(){8 string a,b;9 int n,m;10 cin >> n >> a >> m >> b;11 a = ' ' + a;b = ' ' +b;12 13 ne[0] = 0;14 for(int i = 2,j = 0;i <= n;i++){//自身匹配,i从2开始15 while(j > 0 && a[i] != a[j+1]) j = ne[j];16 if(a[i] == a[j+1]) j++;17 ne[i] = j;18 }19 20 for(int i = 1,j = 0;i <= m;i++){//对目标字符串匹配,求a在b的所有出现下标21 while(j > 0 && b[i] != a[j+1]) j = ne[j];22 if(b[i] == a[j+1]) j++;23 f[i] = j;//f[i]表示文本串以b[i]结尾的子串的后缀和模式串的前缀的最长匹配长度24 if(f[i] == n){ cout << i - n << ' '; }25 }26}xxxxxxxxxx531//https://vjudge.net/problem/HDU-1711234
5template<typename T>6struct KMP{7 T a;8 std::vector<int> ne;9
10 KMP(){}11 KMP(const T&v){ init(v); }12
13 void init(const T&v){//1_idx14 a = v;15 ne.assign(a.size(),{});16 for(int i = 2,j = 0;i < a.size();i++){17 while(j > 0 && a[i] != a[j+1]) j = ne[j];18 if(a[i] == a[j+1]) j++;19 ne[i] = j;20 }21 }22
23 std::vector<int> kmp(const T&b){24 std::vector<int>ans;25 for(int i = 1,j = 0;i < b.size();i++){26 while(j > 0 && b[i] != a[j+1]) j = ne[j];27 if(b[i] == a[j+1]) j++;28 //f[i] = j;29 if(j == a.size()-1) ans.push_back(i-a.size()+2);//此处再令j=0,即为求不可重复的匹配30 }31 return ans;32 }33};34
35
36void sol(){37 int n,m; std::cin >> n >> m;38 std::vector<int>a(n+1),b(m+1);39 for(int i = 1;i <= n;i++){ std::cin >> a[i]; }40 for(int i = 1;i <= m;i++){ std::cin >> b[i]; }41
42 KMP<std::vector<int>> kmp(b);43
44 auto ans = kmp.kmp(a);//求a中出现模版串的所有下标45 if(ans.size()) std::cout << ans[0] << '\n';46 else std::cout << -1 << '\n';47}48
49int main(){50 std::ios::sync_with_stdio(false);std::cin.tie(0);51 int t; std::cin >> t;52 while(t--) sol();53}
前缀统计
统计每个前缀的出现次数
string s = abab; f(s) = a* 2 + ab*2 + aba + abab = 6
xxxxxxxxxx611//原题数据较弱 https://acm.hdu.edu.cn/showproblem.php?pid=3336234
5template<typename T>6struct KMP{ //1_idx7 T a;8 std::vector<int> ne;9
10 KMP(){}11 KMP(const T&v){ init(v); }12
13 void init(const T&v){14 a = v;15 ne.assign(a.size(),{});16 for(int i = 2,j = 0;i < a.size();i++){17 while(j > 0 && a[i] != a[j+1]) j = ne[j];18 if(a[i] == a[j+1]) j++;19 ne[i] = j;20 }21 }22
23 std::vector<int> kmp(const T&b){24 std::vector<int>ans;25 for(int i = 1,j = 0;i < b.size();i++){26 while(j > 0 && b[i] != a[j+1]) j = ne[j];27 if(b[i] == a[j+1]) j++;28 //f[i] = j;29 if(j == a.size()-1) ans.push_back(i-a.size()+2);30 }31 return ans;32 }33};34
35const int mod = 10007;36
37void sol(){38 int n; std::cin >> n;39 std::string s; std::cin >> s;40 s = ' ' + s;41 KMP<std::string>t(s);42
43 auto ne = t.ne;44
45 std::vector<int>f(n+1);46 for(int i = 1;i <= n;i++) {47 f[i] = f[ne[i]] + 1;48 }49
50 int ans = 0;51 for(int i = 1;i <= n;i++) {52 ans = (ans + f[i]) % mod;53 }54 std::cout << ans << '\n';55}56
57int main(){58 std::ios::sync_with_stdio(false); std::cin.tie(0);59 int t; std::cin >> t;60 while(t--) sol();61}
循环节
字符串的循环节
诺字符串S可以由子串A循环而成,则子串A是S的周期,如S = “abababab”,则周期=ab 、abab
如果(ne[i] > 0 && i%[i-ne[i]] == 0),则前缀s[1~i]的最小循环节k = i-ne[i]
xxxxxxxxxx271//https://www.acwing.com/problem/content/143/2//给定字符串s,求具有循环节的前缀长度i和其最小循环节对应的循环次数34using namespace std;5const int N = 1000006;6int ne[N];7
8int main(){9 int n,idx = 0;;10 while(cin >> n,n){11 cout << "Test case #" << ++idx << endl;12 string s;cin >> s;13 s = ' ' + s;14 for(int i = 2,j = 0;i <= n;i++){15 while(j > 0 && s[i] != s[j+1]) j = ne[j];16 if(s[i] == s[j+1]) j++;17 ne[i] = j;18 }19
20 for(int i = 1;i <= n;i++){21 if(ne[i] && i%(i-ne[i]) == 0){22 cout << i << ' ' << i/(i-ne[i]) << endl;//子串s[1~i]的最短循环节循环的次数23 }24 }25 cout << endl;26 }27}
最长公共子串
求多个字符串的最长公共子串
二分+KMP
xxxxxxxxxx931//https://vjudge.net/problem/HDU-23282//求长公共子串,诺不唯一,输出字典序最小的3456
7template<typename T>8struct KMP{ //1_idx9 T a;10 std::vector<int> ne;11
12 KMP(){}13 KMP(const T&v){ init(v); }14
15 void init(const T&v){16 a = v;17 ne.assign(a.size(),{});18 for(int i = 2,j = 0;i < a.size();i++){19 while(j > 0 && a[i] != a[j+1]) j = ne[j];20 if(a[i] == a[j+1]) j++;21 ne[i] = j;22 }23 }24
25 std::vector<int> kmp(const T&b){26 std::vector<int>ans;27 for(int i = 1,j = 0;i < b.size();i++){28 while(j > 0 && b[i] != a[j+1]) j = ne[j];29 if(b[i] == a[j+1]) j++;30 //f[i] = j;31 if(j == a.size()-1) {32 ans.push_back(i-a.size()+2);33 return ans;//匹配成功一次直接返回即可34 }35 }36 return ans;37 }38};39
40const int N = 4003;41int n;42std::string s[N];43std::string ans;44
45bool check(int len){46 bool ok = 0;47 for(int i = 1;i+len-1 < s[1].size();i++){48 auto now = s[1].substr(i,len);49 if(ok && (ans <= now)) continue;//剪枝50
51 KMP<std::string>t(' ' + now);52 bool flag = 1;53 for(int j = 2;j <= n;j++){54 if(t.kmp(s[j]).empty()) {55 flag = 0;56 break;57 }58 }59 if(flag) {60 ok = 1;61 if(ans.size() < now.size()) ans = now;62 else if(ans.size() == now.size()) ans = std::min(ans,now);63 }64 }65 return ok;66}67
68void sol(){69 ans = "";70 int mn = N,p = 1;71 for(int i = 1;i <= n;i++) {72 std::cin >> s[i];73 if(s[i].size() < mn) {74 mn = s[i].size();75 p = i;76 }77 s[i] = ' ' + s[i];78 }79 std::swap(s[1],s[p]);80 int l = 0,r = mn;81 while(l < r){82 int mid = l + r + 1 >> 1;83 if(check(mid)) l = mid;84 else r = mid - 1;85 }86 if(ans.size()) std::cout << ans << '\n';87 else std::cout << "IDENTITY LOST\n";88}89
90int main(){91 std::ios::sync_with_stdio(false); std::cin.tie(0);92 while(std::cin >> n,n) sol();93}
exKMP
也被称之为
对于一个长度为n的字符串s(1_idx),定义z[i]表示s和s[i,n](即以s[i]开头的后缀)的最长公共前缀,我们将z称之为s的z函数。考虑到z[1]的特殊性质,一般将其设置为z[1]=0。 例如z("aaaba") = {0,2,1,0,1}。
时间复杂度
xxxxxxxxxx411template<typename T>2struct EXKMP{ //1_idx3 T a;4 std::vector<int>z;5
6 EXKMP(){}7 EXKMP(const T&v){ init(v); }8
9 void init(const T &v) {10 a = v;11 z = std::vector<int>(a.size());12 for(int i = 2,k = 1;i < a.size();i++){13 if(k+z[k]-i <= z[i-k+1]) {14 z[i] = k+z[k]-i;15 if(z[i] < 0) z[i] = 0;16 while(i+z[i] < a.size() && a[z[i]+1] == a[z[i]+i]) ++z[i];17 k = i;18 }19 else{20 z[i] = z[i-k+1];21 }22 }23 z[1] = 0; //or a.size()-1?24 }25
26 std::vector<int> exkmp(const T&v){//ans[i]表示v的后缀子串v.substr(i)与字符串a的最长公共前缀27 std::vector<int>ans(v.size());28 for(int i = 1,k = 1;i < v.size();i++){29 if(k+ans[k]-i <= z[i-k+1]) {30 ans[i] = k+ans[k]-i;31 if(ans[i] < 0) ans[i] = 0;32 while(i+ans[i] < v.size() && ans[i] < a.size() && a[ans[i]+1] == v[ans[i]+i]) ++ans[i];33 k = i;34 }35 else{36 ans[i] = z[i-k+1];37 }38 }39 return ans;40 }41};
Trie字典树
高效地存储和查找字符串集合的数据结构
字符串统计
xxxxxxxxxx61123
4namespace TRIE{//0_idx5 const int N = 100005,M = 26;//max_s.length6 int son[N][M],cnt[N],idx;7
8 struct Trie{9
10 Trie(){idx = 0;init(idx);}11
12 void init(int p){13 cnt[p] = 0;14 memset(son[p], 0, sizeof(son[p]));15 }16
17 int get(char x){//18 if(x >= 'a' && x <= 'z') return x - 'a';19 if(x >= 'A' && x <= 'Z') return x - 'A' + 26;20 if(x >= '0' && x <= '9') return x - '0' + 52;21 return -1;22 }23
24 void insert(const std::string &s){25 int p = 0;26 for(int i = 0;i < s.size();i++){27 int u = get(s[i]);28 if(!son[p][u]) {29 son[p][u] = ++idx;30 init(idx);31 }32 p = son[p][u];33 //cnt[p]++; //前缀++34 }35 cnt[p]++; //字符串++;36 }37
38 int query(const std::string &s){39 int p = 0;40 for(int i = 0;i < s.size();i++){41 int u = get(s[i]);42 if(!son[p][u]) return 0;43 p = son[p][u];44 }45 return cnt[p];46 }47 };48};49using TRIE::Trie;50
51
52int main(){53 Trie t;54 int q; std::cin >> q;55 while(q--){56 char op; std::string s;57 std::cin >> op >> s;58 if(op == 'I') { t.insert(s); }59 else { std::cout << t.query(s) << '\n'; }60 }61}
前缀统计
给定 N 个字符串
xxxxxxxxxx671//https://www.acwing.com/problem/content/144/234
5namespace TRIE{//0_idx6 const int N = 1000006,M = 26;//max_s.length7 int son[N][M],cnt[N],idx;8
9 struct Trie{10
11 Trie(){init();}12
13 void init(){14 idx = cnt[0] = 0;15 memset(son[0], 0, sizeof(son[0]));16 }17
18 int get(char x){19 if(x >= 'a' && x <= 'z') return x - 'a';20 if(x >= 'A' && x <= 'Z') return x - 'A' + 26;21 if(x >= '0' && x <= '9') return x - '0' + 52;22 return 0;23 }24
25 void insert(const std::string &s){26 int p = 0;27 for(int i = 0;i < s.size();i++){28 int u = get(s[i]);29 if(!son[p][u]) {30 son[p][u] = ++idx;31 cnt[idx] = 0;32 std::memset(son[idx],0,sizeof son[idx]);33 }34 p = son[p][u];35 //cnt[p]++; //suff++36 }37 cnt[p]++; //string++;38 }39
40 int query(const std::string &s){41 int ans = 0;42 int p = 0;43 for(int i = 0;i < s.size();i++){44 int u = get(s[i]);45 if(!son[p][u]) return ans;46 p = son[p][u];47 ans += cnt[p];48 }49// return cnt[p];50 return ans;51 }52 };53};54using TRIE::Trie;55
56int main(){57 Trie t;58 int n,q; std::cin >> n >> q;59 while(n--){60 std::string s; std::cin >> s;61 t.insert(s);62 }63 while(q--){64 std::string s; std::cin >> s;65 std::cout << t.query(s) << '\n';66 }67}
给定 N 个字符串
xxxxxxxxxx701//https://www.luogu.com.cn/problem/P8306234
5namespace TRIE{//0_idx6 const int N = 3000006,M= 70;//max_s.length7 int son[N][M],cnt[N],idx;8
9 struct Trie{10
11 Trie(){init();}12
13 void init(){14 cnt[0] = 0;15 memset(son[0], 0, sizeof(son[0]));16 idx = 0;17 }18
19 int get(char x){20 if(x >= 'a' && x <= 'z') return x - 'a';21 if(x >= 'A' && x <= 'Z') return x - 'A' + 26;22 if(x >= '0' && x <= '9') return x - '0' + 52;23 return 0;24 }25
26 void insert(const std::string &s){27 int p = 0;28 for(int i = 0;i < s.size();i++){29 int u = get(s[i]);30 if(!son[p][u]) {31 son[p][u] = ++idx;32 cnt[idx] = 0;33 std::memset(son[idx],0,sizeof son[idx]);34 }35 p = son[p][u];36 cnt[p]++;37 }38 }39
40 int query(const std::string &s){41 int p = 0;42 for(int i = 0;i < s.size();i++){43 int u = get(s[i]);44 if(!son[p][u]) return 0;45 p = son[p][u];46 }47 return cnt[p];48 }49 };50};51using TRIE::Trie;52
53void sol(){54 Trie t;55 int n,q; std::cin >> n >> q;56 while(n--){57 std::string s; std::cin >> s;58 t.insert(s);59 }60 while(q--){61 std::string s; std::cin >> s;62 std::cout << t.query(s) << '\n';63 }64}65
66int main(){67 std::ios::sync_with_stdio(false); std::cin.tie(0);68 int t; std::cin >> t;69 while(t--) sol();70}
最大异或对
在给定的 𝑁 个整数 𝐴1,𝐴2……𝐴𝑁 中选出两个进行 𝑥𝑜𝑟(异或)运算,得到的结果最大是多少?
xxxxxxxxxx691//https://www.luogu.com.cn/problem/P10471234
5namespace TRIE{//0_idx6 const int N = 100005*32;7 int son[N][2],cnt[N],idx;8
9 struct Trie{10
11 Trie(){init();}12
13 void init(){14 idx = cnt[0] = 0;15 memset(son[0], 0, sizeof(son[0]));16 }17
18 int get(char x){19 if(x >= 'a' && x <= 'z') return x - 'a';20 if(x >= 'A' && x <= 'Z') return x - 'A' + 26;21 if(x >= '0' && x <= '9') return x - '0' + 52;22 return 0;23 }24
25 void insert(const int &x){26 int p = 0;27 for(int i = 30;i >= 0;i--){28 bool u = x >> i & 1;29 if(!son[p][u]) {30 son[p][u] = ++idx;31 cnt[idx] = 0;32 std::memset(son[idx],0,sizeof son[idx]);33 }34 p = son[p][u];35 }36 cnt[p]++;37 }38
39 int query(const int &x){40 int ans = 0;41 int p = 0;42 for(int i = 30;i >= 0;i--){43 bool u = x >> i & 1;44 if(son[p][!u]) {//高位开始,每次找与当前位相反的45 ans = ans<<1|1;46 p = son[p][!u];47 }48 else{49 ans = ans<<1;50 p = son[p][u];51 }52 }53 return ans;54 }55 };56};57using TRIE::Trie;58
59int main(){60 Trie t;61 int n; std::cin >> n;62 int ans = 0;63 while(n--){64 int x; std::cin >> x;65 ans = std::max(ans,t.query(x));66 t.insert(x);67 }68 std::cout << ans;69}
给定一棵 n 个点的带边权树,求树中最大的异或路径值。
思路:设d[x]表示根节点到x的路径异或值,则有d[x] = d[father[x]] ^ weight(x,father[x]),DFS一遍可以求出所有d[i],x到y的路径异或值就等于d[x]^d[y] (x到根 和 y到根,重叠部分异或抵消了,就相当于x到y),问题就变为d[N]中求最大异或对
xxxxxxxxxx68123using namespace std;4const int N = 200005;5int n;6int a[N];7int h[N],ne[N],e[N],w[N],idx;8int son[N*32][2],tot;9int d[N];10
11void add(int a,int b,int c){12 w[idx] = c,e[idx] = b,ne[idx] = h[a],h[a] = idx++;13}14
15void dfs(int u,int fa){16 for(int i = h[u];~i;i = ne[i]){17 int k = e[i];18 if(k != fa){19 d[k] = d[u] ^ w[i];20 dfs(k,u);21 }22 }23}24
25void insert(int x){26 int p = 0;27 for(int i = 31;i >= 0;i--){28 bool u = x >> i & 1;29 if(!son[p][u]) son[p][u] = ++tot;30 p = son[p][u];31 }32}33
34int query(int x){35 int p = 0;36 int ans = 0;37 for(int i = 31;i >= 0;i--){38 bool u = x >> i & 1;39 if(son[p][!u]) {40 ans = ans*2 + 1;41 p = son[p][!u];42 }43 else{44 ans = ans*2;45 p = son[p][u];46 }47 }48 return ans;49}50
51int main(){52 memset(h,-1,sizeof h);53 cin >> n;54 for(int i = 1;i < n;i++){55 int a,b,c;cin >> a >> b >> c;56 add(a,b,c);57 add(b,a,c);58 }59
60 dfs(1,1);61
62 int ans = 0;63 for(int i = 1;i <= n;i++){64 ans = max(ans,query(d[i]));65 insert(d[i]);66 }67 cout << ans;68}
AC自动机
多个模式串匹配一个/多个文本串
构建Trie前缀树
构建失配指针:状态
的 faile指针 指向另一个状态 ,其中 ,且 是 的最长后缀(即在诺干个后缀状态中取最长的一个作为 fail指针)。查询文本串:例如,当前要匹配的文本串为
shers,在字典树上找到状态9:she时,此时无后续节点,失配,直接跳转到状态9:she的最长后缀状态2:he,然后继续匹配。
xxxxxxxxxx931//模版题 AC自动机(简单版) https://www.luogu.com.cn/problem/P38082//给定n个模式串和一个文本串,求文本串中出现了多少个模式串3456
7namespace ACAM{//0_idx8 const int N = 1000006, M = 26;//N:所有模式串的总长度9 int son[N][M],cnt[N],fail[N],idx;10
11 struct Trie{12
13 Trie(){idx = 0;init(idx);}14
15 void init(int p){16 fail[p] = cnt[p] = 0;17 std::memset(son[p], 0, sizeof(son[p]));18 }19
20 int get(char x){21 if(x >= 'a' && x <= 'z') return x - 'a';22 if(x >= 'A' && x <= 'Z') return x - 'A' + 26;23 if(x >= '0' && x <= '9') return x - '0' + 52;24 return -1;25 }26
27 void insert(const std::string &s){28 int p = 0;29 for(int i = 0;i < s.size();i++){30 int u = get(s[i]);31 if(!son[p][u]) {32 son[p][u] = ++idx;33 init(idx);34 }35 p = son[p][u];36 }37 cnt[p]++;38 }39
40 void get_fail(){//构建失配指针41 fail[0] = 0;42 std::queue<int>q;43 for(int i = 0;i < M;i++){//第二层的fail全部指向根节点44 if(son[0][i]) {45 fail[son[0][i]] = 0;46 q.push(son[0][i]);47 }48 }49 while(q.size()){50 int p = q.front();51 q.pop();52 for(int u = 0;u < M;u++){53 if(son[p][u]) {//如果子节点存在54 fail[son[p][u]] = son[fail[p]][u];//子节点的fail指针指向当前节点的fail指针指向的相同子节点55 q.push(son[p][u]);//子节点入队56 }57 else {//如果子节点不存在58 son[p][u] = son[fail[p]][u];//直接让子节点指向当前节点的fail指针指向的相同子节点59 }60 }61 }62 }63
64 int query(const std::string &s){65 int p = 0,ans = 0;66 for(int i = 0;i < s.size();i++){67 int u = get(s[i]);68 p = son[p][u];//依次读入单词,然后指针跳转到子节点69 ans += cnt[p];70 cnt[p] = 0;71// for(int t = p;t && cnt[t] != -1;t = fail[t]){//不断跳失配指针并沿路统计答案72// ans += cnt[t];73// cnt[t] = -1;//清空,本题出现多次只算一次,诺多文本串匹配,可以开个vis数组标记状态t74// }75 }76 return ans;77 }78 };79};80using ACAM::Trie; //Trie t;81
82int main(){83 Trie t;84 int n; std::cin >> n;85 std::string s;86 for(int i = 1;i <= n;i++){87 std::cin >> s;88 t.insert(s);89 }90 t.get_fail();91 std::cin >> s;92 std::cout << t.query(s) << '\n';93}
拓扑排序优化
fail 指针的一个性质:一个 AC 自动机中,如果只保留 fail 边,那么剩余的图一定是一棵树。这样 AC 自动机的匹配就可以转化为在 fail 树上的链求和问题
观察到时间主要浪费在在每次都要跳 fail。如果我们可以预先记录,最后一并求和,那么效率就会优化。
于是我们按照 fail 树,做一次内向树上的拓扑排序,就能一次性求出所有模式串的出现次数。
xxxxxxxxxx1181//https://www.luogu.com.cn/problem/P53572//给定n个模式串和一个文本串,分别求出每个模式串在文本串中出现的次数(可重叠)34using namespace std;5
6int id;7std::vector<int>vis,mp;8
9namespace ACAM{//0_idx10 const int N = 200005, M = 26;11 int son[N][M],cnt[N],fail[N],idx;12 int du[N],ans[N];13
14 struct Trie{15
16 Trie(){idx = 0;init(idx);}17
18 void init(int p){19 ans[p] = du[p] = 0;20 fail[p] = cnt[p] = 0;21 std::memset(son[p], 0, sizeof(son[p]));22 }23
24 int get(char x){25 if(x >= 'a' && x <= 'z') return x - 'a';26 if(x >= 'A' && x <= 'Z') return x - 'A' + 26;27 if(x >= '0' && x <= '9') return x - '0' + 52;28 return -1;29 }30
31 void insert(const std::string &s){32 int p = 0;33 for(int i = 0;i < s.size();i++){34 int u = get(s[i]);35 if(!son[p][u]) {36 son[p][u] = ++idx;37 init(idx);38 }39 p = son[p][u];40 }41 if(!cnt[p]) cnt[p] = id;//状态p对应字符串id+去重42 mp[id] = cnt[p];//字符串id对应状态p的id43 }44
45 void get_fail(){46 std::queue<int>q;47 for(int i = 0;i < M;i++){48 if(son[0][i]) {49 fail[son[0][i]] = 0;50 q.push(son[0][i]);51 }52 }53 while(q.size()){54 int p = q.front();55 q.pop();56 for(int u = 0;u < M;u++){57 if(son[p][u]) {58 fail[son[p][u]] = son[fail[p]][u];59 du[son[fail[p]][u]]++;//额外记录入度60 q.push(son[p][u]);61 }62 else {63 son[p][u] = son[fail[p]][u];64 }65 }66 }67 }68
69 int query(const std::string &s){70 int p = 0;71 for(int i = 0;i < s.size();i++){72 int u = get(s[i]);73 p = son[p][u];74 ans[p]++;75 }76 topu();77 return 0;78 }79 void topu(){//拓扑排序统计答案80 std::queue<int>q;81 for(int i = 1;i <= idx;i++){82 if(!du[i]) q.push(i);//入度为零的点,必定是一个 Fail 链的末尾83 }84 while(q.size()){85 int p = q.front();86 q.pop();87 vis[cnt[p]] = ans[p];88
89 ans[fail[p]] += ans[p];90 if(!--du[fail[p]]) q.push(fail[p]);91 }92 }93 };94};95using ACAM::Trie; //Trie t;96
97const int N = 200005;98std::string s[N];99
100int main(){101 Trie t;102 int n; std::cin >> n;103 vis = mp = std::vector<int>(n+1,0);104
105 for(int i = 1;i <= n;i++){106 std::cin >> s[i];107 id = i;108 t.insert(s[i]);109 }110 t.get_fail();111
112 std::cin >> s[0];113 t.query(s[0]);114
115 for(int i = 1;i <= n;i++){116 std::cout << vis[mp[i]] << '\n';117 }118}
回文串
Manacher
O(N)求得对于每个i的最长回文长度,这里下标从0开始
t = "abbba" 则对应s = " #a#b#b#b#a#",可以同时处理奇偶回文串,而不必分开讨论,对s求得d1[ ]
d1[i]为以s[i]为中心的最长回文串长度。
对于t[i]来说,d1[i*2]-1 即为以i为中心,最长的奇数长度回文串
d1[i*2+1]-1即为以(i,i+1)中点为中心最长的偶数长度回文串
xxxxxxxxxx461//https://www.luogu.com.cn/problem/P38052//模版题,给定字符串,求最长的回文子串长度3template<typename T>4struct Manacher{ //1_idx5 int n,ans;6 T s;7 std::vector<int>d;8
9 Manacher(){}10 Manacher(const T &v){11 init(v);12 }13
14 void init(const T &v){15 s = " #";16 for(int i = 1;i < v.size();i++){17 s += v[i];18 s += '#';19 }20 n = s.size()-1;21 d = std::vector<int>(n+1,0);22
23 ans = 0;24 for(int i = 1,l = 0,r = 0;i <= n;i++){25 if(i > r) d[i] = 1;26 else d[i] = std::min(d[l*2-i],r-i+1);27 while(i-d[i] >= 1 && i+d[i] <= n && s[i-d[i]] == s[i+d[i]]) d[i]++;28 if(i+d[i]-1 > r) l = i,r = i+d[i]-1;29 ans = std::max(ans,d[i]-1); //减去加入的'#'30 }31 }32
33 bool query(int l,int r){ //查询区间是否为回文串34 l <<= 1,r <<= 1;35 int mid = l + r >> 1;36 return d[mid]-1 >= r-mid;37 }38};39
40int main(){41 std::string s;42 std::cin >> s;43
44 Manacher t(s);45 std::cout << t.ans;46}
最小表示法
求s的循环同构中字典序最小,且下标最小的一个
时间复杂度
xxxxxxxxxx391//https://www.luogu.com.cn/problem/P1368234
5template<typename T>6int get_min(T a){//1_idx7 int n = a.size()-1;8 for(int i = 1;i <= n;i++) a.push_back(a[i]);9 int i = 1,j = 2;10 while(i <= n && j <= n){11 int k = 0;12 while(k < n && a[i+k] == a[j+k]) k++;13 if(k == n) break;14 if(a[i+k] > a[j+k]) {//诺将符号取反即为最大表示法15 i += k+1;//诺a[i+k]>a[j+k],则a[i]~a[i+k]>a[j]16 if(i == j) i++;//保持i != j17 }18 else {//a[j]同理19 j += k+1;20 if(i == j) j++;21 }22 }23 return std::min(i,j);24}25
26
27int main(){28 int n; std::cin >> n;29 std::vector<int>a(n+1);30 for(int i = 1;i <= n;i++){31 scanf("%d",&a[i]);32 }33 34 int id = get_min(a);35 36 for(int i = 0;i < n;i++){37 printf("%d ",a[(id+i-1)%n+1]);38 }39}xxxxxxxxxx31//https://acm.hdu.edu.cn/showproblem.php?pid=26092//诺两个字符串具有的循环同构,则称两个字符串本质相同,求本质不同的字符串的有多少种3//思路:将每个字符串的最小表示法用set/map存起来,最后容器的大小即为答案
后缀数组
这里假定字符串长度为n,下标从1开始。“后缀i”表示以第i个字符开头的后缀,储存时用i表示字符串s的后缀s[i...n]。
后缀数组sa[i]表示将所有后缀排序后,第i小的后缀的编号。
排名数组rk[i]表示后缀i的排名,重要的辅助数组。
这两个数组满足性质:sa[rk[i]] = rk[sa[i]] = i。
height数组height[i],即lcp(sa[i],sa[i-1])
后缀数组示例
P10469 后缀数组 - 洛谷 (luogu.com.cn)
排序+哈希+二分
时间复杂度
xxxxxxxxxx56123using namespace std;4const unsigned long long base = 131;5const int N = 300005;6int n;7string s;8int sa[N],rk[N];9unsigned long long hs[N],p[N];10
11void init(string &s){12 hs[0] = 0;13 p[0] = 1;14 for(int i = 0;i < s.size();i++){15 p[i+1] = p[i] * base;16 hs[i+1] = hs[i] * base + s[i];17 }18}19
20unsigned long long query(int l,int r){21 return hs[r] - hs[l-1]*p[r-l+1];22}23
24int lcp(int x,int y){//二分查找后缀x和后缀y的最长公共前缀25 int l = 0,r = min(n-x+1,n-y+1);26 while(l < r){27 int mid = l + r + 1 >> 1;28 if(query(x,x+mid-1) == query(y,y+mid-1)) l = mid;29 else r = mid - 1;30 }31 return l;32}33
34bool cmp(int x,int y){35 int len = lcp(x,y);36 return s[x+len] < s[y+len];//s[x+len]和s[y+len]即为第一个不同的位置37}38
39int main(){40 cin >> s; n = s.size();41 init(s);42 s = ' ' + s;43
44 for(int i = 1;i <= n;i++) sa[i] = i;45
46 stable_sort(sa+1,sa+n+1,cmp);47 48 for(int i = 1;i <= n;i++){49 cout << sa[i] - 1 << ' '; //sa[] 本题下标从0开始,减1即可50 rk[sa[i]] = i;//rk[]51 }52 cout << '\n';53 for(int i = 1;i <= n;i++){54 cout << lcp(sa[i],sa[i-1]) << ' ';//height[]55 }56}
倍增实现
时间复杂度
xxxxxxxxxx721//https://www.luogu.com.cn/problem/P1046923
4namespace SA{//1_idx 未验证多测是否正确初始化5 int n;6 std::vector<int>sa,c,x,y,rk,height;7
8 template<typename T>9 void build (const T &s) {//get:sa[]10 sa = x = y = std::vector<int>(n+1);11 int m = 300;//字符值域12 c.resize(std::max(n,m)+1);13 for (int i = 1; i <= m; i++) c[i] = 0;14 for (int i = 1; i <= n; i++) c[x[i] = s[i]]++;15 for (int i = 1; i <= m; i++) c[i] += c[i-1];16 for (int i = n; i >= 1; i--) sa[c[x[i]]--] = i;17 for (int j = 1; j <= n; j <<= 1) {18 int p = 0;19 for (int i = n - j + 1; i <= n; i ++) y[++p] = i; 20 for (int i = 1; i <= n; i++) if (sa[i] > j) y[++p] = sa[i] - j;21 for (int i = 1; i <= m; i++) c[i] = 0;22 for (int i = 1; i <= n; i++) c[x[y[i]]]++;23 for (int i = 1; i <= m; i++) c[i] += c[i-1];24 for (int i = n; i >= 1; i--) sa[c[x[y[i]]]--] = y[i];25 std::swap (x, y);26 p = 1;27 x[sa[1]] = 1;28 for (int i = 2; i <= n; i ++) {29 x[sa[i]] = y[sa[i-1]] == y[sa[i]] && y[sa[i-1]+j] == y[sa[i]+j] ? p : ++p;30 }31 if (p >= n) break;32 m = p;33 }34 }35
36 template<typename T>37 void make (const T &s) {//get:rk[],height[]38 rk = height = std::vector<int>(n+1);39 int k = 0;40 for (int i = 1; i <= n; i ++) rk[sa[i]] = i;41 for (int i = 1; i <= n; i ++) {42 if (rk[i] == 1) continue;43 if (k) k --;44 int j = sa[rk[i] - 1];45 while (j + k <= n and i + k <= n and s[i + k] == s[j + k]) {46 ++ k;47 }48 height[rk[i]] = k;49 }50 }51
52 template<typename T>53 std::vector<int> get_sa(const T &s){54 n = s.size()-1;55 build(s);56 make(s);57 return sa;58 }59}60using SA::get_sa,SA::sa,SA::rk,SA::height;61
62
63int main () {64 std::string s; std::cin >> s; s = ' ' + s;65 int n = s.size()-1;66
67 get_sa(s);68
69 for(int i = 1;i <= n;i++){ std::cout << sa[i]-1 << ' '; }70 std::cout << '\n';71 for(int i = 1;i <= n;i++){ std::cout << height[i] << ' '; } 72}
height数组
lcp为最长公共前缀,下文以lcp(i,j)表示后缀i和后缀j的最长公共前缀。
height[ ]数组的定义:height[i] = lcp(sa[i],sa[i-1]),即第i名的后缀与它前一名的后缀的最长公共前缀。height[1]可以视作0。
一些应用
两子串最长公共前缀
lcp(sa[i],sa[j]) = min({height[i+1...j]})如果
height一直大于某个数,前这么多位就一直没变过;反之,由于后缀已经排好序了,不可能之后变回来。 求两子串的最长公共前缀就转化为了RMQ问题。
比较两子串的大小关系 假设比较的子串为
A = s[a,b]和B = s[c,d]若
, 。 否则, 。
求字符串循环同构的字典序排名
[P4051 JSOI2007] 字符加密 - 洛谷 (luogu.com.cn) 将s复制一遍后,求sa[ ]
求不同子串的数目
P2408 不同子串个数 - 洛谷 (luogu.com.cn) 每个子串一定是某个后缀的前缀,所以可以计算子串总数,枚举每个后缀,减掉重复。
求至少出现k次的子串(可重叠)的最大长度
[P2852 USACO06DEC] Milk Patterns G - 洛谷 (luogu.com.cn) 出现至少k次意味着后排序后,有至少连续k个后缀以这个子串作为公共前缀。 所以求出每相邻k-1个height的最小值,这些最小值的最大值即为答案,这里可以用单调队列O(n)解决
结合并查集/线段树/单调栈等数据结构
其它
求字符串S中长度为k的子序列中,字典序最小的一个。
贪心实现,依次考虑子序列的每一位,从合法区间[l,r]中选择字典序最小的一个字母,选择该字母后,该字母前面的字母都不能选,于是我们可以不断缩小合法区间。
xxxxxxxxxx331//https://vjudge.net/problem/AtCoder-typical90_f#author=GPT_zh234using namespace std;5int n,k;6string s;7queue<int>q[30];8
9int main(){10 cin >> n >> k >> s;11 for(int i = 0;i < s.size();i++){12 q[s[i]-'a'].emplace(i);13 }14
15 int l = 0;16 string ans;17 for(int i = 0;i < k;i++){18 int r = n - k + i;//右端点必须小于r,否则凑不齐长度k的序列19 for(int c = 0;c < 26;c++){20 while(q[c].size() && q[c].front() < l){//左端点l前面的字母都可以排除21 q[c].pop();22 }23 if(q[c].empty()) continue;24 if(q[c].front() <= r){//选择该位后,缩小左端点25 ans += 'a' + c;26 l = q[c].front();27 q[c].pop();28 break;29 }30 }31 }32 cout << ans;33}
给定一个长度为N的字符串S,求有多少个的子序列 = 字符串T。
设dp[i]为T的前i个前缀出现的次数,时间复杂度
xxxxxxxxxx231//https://vjudge.net/problem/AtCoder-typical90_h#author=GPT_zh23using namespace std;4const int N = 200005,mod = 1e9+7;5string t = " atcoder";6int a[N];7long long dp[5];8
9int main(){10 int n;cin >> n;11 int m = t.size()-1;12 string s;cin >> s;s = ' ' + s;13
14 dp[0] = 1;15 for(int i = 1;i <= n;i++){16 for(int j = 1;j <= m;j++){17 if(s[i] == t[j]){18 dp[j] = (dp[j] + dp[j-1])%mod;19 }20 }21 }22 cout << dp[m] << '\n';23}
图论
图论部分简介 - OI Wiki (oi-wiki.org)
作图工具Graph Editor (csacademy.com)
树和图的存储
邻接矩阵
空间复杂度n^2,适合存储稠密图,可以快速查询一条边是否存在
xxxxxxxxxx71int arr[N][N];2
3void add(int a,int b){4 arr[a][b] = x;//无权值则x为bool值代表是否连通,有权值则x为权值5}6//单向 add(a,b);7//双向 add(b,a);
邻接表
适合存稀疏图,不支持随机访问
初始 h[u] -> -1 add(u,1) h[u] -> 1 -> -1 add(u,2) h[u] -> 2 -> 1 -> -1 add(u,3) h[u] -> 3 -> 2 -> 1 -> -1
xxxxxxxxxx181//数组模拟链表实现 ---推荐2//诺为无向图,则边i和边i^1互为一条反边3int h[N], e[M], ne[M], idx;4
5//存边6void add(int a, int b) {7 e[idx] = b,ne[idx] = h[a],h[a] = idx++;8}9
10//遍历边11for(int i = h[x];i != -1;i = ne[i]){12 int k = e[i];13 cout << k << ' ';14}15
16int main() {17 memset(h, -1, sizeof h);//注意h需要初始化为-1 18}
xxxxxxxxxx121//STL vector实现2//在内存和时间上都比数组模拟链表能差,可能被卡时间3vector<int>e[N];4
5void add(int a, int b) {//存边6 e[a].push_back(b);7}8
9//遍历边10for(int i = 0;i < v[x].size();i++){11 cout << v[x][i] << ' ';12}xxxxxxxxxx121//STL list实现 由于内存不连续遍历不如vector但支持删除操作2list<int>e[N];3
4//存边5void add(int a, int b) {6 e[a].push_back(b);7}8
9//遍历边10for(int &k:e[u]){11 cout << k << " ";12}
树和图的遍历
| 二叉树的遍历 | |
|---|---|
| 前序遍历 | 根-左-右 |
| 中序遍历 | 左-根-右 |
| 后序遍历 | 左-右-根 |
DFS
xxxxxxxxxx381234using namespace std;5const int N = 100010, M = N * 2;6int n;7int h[N], e[M], ne[M], idx;8bool st[N];//存哪些点已经遍历过了9
10void add(int a, int b) {11 e[idx] = b;ne[idx] = h[a];h[a] = idx++;12}13
14void dfs(int u) {15 st[u] = 1;//标记这个点遍历过了16 for (int i = h[u]; i != -1;i = ne[i]) {//搜索下一条未被遍历过的条边17 int k = e[i];18 if (!st[k]) {19 dfs(k);20 }21 }22}23
24void dfs2(int u,int fa){//(无环图)可以省下一个vis数组25 for(int i = h[u];~i;i = ne[i]){26 int k = e[i];27 if(k != fa){28 dfs2(k,u);29 }30 }31}32
33int main() {34 memset(h, -1, sizeof h);35
36 dfs(1);37 //dfs2(1,0); 诺根节点编号从0开始,则应dfs2(0,-1)38}
xxxxxxxxxx131//dfs 无向图叶子节点2void dfs(int u,int fa){//u为当前节点,fa为当前节点的父节点3 if(q[u].size() == 1 && q[u][0] == fa){4 leaf.emplace_back(u);//此时u为叶子节点5 }6 st[u] = 1;7 for(auto &k:q[u]){8 if(!st[k]){9 dfs(k,u);10 st[k] = 1;11 }12 }13}
DFS序列
xxxxxxxxxx131void dfs(int u){2 d[++p] = u;3 st[u] = 1;//每个点第一次被标记为走过时的p即为该点的时间戳4 for(int i = h[u];~i;i = ne[i]){5 int k = e[i];6 if(!st[k]){7 dfs(k);8 }9 }10 d[++p] = u;//每个点来回会被标记两次,最后长度为2*n11}12
13for(int i = 1;i <= 2*n;i++) cout << d[i] << ' ';
每个节点x的编号在dfs序中恰好出现两次,假设这两次的位置为L[x]和R[x],则闭区间 L[x] ~ R[x] 就是以x为根的子树的dfs序。可以通过dfs序把子树统计转化为序列上的区间统计。例题:线段树维护子树信息Assign the task - HDU 3974 - Virtual Judge (vjudge.net)
DFS序:1 2 8 8 5 5 2 7 7 4 3 9 9 3 6 6 4 1
连通块划分
对一个森林多次dfs/bfs可以划分出每一棵树,并查集可以达到类似效果
xxxxxxxxxx40123using namespace std;4const int N = 100005;5int n,m;6int h[N],e[N],ne[N],idx;7int id[N],cnt;8
9void add(int a,int b){10 e[idx] = b,ne[idx] = h[a],h[a] = idx++;11}12
13void dfs(int u){14 id[u] = cnt;15 for(int i = h[u];~i;i = ne[i]){16 int k = e[i];17 if(!id[k]){18 dfs(k);19 }20 }21}22
23
24int main(){25 memset(h,-1,sizeof h);26 cin >> n >> m;27 while(m--){28 int a,b;cin >> a >> b;29 add(a,b);30 add(b,a);31 }32
33 for(int i = 1;i <= n;i++){34 if(!id[i]){35 cnt++;36 dfs(i);37 }38 cout << i << ' ' << id[i] << '\n';39 }40}
BFS
xxxxxxxxxx411//1号点到n号点的最短距离 https://www.acwing.com/problem/content/849/2345using namespace std;6const int N = 100010;7int n, m;8queue<int>q;//队列实现9int h[N], e[N], ne[N], idx,d[N];//d计算距离10void add(int a,int b) {//插入有向边a→b11 e[idx] = b;12 ne[idx] = h[a];13 h[a] = idx++;14}15
16int bfs() {17 q.push(1);18 d[1] = 0;19 while (q.size()) {20 int t = q.front();//取队头21 q.pop();22 for (int i = h[t]; i != -1;i = ne[i]) {//扩展t23 int k = e[i];24 if (d[k] == -1) {//如果没走过25 d[k] = d[t]+1;//则标记为走过,距离+126 q.push(k);//该点入队27 }28 }29 }30 return d[n];31}32int main() {33 memset(h, -1, sizeof h);//初始化邻接表指向-134 memset(d, -1, sizeof d);//初始化距离为-135 cin >> n >> m;36 while (m--) {37 int a, b; cin >> a >> b;38 add(a, b);39 }40 cout << bfs();41}
拓扑排序
在图论中,拓扑排序(Topological Sorting)是一个有向无环图(DAG, Directed Acyclic Graph)的所有顶点的线性序列。且该序列必须满足下面两个条件:
每个顶点出现且只出现一次。
若存在一条从顶点 A 到顶点 B 的路径,那么在序列中顶点 A 出现在顶点 B 的前面。

所有入度为0的点都可以作为起点,一个有向无环图至少存在一个入度为0的点,答案一般不唯一
xxxxxxxxxx501234using namespace std;5const int N = 100010;6int n, m;7queue<int>q;8int h[N], e[N], ne[N], idx;9int d[N],arr[N], cnt;//d[N]存每条边的入度,arr[cnt]记录每次入队的点10void add(int a, int b) {11 e[idx] = b, ne[idx] = h[a], h[a] = idx++;12}13bool topsort() {14 for (int i = 1; i <= n; i++) {15 if (d[i] == 0) {//先将所有入度为0的点入队16 q.push(i); 17 arr[cnt++] = i;18 }19 }20 while (q.size()) {21 int t = q.front();//取队头22 q.pop();23 for (int i = h[t]; i != -1; i = ne[i]) {//拓展t24 int k = e[i];25 d[k]--;//指向的边度数-126 if (d[k] == 0) {//如果入度为0则入队27 q.push(k);28 arr[cnt++] = k;29 }30 }31 }32 return cnt == n;//不相等说明存在重边或自环33}34int main() {35 memset(h, -1, sizeof h);36 cin >> n >> m;37 while (m--) {38 int a, b; cin >> a >> b;39 add(a, b);40 d[b]++;//b边入度+141 }42 if (topsort()) {43 for (int i = 0; i < n; i++) {44 cout << arr[i] << ' ';45 }46 }47 else cout << -1;48
49 return 0;50}
最短路
记录路径
邻接表用pre[a] = b 表示a是由b过来的,邻接矩阵用pre[i][j] = k
xxxxxxxxxx111//对于邻接表 https://codeforces.com/problemset/problem/20/C2if(dist[k] > dist[t] + w[i]){3 pq.push({dist[y],y});4 dist[k] = dist[t] + w[i];5 pre[t] = k;//每次更新最短路时记录路径6}7
8//输出最短路时由终点往前推9for(int i = n;i != 0;i = pre[n]){10 cout << i << " ";//此输出为反向,诺要正向输出路径,将i记录下来逆序输出即可11}xxxxxxxxxx161//对于邻接矩阵2if(dist[i][j] > dis[i][k] + dist[k][j]){3 dist[i][j] > dis[i][k] + dist[k][j];4 path[i][j] = k;//更新时记录路径5}6
7void print(int a,int b){8 if(path[a][b] == 0) return; 9 print(a,path[a][b]);//前半部 10 cout<< path[a][b] << " ";//输出该点 11 print(path[a][b],b);//后半部 12}13
14cout << x << ' ';//输出从x到y的最短路,无需逆序输出。15print(x,y);16cout << y;
最短路条数
求最短路有多少条
xxxxxxxxxx101//https://www.luogu.com.cn/problem/P11442//初始时cnt[1] = 1;3if(dist[y] > dist[x] + w[i]){4 dist[y] = dist[x] + w[i];5 pq.push({dist[y],y});6 cnt[y] = cnt[x];//更新y节点时,最短路条数继承自x7}8else if(dist[y] == dist[x] + w[i]){9 cnt[y] = (cnt[y] + cnt[x]) % mod;//诺有多条则累加10}
诺要求最多同时划分成多少条最短路,他们之间不含公共边。则可以将所有最短路径上的边保留下来,(如何确定一条边是否为最短路:正反图各跑一次,诺起点->a + a->b + b->终点 == 最短路,则边(a,b)为最短路上的一条边),跑最大流即为答案。4264. 规划最短路 - AcWing题库
单源最短路
仅正权边
Dijkstra朴素
适合稠密图,邻接矩阵存 O(n^2)
xxxxxxxxxx441//https://www.acwing.com/activity/content/problem/content/918/234using namespace std;5const int N = 510;6int n, m;7int g[N][N];//邻接矩阵存稠密图8int dist[N];//记录每个点距离第一个点的距离9bool st[N];//记录每个点的最短距离是否已经确认10
11int Dijkstra() {12 memset(dist, 0x3f, sizeof dist);//初始化距离为无限大13 dist[1] = 0;//第一个点到自身距离为014
15 for (int i = 0; i < n;i++) {//n个点进行n次迭代16 int t = -1;//t存储当前访问的点17
18 for (int j = 1; j <= n;j++) {//从1号点开始到n号点19 if (!st[j] && (t == -1 || dist[t] > dist[j]))//t=寻找所有st=0中dist最小的点20 t = j;21 }22 //if(t == n) break;23 24 st[t] = 1;25
26 for (int j = 1; j <= n;j++) {//再用t依次更新每个点所到相邻的点路径值27 dist[j] = min(dist[j], dist[t] + g[t][j]);28 //min(1~j , 1~t + t~j) d[j]取最短路径值29 }30 }31
32 if (dist[n] == 0x3f3f3f3f) return -1;//如果第n号点距离为无穷,则不存在最短路33 return dist[n];34}35
36int main() {37 memset(g, 0x3f, sizeof g);//求最短,稠密图初始化为无限大38 cin >> n >> m;39 while (m--) {40 int a, b, c; cin >> a >> b >> c;41 g[a][b] = min(g[a][b], c);//处理重边,保留最短的一条42 }43 cout << Dijkstra();44}
Dijkstra堆优化
适合稀疏图 ,优先队列小根堆实现 O(mlogn)
xxxxxxxxxx551//https://www.acwing.com/problem/content/description/852/2345using namespace std;6using PII = pair<int, int>;//first为距离,队列根据距离排序,second为对应点7const int N = 150005;8int n, m;9priority_queue<PII, vector<PII>, greater<PII>>pq;10int h[N], e[N], ne[N], idx;11int w[N];//存权重12int dist[N];13bool st[N];//如果为true说明这个点的最短路径已经确定14
15void add(int a, int b, int c) {16 w[idx] = c;17 e[idx] = b;18 ne[idx] = h[a];19 h[a] = idx++;20}21
22int Dijkstra() {23 memset(dist, 0x3f, sizeof dist);24 dist[1] = 0;25 pq.push({ 0,1 });26 27 while (pq.size()){28 auto t = pq.top();//取队头t29 pq.pop();30 int ver = t.second, distance = t.first;31 32 if (st[ver])continue;//如果距离已经确定,则跳过该点33 st[ver] = 1;34 35 for (int i = h[ver]; i != -1;i = ne[i]) {//拓展t36 int k = e[i];37 if (dist[k] > distance + w[i]) {38 dist[k] = distance + w[i];39 pq.push({dist[k],k});40 }41 }42 }43 if (dist[n] == 0x3f3f3f3f) return -1;//不存在输出-144 return dist[n];45}46int main() {47 memset(h, -1, sizeof h);48 cin >> n >> m;49 while (m--){50 int a, b, c; cin >> a >> b >> c;51 add(a, b, c);52 }53
54 cout << Dijkstra();55}
含负权边
在图中如果存在负环,则从该环上任意一点出发,沿着环重复行走可以使路径总权值无限减小,因此通常意义上的“最短路”在这种情况下是没有定义的(路径长度可以无限小)。然而,根据具体问题的需求,我们仍然可以通过一些方法处理含有负环的最短路问题。
Bellman-Ford
可以解决有边数限制的问题(诺问题允许忽略负环,如限制路径边数时,可以处理负权环)
时间复杂度O(mn) 结构体存边
xxxxxxxxxx341//https://www.acwing.com/problem/content/855/2//1~n号点,最多经过k条边的最短距离,边权可能为负数345using namespace std;6const int N = 505, M = 10004;7int n, m, k;//k代表最多经过k条边,诺是没限制k的最短路,则取n-18int dist[N], backup[N];//backup备份防止串联9struct Edge { int a, b, w; }edges[M];//使用结构体存储10
11int bellman_ford() {12 memset(dist, 0x3f, sizeof dist);13 dist[1] = 0;14 15 for (int i = 0; i < k; i++) {//k次循环16 memcpy(backup, dist, sizeof backup);17 for (int j = 0; j < m; j++) {//遍历所有边18 int a = edges[j].a, b = edges[j].b, w = edges[j].w;19 dist[b] = min(dist[b], backup[a] + w);//使用上一次备份数据防止节点最短距离串联20 }21 }22 return dist[n];23}24
25int main() {26 cin >> n >> m >> k;27 for (int i = 0; i < m;i++) {28 int a, b, w; cin >> a >> b >> w;29 edges[i] = { a,b,w };30 }31 int k = bellman_ford();32 if (k > 0x3f3f3f3f/2) cout << "impossible";//最后一次松弛后可能会在0x3f3f3f3f附近33 else cout << k;34}
SPFA
也称为 队列优化的Bellman-Ford
一般O(m),最坏O(mn),邻接表存,队列实现
如果不被卡(
SPFA已经死了.jpg),可以用来替代Dijkstra堆优化版
xxxxxxxxxx531//https://www.acwing.com/problem/content/853/2345using namespace std;6const int N = 100005;7int n, m;8
9int h[N], e[N], ne[N], idx;10int dist[N],w[N];11bool st[N];12
13void add(int a, int b,int c) {14 w[idx] = c, e[idx] = b, ne[idx] = h[a], h[a] = idx++;15}16
17int SPFA() {18 memset(dist, 0x3f, sizeof dist);//距离初始化为正无穷19 queue<int>q;//队列存最小距离变小的点,再用它扩展到邻接表中其它相邻的点20 dist[1] = 0;//起点到自身距离为021 q.push(1);22 st[1] = 1;23 24 while (q.size()){25 auto t = q.front();26 q.pop();27 st[t] = 0;//从队列中取出来之后该节点st被标记为false28
29 for (int i = h[t]; i != -1;i = ne[i]) {30 int k = e[i];31 if (dist[k] > dist[t] + w[i]) {32 dist[k] = dist[t] + w[i];33 if (!st[k]) {//将当前未加入队列的结点入队34 q.push(k);35 st[k] = 1;36 } 37 }38 }39 }40 return dist[n];41}42
43int main() {44 memset(h, -1, sizeof h);//链表头节点初始化为-145 cin >> n >> m;46 while (m--){47 int a, b, c; cin >> a >> b >>c;48 add(a, b, c);49 }50 int t = SPFA();51 if (t == 0x3f3f3f3f) puts("impossible");52 else cout << t;53}
多源最短路
Floyd
可以有负权边,不能有负权环 O(n^3)
邻接矩阵存储
xxxxxxxxxx11(k:1~n) (i:1~n) (j:1~n) {d[i,j] = min(d[i,j], d[i,k] + d[k,j]);}
xxxxxxxxxx401//https://www.acwing.com/problem/content/description/856/23using namespace std;4const int inf = 0x3f3f3f3f;5const int N = 210;6int d[N][N];7int n, m, q;8
9void floyd() {10 for (int k = 1; k <= n; k++) {//在枚举第k层前,已经得到了前k-1个点之间(路径不包含k)的最短路11 for (int i = 1; i <= n; i++) {12 for (int j = 1; j <= n; j++) {13 d[i][j] = min(d[i][j], d[i][k] + d[k][j]);//核心代码14 }15 }16 }17}18
19int main() {20 cin >> n >> m >> q;21 for (int i = 1; i <= n; i++) {22 for (int j = 1; j <= n; j++) {23 if (i != j) d[i][j] = inf;//初始化为无穷,处理自环24 }25 }26
27 while (m--) {28 int a, b, c; cin >> a >> b >> c;//a到b的距离为c29 d[a][b] = min(d[a][b], c);//处理重边30 }31
32 floyd();33
34 while (q--) {35 int a, b; cin >> a >> b;36 if (d[a][b] > inf / 2) cout << "impossible" << endl;37 //因为存在负权边,可能会把inf更新变小,导致d[a][b]略小于inf38 else cout << d[a][b] << endl;39 }40}
传递闭包
在一张点数为 n 的有向图的邻接矩阵中,给出任意两点间是否有直接连边的关系,让你求出任意两点之间是否有直接连边或间接连边的关系。
如果i能到达k,且k能到达j,则i也能到达j
xxxxxxxxxx331//https://www.luogu.com.cn/problem/B36112//给定一张点数为 n 的有向图的邻接矩阵,图中不包含自环,求该有向图的传递闭包34using namespace std;5const int N = 105;6int n;7int a[N][N];8
9void floyd(){10 for(int k = 1;k <= n;k++){11 for(int i = 1;i <= n;i++){12 for(int j = 1;j <= n;j++){13 a[i][j] |= a[i][k] & a[k][j];14 }15 }16 }17}18
19int main(){20 cin >> n;21 for(int i = 1;i <= n;i++){22 for(int j = 1;j <= n;j++){23 cin >> a[i][j];24 }25 }26 floyd();27 for(int i = 1;i <= n;i++){28 for(int j = 1;j <= n;j++){29 cout << a[i][j] << ' ';30 }31 cout << '\n';32 }33}
Johnson
可以有负权边,不能有负权环
时间复杂度
新建一个虚拟的0号节点,从这个点向其它点连一条边权为0的边。然后用Bellman-Ford求出0号节点到其它所有点的最短路,记为
h[ ]。假设存在一条从x到y边权为w的边,则我们重新设置边权为
w+p[x]-p[y](类似于物理学中的势能概念),消除负权边的影响。接下来以每个点为起点跑n轮Dijkstra算法,再消除势能差,即可求出任意两点间的最短路。
xxxxxxxxxx951//https://www.luogu.com.cn/problem/P59052345using namespace std;6const long long INF = 1e9;7const int N = 3003,M = 10004;8int n,m;9int h[N],e[M],ne[M],w[M],idx;10long long dist[N][N],p[N];11bool vis[N];12int cnt[N];13
14void add(int a,int b,int c){15 w[idx] = c,e[idx] = b,ne[idx] = h[a],h[a] = idx++;16}17
18bool spfa(){19 memset(p,0x3f,sizeof p);20 queue<int>q;21 q.push(0);22 p[0] = 0;23 vis[0] = 1;24 while(q.size()){25 int t = q.front();26 q.pop();27 vis[t] = 0;28 for(int i = h[t];~i;i = ne[i]){29 int k = e[i];30 if(p[k] > p[t] + w[i]){31 p[k] = p[t] + w[i];32 if(!vis[k]){33 q.push(k);34 vis[k] = 1;35 cnt[k]++;36 if(cnt[k] == n+1) return 1;37 }38 }39 }40 }41 return 0;42}43
44void dijkstra(int bg){45 memset(vis,0,sizeof vis);46 priority_queue<pair<int,int>,vector<pair<int,int>>,greater<pair<int,int>>>pq;47 pq.push({0,bg});48 dist[bg][bg] = 0;49 while(pq.size()){50 auto [distance,ver] = pq.top();51 pq.pop();52 if(vis[ver]) continue;53 else vis[ver] = 1;54 for(int i = h[ver];~i;i = ne[i]){55 int k = e[i];56 if(dist[bg][k] > dist[bg][ver] + w[i]){57 dist[bg][k] = dist[bg][ver] + w[i];58 if(!vis[k]){59 pq.push({dist[bg][k],k});60 }61 }62 }63 }64}65
66int main(){67 memset(h,-1,sizeof h);68 cin >> n >> m;69 for(int i = 1;i <= m;i++){70 int a,b,c;cin >> a >> b >> c;71 add(a,b,c);72 }73 for(int i = 1;i <= n;i++) add(0,i,0);74
75 if(spfa()) {cout << -1;return 0;}76
77 for(int u = 1;u <= n;u++){78 for(int i = h[u];~i;i = ne[i]){79 w[i] += p[u] - p[e[i]];80 }81 }82
83 memset(dist,0x3f,sizeof dist);84 for(int i = 1;i <= n;i++) { dijkstra(i); }85
86 for(int i = 1;i <= n;i++){87 long long ans = 0;88 for(int j = 1;j <= n;j++){89 dist[i][j] += p[j] - p[i];//最后消除势能差,此时dist[i][j]即为i到j的最短路90 if(dist[i][j] < INF ) ans += j * dist[i][j];91 else ans += j * INF;92 }93 cout << ans << '\n';94 }95}
(LCA)
求无向树上任意两点的距离
O(NlogN)预处理,O(logN)查询
设两个节点分别为x,y,dist[i]为根节点到点i的距离,lca(x,y)表示两点的最近公共祖先。 则 ans = dist[x] + dist[y] - 2*dist[lca(x,y)]
xxxxxxxxxx701//例题:How far away? https://vjudge.net/problem/HDU-2586#author=GPT_zh2345using namespace std;6
7void sol(){8 int n,q;scanf("%d %d",&n,&q);9 vector<vector<int>>fa(n+1,vector<int>(20));10 vector<vector<pair<int,int>>>e(n+1);11 vector<int>dist(n+1,0x3f3f3f3f),st(n+1),deep(n+1);12 for(int i = 1;i < n;i++){13 int a,b,c;scanf("%d %d %d",&a,&b,&c);14 e[a].push_back({b,c});15 e[b].push_back({a,c});16 }17 auto uuz = [&](){18 priority_queue<pair<int,int>,vector<pair<int,int>>,greater<pair<int,int>>>pq;19 dist[1] = 0;20 pq.push({0,1});21 while(pq.size()){22 auto [distance,ver] = pq.top();pq.pop();23 if(st[ver]) continue;24 else st[ver] = 1;25 for(auto &[k,w]:e[ver]){26 if(dist[k] > distance + w){27 dist[k] = distance + w;28 pq.push({dist[k],k});29 }30 }31 }32 };33 auto dfs = [&](auto &dfs,int u,int father)->void{34 deep[u] = deep[father] + 1;35 fa[u][0] = father;36 for(int i = 1;(1 << i) <= deep[u];i++){37 fa[u][i] = fa[fa[u][i-1]][i-1];38 }39 for(auto [k,w]:e[u]){40 if(k == father) continue;41 dfs(dfs,k,u);42 }43 };44 auto lca = [&](int x,int y){45 if(deep[x] < deep[y]) swap(x,y);46 for(int i = 20;i >= 0;i--){47 if(deep[fa[x][i]] >= deep[y]) x = fa[x][i];48 }49 if(x == y) return x;50 for(int i = 20;i >= 0;i--){51 if(fa[x][i] != fa[y][i]){52 x = fa[x][i];53 y = fa[y][i];54 }55 }56 return fa[x][0];57 };58 uuz();59 dfs(dfs,1,0);60 while(q--){61 int a,b;scanf("%d %d",&a,&b);62 int k = lca(a,b);63 printf("%d\n",dist[a] + dist[b] - 2*dist[k]);64 }65}66
67int main(){68 int t;scanf("%d",&t);69 while(t--) sol();70}
树上问题
树的重心
重心是指树中的一个结点,如果将这个点删除后,剩余各个连通块中点数的最大值最小,那么这个节点被称为树的重心。
树的重心如果不唯一,则至多有两个且相邻
以树的重心为根时,所有子树的大小都不超过整棵树大小的一半。
树中所有点到某个点的距离和中,到重心的距离和最小;如果有两个重心,那么到它们的距离和一样。
把两棵树通过一条边相连得到一棵新的树,那么新的树的重心在连接原来两棵树的重心的路径上。
在一棵树上添加或删除一个叶子,那么它的重心最多只移动一条边的距离。
xxxxxxxxxx501234using namespace std;5const int N = 200005;6int n,m;7int h[N],e[N],ne[N],idx;8bool st[N];9int siz[N],weight[N];//siz[i]表示dfs(x)选x为树根时,所有根节点为i的子树的大小(包括i)10 //weight[i]表示第i个节点的最大的子树的大小,与dfs(x)的x选取无关11vector<int>cent;//重心可能有两个12
13void add(int a,int b){14 e[idx] = b,ne[idx] = h[a],h[a] = idx++;15}16
17void dfs(int u){18 st[u] = 1;19 siz[u] = 1;20 int &now = weight[u];21 for(int i = h[u];~i;i = ne[i]){22 int k = e[i];23 if(!st[k]){24 dfs(k);25 siz[u] += siz[k];26 now = max(now,siz[k]);27 }28 }29 now = max(now,n - siz[u]);30 if(now <= n/2){//根据定义2:以树的重心为根时,所有子树的大小都不超过整棵树大小的一半31 cent.emplace_back(u);32 }33}34
35int main(){36 memset(h,-1,sizeof h);37 cin >> n >> m;38 for(int i = 1;i <= m;i++){39 int a,b;cin >> a >> b;40 add(a,b);41 add(b,a);42 }43
44 dfs(1);//任选一个点当做树根dfs均可45
46 cout << cent[0] << endl;47 //cout << cent[1] << endl;//重心可能有两个48
49 //cout << weight[cent[0]] << endl;50}xxxxxxxxxx571//给定一棵有根树,求出每一棵子树(有根树意义下且包含整颗树本身)的重心是哪一个节点。O(N)2//https://codeforces.com/contest/685/problem/B345using namespace std;6const int N = 300005;7int n,q;8int h[N],e[N],ne[N],idx;9bool st[N];10
11int fa[N];//存父节点12int ans[N];//当前节点的重心13int siz[N];//子树大小:所有子树上节点数 + 该节点14int weight[N];//节点重量:即所有子树「大小」的最大值15
16void add(int a,int b){17 e[idx] = b,ne[idx] = h[a],h[a] = idx++;18}19
20void dfs(int u){21 siz[u] = 1;22 ans[u] = u;23 for(int i = h[u];~i;i = ne[i]){24 int k = e[i];25 dfs(k);26 siz[u] += siz[k];27 weight[u] = max(weight[u],siz[k]);28 }29 for(int i = h[u];~i;i = ne[i]){30 int k = e[i];31 int p = ans[k];32 while(p!=u){33 if(max(weight[p],siz[u]-siz[p]) <= siz[u]/2){34 ans[u] = p;35 break;36 }37 else{38 p = fa[p];39 }40 }41 }42}43
44int main(){45 memset(h,-1,sizeof h);46 cin >> n >> q;47 for(int i = 2;i <= n;i++){48 int a;cin >> a;49 fa[i] = a;50 add(a,i);51 }52 dfs(1);53 while(q--){54 int u;cin >> u;55 cout << ans[u] << endl;56 }57}
树的中心
在树中,如果节点
性质:
树的中心不一定唯一,但最多有
个,且这两个中心是相邻的。树的中心一定位于树的直径上。
树上所有点到其最远点的路径一定交会于树的中心。
当树的中心为根节点时,其到达直径端点的两条链分别为最长链和次长链。
当通过在两棵树间连一条边以合并为一棵树时,连接两棵树的中心可以使新树的直径最小。
树的中心到其他任意节点的距离不超过树直径的一半。
xxxxxxxxxx621//树形dp实现 https://www.acwing.com/problem/content/1075/2//给定一颗树,找到一个点,使得该点到树中其它节点的最远距离最近345using namespace std;6const int N = 200005,INF = 0x3f3f3f3f;7int n;8int h[N],e[N],ne[N],w[N],idx;9int d1[N],d2[N],p1[N],p2[N],up[N];10//d1,d2表示向下的最大值和次大值,p1,p2表示d1,d2是由哪个子节点更新过来的11//up表示向上的最大值12
13void add(int a,int b,int c){14 w[idx] = c,e[idx] = b,ne[idx] = h[a],h[a] = idx++;15}16
17void dfs_d(int u,int fa){//子节点向父节点更新,获得向下的d1,d218 d1[u] = d2[u] = -INF;//初始化为-INF可以处理负权边19 for(int i = h[u];~i;i = ne[i]){20 int k = e[i];21 if(k == fa) continue;22 dfs_d(k,u);23 int t = d1[k] + w[i];24 if(t > d1[u]){25 d2[u] = d1[u]; d1[u] = t;26 p2[u] = p1[u]; p1[u] = k;27 }28 else if(t > d2[u]){29 d2[u] = t;30 p2[u] = k;31 }32 }33 if(d1[u] == -INF){ d1[u] = d2[u] = 0;}//特殊处理叶子节点34}35
36void dfs_u(int u,int fa){//父节点向子节点更新,获得向上的up37 for(int i = h[u];~i;i = ne[i]){38 int k = e[i];39 if(k == fa)continue;40 if(p1[u] == k){ up[k] = max(up[u],d2[u])+w[i]; }41 else { up[k] = max(up[u],d1[u])+w[i]; }42 dfs_u(k,u);43 }44}45
46int main(){47 memset(h,-1,sizeof h);48 cin >> n;49 for(int i = 1;i < n;i++){50 int a,b,c;cin >> a >> b >> c ;51 add(a,b,c);add(b,a,c);52 }53
54 dfs_d(1,0);55 dfs_u(1,0);56
57 int ans = INF;58 for(int i = 1;i <= n;i++){59 ans = min(ans,max(d1[i],up[i]));60 }61 cout << ans;62}
树的直径
树上任意两节点之间最长的简单路径即为「树的直径」。(可能有多条,所有直径经过相同的中点)
树形DP实现:我们记录当
xxxxxxxxxx421//https://www.acwing.com/problem/content/1074/234using namespace std;5const int N = 200005;6int n;7
8int h[N],e[N],ne[N],w[N],idx;9int d1[N],d2[N],ans;//d1记录最长路径,d2记录次长路径,初始为0,避免负权边的影响10
11void add(int a,int b,int c){12 w[idx] = c,e[idx] = b,ne[idx] = h[a],h[a] = idx++;13}14
15void dfs(int u,int fa){16 for(int i = h[u];~i;i = ne[i]){17 int k = e[i];18 if(k == fa) continue;19 dfs(k,u);20 int t = d1[k] + w[i];21 if(t > d1[u]){//诺t>d1同时更新d1,d222 d2[u] = d1[u];23 d1[u] = t;24 }25 else if(t > d2[u]){//else if诺t>d2只需更新d226 d2[u] = t;27 }28 }29 ans = max(ans,d1[u]+d2[u]);30}31
32int main(){33 memset(h,-1,sizeof h);34 cin >> n;35 for(int i = 1;i < n;i++){36 int a,b,c;cin >> a >> b >> c;37 add(a,b,c);38 add(b,a,c);39 }40 dfs(1,0);41 cout << ans;42}
两次DFS/BFS实现(边权必须非负):首先从任意节点
定理:在一棵树上,从任意节点y开始进行一次 DFS,到达的距离其最远的节点z必为直径的一端。
xxxxxxxxxx391//https://www.luogu.com.cn/problem/B4016234using namespace std;5using ll = long long;6const int N = 400005;7
8int n;9int h[N],e[N],ne[N],w[N],idx;10ll dist[N];11ll maxn;//记录最远点能到的点12
13void add(int a,int b,int c){14 w[idx] = c,e[idx] = b,ne[idx] = h[a],h[a] = idx++;15}16
17void dfs(int u,int fa){18 for(int i = h[u];i != -1;i = ne[i]){19 int k = e[i];20 if(k == fa) continue;21 dist[k] = dist[u] + w[i];22 if(dist[k] > dist[maxn]){maxn = k;}//更新最远能到的点23 dfs(k,u);24 }25}26
27int main(){28 memset(h,-1,sizeof h);29 cin >> n;30 for(int i = 1;i < n;i++){31 int a,b,c = 1;cin >> a >> b;32 add(a,b,c);33 add(b,a,c);34 }35 dfs(1,0);36 dist[maxn] = 0;37 dfs(maxn,0);38 cout << dist[maxn] << endl;;39}xxxxxxxxxx211//bfs(1)2void bfs(int u){3 queue<pair<int,int>>q;4 memset(st,0,sizeof st);5 q.push({u,0});6 st[u] = 1;7 while(q.size()){8 auto [v,dist] = q.front();9 q.pop();10 for(int i = h[v];~i;i = ne[i]){11 int k = e[i];12 if(st[k]) continue;13 st[k] = 1;14 q.push({k,dist+w[i]});15 if(ans < dist + w[i]){16 ans = dist + w[i];17 maxn = k;18 }19 }20 }21}
最近公共祖先
性质
; 是 的祖先,当且仅当 ;如果
不为 的祖先并且 不为 的祖先,那么 分别处于 的两棵不同子树中;前序遍历中,
出现在所有 中元素之前,后序遍历中 则出现在所有 中元素之后;两点集并的最近公共祖先为两点集分别的最近公共祖先的最近公共祖先,即
;两点的最近公共祖先必定处在树上两点间的最短路上;
,其中 是树上两点间的距离, 代表某点到树根的距离。
倍增求LCA
O(NlogN)预处理,O(logN)查询,空间复杂度O(NlogN)
dfs预处理fa[i,j],表示节点i的第
求lca时,让深度最小的节点跳到与另一个节点同一个深度,诺此时两个节点相同直接返回当前节点。 再让两个节点同时往上跳到最后一个非公共祖先节点,该点的父亲节点即为LCA节点
xxxxxxxxxx491//https://www.luogu.com.cn/problem/P3379234using namespace std;5const int N = 500005;6int n,q,root;7vector<int> e[N];8int fa[N][22],deep[N];//深度从1开始9
10void dfs(int u,int father){11 fa[u][0] = father;12 deep[u] = deep[father] + 1;13 for(int i = 1;(1 << i) <= deep[u];i++){14 fa[u][i] = fa[fa[u][i-1]][i-1];15 }16 for(auto k:e[u]){17 if(k == father) continue;18 dfs(k,u);19 }20}21
22int lca(int x,int y){23 if(deep[x] < deep[y]) swap(x,y);//一般以x作为深度较深的点24 for(int i = 20;i >= 0;i--){//再让x跳到与y同一深度25 if(deep[fa[x][i]] >= deep[y]) x = fa[x][i];26 }27 if(x == y) return x;28 for(int i = 20;i >= 0;i--){29 if(fa[x][i] != fa[y][i]){30 x = fa[x][i];31 y = fa[y][i];32 }33 }34 return fa[x][0];35}36
37int main(){38 cin >> n >> q >> root;39 for(int i = 1;i < n;i++){40 int a,b;cin >> a >> b;41 e[a].emplace_back(b);42 e[b].emplace_back(a);43 }44 dfs(root,0);45 while(q--){46 int a,b;cin >> a >> b;47 cout << lca(a,b) << '\n';48 }49}
Tarjan求LCA
离线算法,使用DFS+并查集实现
时间复杂度O(N+Q),常数较大
xxxxxxxxxx541234using namespace std;5const int N = 1000006;6int n,q,root;7int h[N],e[N],ne[N],idx;8int dfn[N],low[N],dn;9vector<pair<int,int>>ask[N];10int ans[N];11int p[N];12
13void add(int a,int b){14 e[idx] = b,ne[idx] = h[a],h[a] = idx++;15}16
17int find(int x){18 if(p[x] != x) p[x] = find(p[x]);19 return p[x];20}21
22void tarjan(int x,int fa){23 for(int i = h[x];~i;i = ne[i]){24 int y = e[i];25 if(y == fa) continue;26 tarjan(y,x);27 p[find(y)] = find(x);28 }29 for(auto [y,i]:ask[x]){30 ans[i] = find(y);31 }32}33
34int main(){35 memset(h,-1,sizeof h);36 cin >> n >> q >> root;37 for(int i = 1;i <= n;i++) p[i] = i;38 for(int i = 1;i < n;i++){39 int a,b;scanf("%d %d",&a,&b);40 add(a,b);add(b,a);41 }42 for(int i = 1;i <= q;i++){43 int a,b;scanf("%d %d",&a,&b);44 if(a == b) ans[i] = a;45 else{46 ask[a].push_back({b,i});47 ask[b].push_back({a,i});48 }49 }50 tarjan(root,0);51 for(int i = 1;i <= q;i++){52 printf("%d\n",ans[i]);53 }54}
树上差分
树上差分可以理解为对树上的某一段路径进行差分操作,这里的路径可以类比一维数组的区间进行理解。适合多次修改(O(logN)),单次查询(需要O(N)dfs统计信息)。
点差分
[P3128 USACO15DEC] Max Flow P - 洛谷 (luogu.com.cn)
每次将a到b简单路径上的点权加上x,则val[a] += x,val[b] += x, val[lca] -= x,val[fa[lca]] -= x。因为回溯统计时lca会被加上2x,所以我们需要让lca减去x,再让lca的父节点减去x。
xxxxxxxxxx68123using namespace std;4const int N = 50004,M = 100005;5int n,q,ans;6int h[N],e[M],ne[M],idx;7int fa[N][22],deep[N];8int val[N];9
10void add(int a,int b){11 e[idx] = b,ne[idx] = h[a],h[a] = idx++;12}13
14void dfs(int u,int father){15 fa[u][0] = father;16 deep[u] = deep[father] + 1;17 for(int i = 1;(1 << i) <= deep[u];i++){18 fa[u][i] = fa[fa[u][i-1]][i-1];19 }20 for(int i = h[u];~i;i = ne[i]){21 int k = e[i];22 if(k != father) dfs(k,u);23 }24}25
26int lca(int x,int y){27 if(deep[x] < deep[y]) swap(x,y);28 for(int i = 20;i >= 0;i--){29 if(deep[fa[x][i]] >= deep[y]) x = fa[x][i];30 }31 if(x == y) return x;32 for(int i = 20;i >= 0;i--){33 if(fa[x][i] != fa[y][i]){34 x = fa[x][i];35 y = fa[y][i];36 }37 }38 return fa[x][0];39}40
41void dfs2(int u,int father){42 for(int i = h[u];~i;i = ne[i]){43 int k = e[i];44 if(k != father){45 dfs2(k,u);46 val[u] += val[k];47 }48 }49 ans = max(ans,val[u]);50}51
52int main(){53 memset(h,-1,sizeof h);54 cin >> n >> q;55 for(int i = 1;i < n;i++){56 int a,b;cin >> a >> b;57 add(a,b);add(b,a);58 }59 dfs(1,0);60 while(q--){61 int a,b;cin >> a >> b;62 int father = lca(a,b);63 val[a]++;val[b]++;64 val[father]--;val[fa[father][0]]--;65 }66 dfs2(1,0);67 cout << ans;68}
边差分
与点差分略有不同。
诺要将a到b上简单路径的边权加x,则dist[a] += x,dist[b] += x,dist[lca] -= 2x。
树哈希
判断一些树是否同构的时,我们常常把这些树转成哈希值储存起来,以降低复杂度。
这里介绍一个实现简单且不容易被卡的做法,这类方法需要一个多重集的哈希函数。以某个结点为根的子树的哈希值,就是以它的所有儿子为根的子树的哈希值构成的多重集的哈希值,即:
其中
以代码中使用的哈希函数为例:
其中
这种哈希十分好写,比每次使用mt19937_64(hs[k])( )生成随机数要快。如果需要换根,第二次 DP 时只需把子树哈希减掉即可。
xxxxxxxxxx481//有根树哈希 https://uoj.ac/problem/7632//给定一棵以1为根的树,你需要输出这棵树中最多能选出多少个互不同构的子树。34567using namespace std;8const int N = 1000006,M = N << 1;9int n;10int h[N],e[M],ne[M],idx;11unsigned long long hs[N];12set<unsigned long long>se;13const unsigned long long mask = mt19937_64(time(0))();14
15void add(int a,int b){16 e[idx] = b,ne[idx] = h[a],h[a] = idx++;17}18
19unsigned long long shift(unsigned long long x){20 x ^= mask;21 x ^= x << 13;22 x ^= x >> 7;23 x ^= x << 17;24 x ^= mask;25 return x;26}27
28void dfs(int u,int fa){29 hs[u] = 1;30 for(int i = h[u];~i;i = ne[i]){31 int k = e[i];32 if(k == fa) continue;33 dfs(k,u);34 hs[u] += shift(hs[k]);35 }36 se.insert(hs[u]);37}38
39int main(){40 memset(h,-1,sizeof h);41 cin >> n;42 for(int i = 1;i < n;i++){43 int a,b;cin >> a >> b;44 add(a,b);add(b,a);45 }46 dfs(1,0);47 cout << se.size();48}
生成树
最小生成树
无向连通图的 最小生成树(Minimum Spanning Tree,MST)为边权和最小的生成树。
从图(一般为无向有环图 )中生成一棵树(n-1条边)时,这棵树每条边的权重相加之和最小。
只有连通图才有生成树,而对于非连通图,只存在生成森林。
Kruskal
适合稀疏图
结构体存边 并查集实现 将边权从小到大排序,遍历边,诺当前边的两个端点属于不同的树,则合并这两颗树
kurskal重构树的一些性质
是一棵二叉树。
如果是按最小生成树建立的话是一个大根堆。
原图中两个点间所有路径上的边最大权值的最小值 = 最小生成树上两点简单路径的边最大权值 = Kruskal 重构树上两点 LCA 的点权。
xxxxxxxxxx471//https://www.acwing.com/problem/content/description/861/234using namespace std;5const int N = 100005, M = 2 * N;6int n, m;7int p[N];//保存并查集8int res, cnt;9struct Edge {10 int a, b, w;11}edges[M];12
13bool cmp(Edge E1, Edge E2) {//自定义sort排序,以结构体w值为依据排序14 return E1.w < E2.w;15}16
17int find(int x) {//并查集18 if (p[x] != x) p[x] = find(p[x]);19 return p[x];20}21
22void kruskal() {23 for (int i = 0; i < m; i++) {24 int pa = find(edges[i].a), pb = find(edges[i].b);25 if (pa != pb) {//如果这个边与之前选择的所有边不会成环,就选择这条边26 p[pa] = pb;//合并27 res += edges[i].w;28 cnt++;29 }30 }31}32
33int main() {34 cin >> n >> m;35 for (int i = 1; i <= n; i++) { p[i] = i; }//初始化并查集36
37 for (int i = 0; i < m; i++) {38 int a, b, w; cin >> a >> b >> w;39 edges[i] = { a,b,w };40 }41 sort(edges, edges + m,cmp);//将所有m条边按照权值的大小进行升序排序42 43 kruskal();44
45 if (cnt < n - 1) puts("impossible");46 else cout << res << endl;47}
Prim朴素
适合稠密图
xxxxxxxxxx411//https://www.acwing.com/problem/content/860/234using namespace std;5const int N = 510, inf = 0x3f3f3f3f;6int n, m;7int dist[N], g[N][N];//dist存点到集合s的距离8bool vis[N];9
10int prim(){//1_idx11 std::memset(dist,0x3f,sizeof dist);12 std::memset(vis,0,sizeof vis);13 int ans = 0;14 for(int i = 0;i < n;i++){15 int t = 0;16 for(int j = 1;j <= n;j++){17 if(!vis[j] && (t == 0 || dist[t] > dist[j])) {18 t = j;//寻找离集合s最近的点t19 }20 }21 vis[t] = 1;22 if(i && dist[t] == 0x3f3f3f3f) return -1;//判断是否连通23 if(i) ans += dist[t];//加入点t24 for(int j = 1;j <= n;j++){25 dist[j] = std::min(dist[j],g[t][j]);//再用点t更新其它点到集合的距离26 }27 }28 return ans;29}30
31int main() {32 cin >> n >> m;33 memset(g, 0x3f, sizeof g);34 while (m--){35 int a, b, c; cin >> a >> b >> c;36 g[a][b] = g[b][a] = min(g[a][b], c);//处理重边37 }38 int t = prim();39 if (t == -1) puts("impossible");40 else cout << t;41}诺prim要输出具体选择的边,可以开一个辅助数组last[],更新dist[]时记录当前点从哪个点转移的,ans+=dist[t]时即选择了边(t,last)。
Prim堆优化
比较复杂不常用,直接用Kruskal算法
Boruvka
Kruskal和Prim的结合,在边具有较多特殊性质的问题中,Boruvka算法具有优势。
唯一性
考虑最小生成树的唯一性。如果一条边 不在最小生成树的边集中,并且可以替换与其 权值相同、并且在最小生成树边集 的另一条边。那么,这个最小生成树就是不唯一的。
对于 Kruskal 算法,只要计算为当前权值的边可以放几条,实际放了几条,如果这两个值不一样,那么就说明这几条边与之前的边产生了一个环(这个环中至少有两条当前权值的边,否则根据并查集,这条边是不能放的),即最小生成树不唯一。
寻找权值与当前边相同的边,我们只需要记录头尾指针,用单调队列即可在
xxxxxxxxxx631//https://vjudge.net/problem/OpenJ_Bailian-1679#author=GPT_zh234using namespace std;5const int N = 105,M = 10004;6int n,m;7int p[N];8
9struct edge{10 int a,b,w;11 bool operator < (edge &e2){12 return w < e2.w;13 }14}e[M];15
16int find(int x){17 if(p[x] != x) p[x] = find(p[x]);18 return p[x];19}20
21void soviet(){22 cin >> n >> m;23 for(int i = 1;i <= n;i++) p[i] = i;24 for(int i = 1;i <= m;i++){25 cin >> e[i].a >> e[i].b >> e[i].w;26 }27 sort(e+1,e+m+1);28
29 int ans = 0,cnt = 0;30 bool flag = 1;31 int tail = 0,sum1 = 0,sum2 = 0;32 for(int i = 1;i <= m+1;i++){33 if(i > tail){34 if(sum1 != sum2){//如果sum1 != sum2 则最小生成树不唯一35 flag = 0;36 break;37 }38 sum1 = sum2 = 0;39 for(int j = i;j <= m+1;j++){40 if(j > m || e[j].w != e[i].w){41 tail = j-1;42 break;43 }44 if(find(e[j].a) != find(e[j].b)) sum1++;//sum1计算当前权值的边可以放几条45 }46 }47 if(i > m) break;48 int pa = find(e[i].a),pb = find(e[i].b);49 if(pa != pb && cnt != n-1){50 sum2++;//sum2计算当前权值的边实际放了几条51 cnt++;52 p[pa] = pb;53 ans += e[i].w;54 }55 }56 if(flag) cout << ans << '\n';57 else cout << "Not Unique!\n";58}59
60int main(){61 int mt;cin >> mt;62 while(mt--) soviet();63}
次小生成树
非严格次小生成树
求解方法
求出无向图的最小生成树
,设其权值和为 。遍历每条未被选中的边
,找到 中 到 的路径上边权最大的一条边 ,在 中用 替换 ,即可得到一颗生成树 ,其权值为 。对所有的
取最小值即为答案。
其中求
xxxxxxxxxx1031//ACM Contest and Blackout https://vjudge.net/problem/UVA-106002//模版题,分别输出无向图的最小生成树和非严格次小生成树的权值,本题保证答案存在34567
8const int N = 105,M = N*N;9int n,m;10int fa[N][22],deep[N],mx[N][22];11int h[N],e[M],ne[M],w[M],idx;12
13void add(int a,int b,int c){14 w[idx] = c,e[idx] = b,ne[idx] = h[a],h[a] = idx++;15}16
17int p[N];18int find(int x){19 if(p[x] != x) p[x] = find(p[x]);20 return p[x];21}22
23struct Edge{24 int a,b,c;25 bool operator < (const Edge &e2) const{26 return c < e2.c;27 }28};29
30void dfs(int u,int father,int weight){//mx[u][i]表示u到其第2^i个父节点的最大权值31 deep[u] = deep[father]+1;32 fa[u][0] = father;33 mx[u][0] = weight;34
35 for(int i = 1;(1 << i) <= deep[u];i++){36 mx[u][i] = std::max(mx[fa[u][i-1]][i-1],mx[u][i-1]);//37 fa[u][i] = fa[fa[u][i-1]][i-1];38 }39 for(int i = h[u];~i;i = ne[i]){40 if(e[i] != father) dfs(e[i],u,w[i]);41 }42}43
44int lca(int a,int b){//求a,b路径上的最大权值45 int ans = 0;46 if(deep[a] < deep[b]) std::swap(a,b);47 for(int i = 20;i >= 0;i--){48 if(deep[fa[a][i]] >= deep[b]) {49 ans = std::max(ans,mx[a][i]);50 a = fa[a][i];51 }52 }53 if(a == b) return ans;54 for(int i = 20;i >= 0;i--){55 if(fa[a][i] != fa[b][i]){56 ans = std::max({ans,mx[a][i],mx[b][i]});57 a = fa[a][i];58 b = fa[b][i];59 }60 }61 ans = std::max({ans,mx[a][0],mx[b][0]});62 return ans;63}64
65void sol(){66 std::memset(h,-1,sizeof h);67 std::memset(fa,0,sizeof fa);//多测需清空fa数组68 idx = 0;69 std::cin >> n >> m;70 for(int i = 1;i <= n;i++) p[i] = i;71 std::vector<Edge>edge;72 for(int i = 1;i <= m;i++){73 int a,b,c; std::cin >> a >> b >> c;74 edge.push_back({a,b,c});75 }76 std::sort(edge.begin(),edge.end());77 78 int ans1 = 0;79 std::vector<Edge>T2;80 for(auto &[a,b,c]:edge){81 int pa = find(a),pb = find(b);82 if(pa == pb) { T2.push_back({a,b,c}); }83 else{84 p[pa] = pb;85 ans1 += c;86 add(a,b,c); add(b,a,c);87 }88 }89 //if(cnt != n-1) {return;} 不存在最小生成树90 //if(T2.empty()) {return;} 不存在次小生成树91 dfs(1,0,0);92 int ans2 = 1e9;93 for(auto &[a,b,c]:T2){//遍历所有未选择的边94 int now = ans1 + c - lca(a,b);//替换最小生成树中(a,b)路径上权值最大的一条边95 ans2 = std::min(ans2,now);//答案取最小96 }97 std::cout << ans1 << ' ' << ans2 << '\n';98}99
100int main(){101 int mt;std::cin >> mt;102 while(mt--) sol();103}
严格次小生成树
在用未选择的边
同时维护节点到祖先路径上的最大边权和严格次大边权即可。
xxxxxxxxxx1151//https://www.luogu.com.cn/problem/P41802//模版,本题保证严格次小生成树存在34567
8const int INF = 0x3f3f3f3f;9const int N = 100005,M = 600005;10int n,m;11int h[N],e[M],ne[M],w[M],idx;12int fa[N][20],deep[N],mx[N][20],mx2[N][20];//mx:最大边权,mx2:次大边权13
14struct Edge{15 int a,b,c;16 bool operator < (const Edge &e2)const{17 return c < e2.c;18 }19};20
21void add(int a,int b,int c){22 w[idx] = c,e[idx] = b,ne[idx] = h[a],h[a] = idx++;23}24
25int p[N];26int find(int x){27 if(p[x] != x) p[x] = find(p[x]);28 return p[x];29}30
31void dfs(int u,int father,int weight){32 deep[u] = deep[father] + 1;33 fa[u][0] = father;34 mx[u][0] = weight;35 mx2[u][0] = -INF;//-INF代表不存在次大边权36 for(int i = 1;(1 << i) <= deep[u];i++){37 mx[u][i] = std::max(mx[fa[u][i-1]][i-1],mx[u][i-1]);38 mx2[u][i] = std::max(mx2[fa[u][i-1]][i-1],mx2[u][i-1]);39
40 if(mx[u][i-1] > mx[fa[u][i-1]][i-1]) mx2[u][i] = std::max(mx2[u][i],mx[fa[u][i-1]][i-1]);41 else if(mx[u][i-1] < mx[fa[u][i-1]][i-1]) mx2[u][i] = std::max(mx2[u][i],mx[u][i-1]);42
43 fa[u][i] = fa[fa[u][i-1]][i-1];44 }45 for(int i = h[u];~i;i = ne[i]){46 if(e[i] != father) dfs(e[i],u,w[i]);47 }48}49
50int lca(int a,int b,int c){51 int ans = -INF;52 if(deep[a] < deep[b]) std::swap(a,b);53 for(int i = 18;i >= 0;i--){54 if(deep[fa[a][i]] >= deep[b]) {55 if(mx[a][i] < c) ans = std::max(ans,mx[a][i]);56 else ans = std::max(ans,mx2[a][i]);57 a = fa[a][i];58 }59 }60 if(a == b) return ans;61 for(int i = 18;i >= 0;i--){62 if(fa[a][i] != fa[b][i]){63 ans = std::max(ans,mx[a][i] < c ? mx[a][i] : mx2[a][i]);64 ans = std::max(ans,mx[b][i] < c ? mx[b][i] : mx2[b][i]);65 a = fa[a][i];66 b = fa[b][i];67 }68 }69 ans = std::max(ans,mx[a][0] < c ? mx[a][0] : mx2[a][0]);70 ans = std::max(ans,mx[b][0] < c ? mx[b][0] : mx2[b][0]);71 return ans;72}73
74int main(){75 std::memset(h,-1,sizeof h);76 std::cin >> n >> m;77 for(int i = 1;i <= n;i++) p[i] = i;78 std::vector<Edge>edge;79 for(int i = 1;i <= m;i++){80 int a,b,c;std::cin >> a >> b >> c;81 edge.push_back({a,b,c});82 }83 std::sort(edge.begin(),edge.end());84
85 std::vector<Edge>T2;86 int cnt = 0;87 long long ans1 = 0;88 for(auto &[a,b,c]:edge){89 int pa = find(a),pb = find(b);90 if(pa == pb) {91 T2.push_back({a,b,c});92 }93 else{94 p[pa] = pb;95 ans1 += c;96 cnt++;97 add(a,b,c); add(b,a,c);98 }99 }100
101// if(cnt != n-1) {return 0;} //无最小生成树102// if(T2.empty()) {return 0;} //无次小生成树103
104 dfs(1,0,0);105 long long ans2 = 1e18;106 for(auto &[a,b,c]:T2){107 long long w2 = lca(a,b,c); //查找生成树中(a,b)路径上边权严格小于c的最大边权108 if(w2 == -INF) continue; //不存在严格次大边109
110 long long now = ans1 + c - w2;111 ans2 = std::min(ans2,now);112 }113// if(ans2 == 1e18) {return 0;} //无严格次小生成树114 std::cout << ans2;115}
生成树计数
无向图的生成树计数
UVA10766 Organising the Organisation - 洛谷 (luogu.com.cn)
给定n个点,两两之间有一条无向边,现在切断m条边,求剩下的图中有多少种不同的生成树
矩阵树定理,时间复杂度
权值设为1求得的是不同生成树的个数。权值设为原边权求得的是不同生成树的权值之和。
xxxxxxxxxx66123
4const int N = 55;5int n,m,root;6int g[N][N];7
8struct Mat{9 std::vector<std::vector<long long>>a;10
11 Mat(int n){12 a = std::vector<std::vector<long long>>(n+1,std::vector<long long>(n+1));13 }14
15 void add(int x,int y,int w){//无向图add(x,y,w),add(y,x,w); 度数矩阵-邻接矩阵16 if(x == y) return;17 a[y][y] = (a[y][y] + w);18 a[x][y] = (a[x][y] - w);19 }20
21 long long det(int n){//高斯消元求n*n行列式的值(1_idx)22 long long ans = 1;23 for(int i = 2;i <= n;i++){//删除根节点1所在行列,下标从2开始即可(无向图可任选根)24 for(int j = i+1;j <= n;j++){25 while(a[j][i]){26 long long t = a[i][i]/a[j][i];//貌似求不求逆元都无所谓?27 for(int k = i;k <= n;k++) {28 a[i][k] -= a[j][k] * t;29 }30 std::swap(a[i],a[j]);31 ans = -ans;32 }33 }34 if(!a[i][i]) return 0;35 ans *= a[i][i];36 }37 return std::abs(ans);38 }39};40
41void sol(){42 Mat M(n);43 for(int i = 1;i <= n;i++){44 for(int j = 1;j <= n;j++){45 g[i][j] = 1;46 }47 }48 for(int i = 1;i <= m;i++){49 int x,y;std::cin >> x >> y;50 g[x][y] = g[y][x] = 0;51 }52
53 for(int i = 1;i <= n;i++){54 for(int j = i+1;j <= n;j++){55 if(g[i][j]) {56 M.add(i,j,1);57 M.add(j,i,1);58 }59 }60 }61 std::cout << M.det(n) << '\n';62}63
64int main(){65 while(std::cin >> n >> m >> root) sol();66}
有向图的生成树计数
[P4455 CQOI2018] 社交网络 - 洛谷 (luogu.com.cn)
给定n个点m条边的有向图,求以1为根节点的生成树个数。
如果要是给定root,不一定为1,将第root行和第n行交换,第root列和第n列交换,然后删除第n行n列再计算即可。
xxxxxxxxxx47123
4const int mod = 1e4+7;5
6struct Mat{7 std::vector<std::vector<long long>>a;8
9 Mat(int n){10 a = std::vector<std::vector<long long>>(n+1,std::vector<long long>(n+1));11 }12
13 void add(int x,int y,int w){//有向图add(x,y,w);14 if(x == y) return;15 a[y][y] = (a[y][y] + w) % mod;16 a[x][y] = (a[x][y] - w) % mod;17 }18
19 long long det(int n){//高斯消元求n*n行列式的值(1_idx)20 long long ans = 1;21 for(int i = 2;i <= n;i++){//下标从2开始,相当于删除根节点1所在行列,22 for(int j = i+1;j <= n;j++){23 while(a[j][i]){24 long long t = a[i][i]/a[j][i];//貌似求不求逆元都无所谓?25 for(int k = i;k <= n;k++) {26 a[i][k] = (a[i][k] - a[j][k] * t % mod + mod) % mod;27 }28 std::swap(a[i],a[j]);29 ans = -ans;30 }31 }32 if(!a[i][i]) return 0;33 ans = (ans * a[i][i] % mod + mod) % mod;34 }35 return std::abs(ans);36 }37};38
39int main(){40 int n,m; std::cin >> n >> m;41 Mat M(n);42 for(int i = 1;i <= m;i++){43 int a,b;std::cin >> a >> b;44 M.add(b,a,1);//本题题意是b向a连边45 }46 std::cout << M.det(n);47}
最小树形图
有向图上的最小生成树(Directed Minimum Spanning Tree)称为最小树形图。
所有从图中生成一颗 根节点能到达其它节点的树,边权和最小的一颗树
常用的算法是朱刘算法(也称 Edmonds 算法),可以在
算法流程,对于每一次循环:
初始化
对于每个点,选择指向它的边权最小的一条边
将选出的边累加到答案里,如果出现孤立点则无解
如果没有环,算法完成,退出循环
把所有不是环上的点全部设置为自己是一个独立的环(大小为1的新环)
进行缩环并更新其它点到环的距离
重新设置n和root
xxxxxxxxxx731//https://www.luogu.com.cn/problem/P47162//给定一个n个点m条边根节点为root的有向图,试求出一颗以root为根的最小树形图34
5const int INF = 0x3f3f3f3f;6const int N = 3003,M = 10004;7
8int mn[N],fa[N];//每个节点的最小入边权值及其父节点9int loop[N];//节点所属的环编号(0表示不在环中)10int vis[N]; //临时标记数组(用于环检测)11
12struct Edge{13 int a,b,c;14}edge[M];15
16int zhuliu(int n,int m,int root){17 int ans = 0;18 while(1){19 int tot = 0;//记录当前迭代检测到的环的数量20 for(int i = 1;i <= n;i++){//初始化数组21 mn[i] = INF;22 fa[i] = vis[i] = loop[i] = 0;23 }24
25 for(int i = 1;i <= m;i++){//贪心找到最小入边图,并记录该图上每个节点的最小入边权值及其父节点26 auto &[a,b,c] = edge[i];27 if(a != b && mn[b] > c){//处理自环(缩环后存在自环)28 mn[b] = c;29 fa[b] = a;30 }31 }32 mn[root] = 0;//根节点没有入边,权值设为033
34 for(int i = 1;i <= n;i++){35 if(mn[i] == INF) return -1;//诺存在孤立节点,无解36 ans += mn[i];//累加当前选择边的权值37 }38
39 for(int a = 1;a <= n;a++){//环检测40 int b = a;41 //沿着父链回溯,直到遇到根节点、已标记节点或已经在环上的节点42 while(b != root && !vis[b] && !loop[b]) {43 vis[b] = a;//标记当前节点44 b = fa[b];//回溯父节点45 }46 if(b != root && vis[b] == a){//发现新环47 loop[b] = ++tot;//记录环上的点在哪个环上48 for(int k = fa[b];k != b;k = fa[k]){49 loop[k] = tot;50 }51 }52 }53 if(!tot) return ans;//如果没有环,算法完成,返回答案54 55 for(int i = 1;i <= n;i++){//处理剩余节点,所有非环上的独立点,每个点自己视为一个环56 if(!loop[i]) loop[i] = ++tot;57 }58 for(int i = 1;i <= m;i++){//重新设置边权,缩环59 auto &[a,b,c] = edge[i];60 edge[i] = {loop[a],loop[b],c-mn[b]};//边权减去已选择的最小入边权值61 }62 n = tot;//完成缩环,重新设置n和root63 root = loop[root];64 }65}66
67int main(){68 int n,m,root; std::cin >> n >> m >> root;69 for(int i = 1;i <= m;i++){70 std::cin >> edge[i].a >> edge[i].b >> edge[i].c;71 }72 std::cout << zhuliu(n,m,root);73}
不定根的最小树形图
Ice_cream’s world II - HDU 2121 - Virtual Judge (vjudge.net)
输出 最小树形图权值 和 选定的根,诺根不唯一,输出编号最小的。
我们建立一个虚拟的超级源点,并向其它所有点连边,边权为大于原图所有边权之和和一个数(假设为sum),诺最后ans - sum >= sum说明虚拟源点建的边用了超过一次,则图不连通无解。否则最终答案就是ans-sum。
主要在于如何找编号最小的根。如果有多个根,则他们最终会成一个环,这个环缩点后会与虚根相连,找出节点编号最小的一个根即可,因为我们从虚根向节点建边时是从m+1到m+n建边的,所以在下一次循环时,第一次访问到连向该环的边时,其编号(减去m)即为编号最小的根。
xxxxxxxxxx8212
3const int INF = 0x3f3f3f3f;4const int N = 2003,M= 20004;5int n,m,root,real_root;6int fa[N],mn[N],vis[N],loop[N];7
8struct Edge{9 int a,b,c;10}edge[M];11
12int zhuliu(int n,int m,int root){13 int ans = 0;14 while(1){15 int cnt = 0;16 for(int i = 1;i <= n;i++){17 mn[i] = INF;18 fa[i] = vis[i] = loop[i] = 0;19 }20 for(int i = 1;i <= m;i++){21 auto &[a,b,c] = edge[i];22 if(a != b && mn[b] > c){23 mn[b] = c;24 fa[b] = a;25 if(a == root) real_root = i;//只需要在这里更新要找的根26 }27 }28 mn[root] = 0;29 for(int i = 1;i <= n;i++){30 if(mn[i] == INF) return -1;31 ans += mn[i];32 }33 for(int a = 1;a <= n;a++){34 int b = a;35 while(b != root && !vis[b] && !loop[b]){36 vis[b] = a;37 b = fa[b];38 }39 if(b != root && vis[b] == a){40 loop[b] = ++cnt;41 for(int k = fa[b];k != b;k = fa[k]){42 loop[k] = cnt;43 }44 }45 }46 if(!cnt) return ans;47 for(int i = 1;i <= n;i++){48 if(!loop[i]) loop[i] = ++cnt;49 }50 for(int i = 1;i <= m;i++){51 auto &[a,b,c] = edge[i];52 edge[i] = {loop[a],loop[b],c-mn[b]};53 }54 n = cnt;55 root = loop[root];56 }57}58
59void sol(){60 int sum = 0;61 for(int i = 1;i <= m;i++){62 int a,b,c;std::cin >> a >> b >> c;63 edge[i] = {++a,++b,c};//本题节点编号从0_idx开始64 sum += c;65 }66 sum++;67 for(int i = 1;i <= n;i++){68 edge[m+i] = {n+1,i,sum};69 }70 int ans = zhuliu(n+1,n+m,n+1);71
72 if(ans == -1 || ans - sum >= sum) std::cout << "impossible\n";73 else std::cout << ans-sum << ' ' << real_root - m - 1<< '\n';74}75
76int main(){77 std::ios::sync_with_stdio(false);std::cin.tie(0);78 while(std::cin >> n >> m) {79 sol();80 std::cout << '\n';81 }82}
二分图
性质:
不存在边数为奇数的环
如果两个集合中的点分别染成黑色和白色,可以发现二分图中的每一条边都一定是连接一个黑色点和一个白色点。
二分图判定
染色法判二分图
判断是否是二分图(不存在奇环)
xxxxxxxxxx511//二分图的判定 https://www.acwing.com/problem/content/862/2345using namespace std;6const int N = 200005;7int n,m;8int h[N],e[N],ne[N],idx;9int color[N];//0代表未染色,1/2表示染了两种颜色10
11void add(int a,int b){12 e[idx] = b,ne[idx] = h[a],h[a] = idx++;13}14
15bool uuz(int be){16 color[be] = 1;17 queue<int>q;18 q.push(be);19 while(q.size()){20 int t = q.front();21 q.pop();22 for(int i = h[t];~i;i = ne[i]){23 int k = e[i];24 if(!color[k]){//如果当前点k未染色,则染为与点t相对的颜色25 color[k] = 3 - color[t];26 q.push(k);27 }28 if(color[k] == color[t]) return 0;//如果已经染色,且相邻节点t与k同色,则不是二分图29 }30 }31 return 1;32}33
34int main(){35 memset(h,-1,sizeof h);36 cin >> n >> m;37 for(int i = 1;i <= m;i++){38 int a,b;cin >> a >> b;39 add(a,b);add(b,a);40 }41
42 for(int i = 1;i <= n;i++){43 if(!color[i]){44 if(uuz(i) == 0) {45 cout << "No";46 return 0;47 }48 }49 }50 cout << "Yes";51}
最大匹配
“任意两条边都没有公共端点”的边的集合被称为图的一组匹配。在二分图中,包含边数最多的一组匹配被称为二分图的最大匹配。
例如图中红边集合为一种最大匹配方案。
匈牙利算法
又称增广路算法,二分图的一组匹配S是最大匹配,当且仅当图中不存在S的增广路。
时间复杂度
xxxxxxxxxx441//https://www.acwing.com/problem/content/863/234using namespace std;5const int N = 510, M = 100005;6int n1, n2, m;7int h[N], e[M], ne[M], idx;8int match[N],res;//match[b] = a; 即为a->b的匹配9bool st[N];10
11void add(int a, int b) {12 e[idx] = b, ne[idx] = h[a], h[a] = idx++;13}14
15bool find(int x) {16 for (int i = h[x]; i != -1;i = ne[x]) {17 int y = e[i];18 if (!st[y]) {//如果某个点没被预定19 st[y] = 1;//预定这个点20 if (match[y] == 0 || find(match[y])) {21 //如果这个点没有匹配的对象或其匹配的对象能匹配其它点22 match[y] = x;//则y与x匹配23 return 1;24 }25 }26 }27 return 0;//如果全被预定了,则匹配失败28}29
30int main() {31 cin >> n1 >> n2 >> m;32 memset(h, -1, sizeof h);33 while (m--){34 int a, b; cin >> a >> b;35 add(a, b);//只连一边(左到右),只有确定集合A到B只有单向关系,两个节点编号才可以相同,否则应让b加上一个偏移36 }37
38 for (int i = 1; i <= n1;i++) {//n1轮39 memset(st, 0, sizeof st);40 //因为每次模拟匹配的预定情况都是不一样的所以每轮模拟都要初始化41 if (find(i)) res++;42 }43 cout << res;44}结果
无向图的最大匹配
只能求最二分图大匹配(即没有奇环),诺为一般图,则应换用带花树算法
xxxxxxxxxx731//https://vjudge.net/problem/HDU-24442//染色且只匹配当前颜色为1的点3//或者最终答案直接除以2(貌似这种具体方案不好求?)4567
8const int N = 205,M = N*N;9int n,m;10
11int h[N],e[M],ne[M],idx;12void add(int a,int b){13 e[idx] = b,ne[idx] = h[a],h[a] = idx++;14}15
16bool vis[N];17int match[N],color[N];18bool dfs(int x,int c){19 color[x] = c;20 for(int i = h[x];~i;i = ne[i]){21 int y = e[i];22 if(!color[y]) {23 if(dfs(y,3-c) == 0) return 0;24 }25 else if(color[y] == c) return 0; 26 }27 return 1;28}29
30bool find(int x){31 if(color[x] != 1) return 0;//只匹配颜色为1的点32 for(int i = h[x];~i;i = ne[i]){33 int y = e[i];34 if(!vis[y]){35 vis[y] = 1;36 if(!match[y] || find(match[y])){37 match[y] = x;38 return 1;39 }40 }41 }42 return 0;43}44
45void sol(){46 for(int i = 1;i <= m;i++){47 int a,b;std::cin >> a >> b;48 add(a,b); add(b,a);49 }50 for(int i = 1;i <= n;i++){51 if(!color[i]){52 if(dfs(i,1) == 0) {std::cout << "No\n";return;}//前提必须是二分图53 }54 }55
56 int ans = 0;57 for(int i = 1;i <= n;i++){58 std::memset(vis,0,sizeof vis);59 ans += find(i);60 }61 std::cout << ans << '\n';62}63
64int main(){65 while(std::cin >> n >> m) {66 idx = 0;67 for(int i = 1;i <= n;i++){68 h[i] = -1;69 color[i] = match[i] = 0;70 }71 sol();72 }73}
多重匹配
xxxxxxxxxx361const int N = 303;2int n1,n2,m;3int g[N][N];4
5std::set<int>se[N];6bool vis[N];7bool find(int x){8 for(int y = 1;y <= n1;y++){9 if(g[x][y] && !vis[y]){10 vis[y] = 1;11 if(se[y].size() < m){//如果当前组容量有剩余,则直接加入12 se[y].insert(x);13 return 1;14 }15 else if(se[y].size() == m){//如果已经满了16 for(auto &x2:se[y]){//尝试从当前组移出一个到其它组17 if(find(x2,mid)){18 se[y].erase(x2);19 se[y].insert(x);20 return 1;21 }22 }23 }24 }25 }26 return 0;27}28
29bool check(){30 for(int i = 1;i <= n1;i++) se[i].clear();31 for(int i = n1+1;i <= n1+n2;i++){32 std::memset(vis,0,sizeof vis);33 if(find(i) == 0) return 0;34 }35 return 1;36}
HK 算法
Hopcroft–Karp算法,时间复杂度为
xxxxxxxxxx951//【模板】二分图最大匹配 https://www.luogu.com.cn/problem/P33862345
6struct HK{7 int n1,n2,dis;8 std::vector<int>cx,cy,dx,dy,vis;9 std::vector<std::vector<int>>g;10 const int INF = 0x3f3f3f3f;11
12 HK(int _n1,int _n2){13 n1 = _n1,n2 = _n2;14 g.assign(n1+1,{});15 }16
17 void add(int a,int b){18 g[a].emplace_back(b);19 }20
21 bool bfs(){22 std::queue<int>q;23 dx = std::vector<int>(n1+1,-1);24 dy = std::vector<int>(n2+1,-1);25
26 dis = INF;27 for(int i = 1;i <= n1;i++){28 if(cx[i] == -1){29 q.push(i);30 dx[i] = 0;31 }32 }33 while(q.size()){34 int x = q.front();35 q.pop();36 if(dx[x] > dis) break;37 for(auto &y:g[x]){38 if(dy[y] == -1){39 dy[y] = dx[x] + 1;40 if(cy[y] == -1) dis = dy[y];41 else{42 dx[cy[y]] = dy[y]+1;43 q.push(cy[y]);44 }45 }46 }47 }48 return dis != INF;49 }50
51 bool dfs(int x){52 for(auto &y:g[x]){53 if(!vis[y] && dy[y] == dx[x] + 1){54 vis[y] = 1;55 if(cy[y] != -1 && dy[y] == dis) continue;56 if(cy[y] == -1 || dfs(cy[y])){57 cy[y] = x;58 cx[x] = y;59 return 1;60 }61 }62 }63 return 0;64 }65
66 int sol(){67 int ans = 0;68 cx = std::vector<int>(n1+1,-1);69 cy = std::vector<int>(n2+1,-1);70 while(bfs()) {71 vis = std::vector<int>(n2+1,0);72 for(int i = 1;i <= n1;i++){73 if(cx[i] == -1 && dfs(i)) ans++;74 }75 }76 return ans;77 }78};79
80
81int main(){82 int n1,n2,m;83 std::cin >> n1 >> n2 >> m;84 HK hk(n1,n2);85 for(int i = 1;i <= m;i++){86 int a,b;std::cin >> a >> b;//n1[] -> n2[]87 hk.add(a,b);88 }89 std::cout << hk.sol() << '\n';90// for(int i = 1;i <= n1;i++){//具体方案,cx[]为左边的点匹配到的方案,cy[]为右边的点匹配到的方案91// if(hk.cx[i] != -1) {//-1则为匹配失败92// std::cout << i << ' ' << hk.cx[i] << '\n';93// }94// }95}
转为网络流模型
边权设为1,源点向左边集合所有点连边,右边集合所有点向汇点连边。求最大流即可。
时间复杂度
xxxxxxxxxx771//基于Dinic实现 【模板】二分图最大匹配 https://www.luogu.com.cn/problem/P33862345
6const int INF = 0x3f3f3f3f;7const int N = 1003,M = 400005;8int h[N],e[M],ne[M],idx,w[M];9int n1,n2,m;10int s,t;11int maxflow;12int d[N],now[N];13
14void add(int a,int b,int c){15 w[idx] = c,e[idx] = b,ne[idx] = h[a],h[a] = idx++;16}17
18bool bfs(){19 memset(d,0,sizeof d);20 std::queue<int>q;21 q.push(s);22 d[s] = 1;23 now[s] = h[s];24 while(q.size()){25 int x = q.front();26 q.pop();27 for(int i = h[x];~i;i = ne[i]){28 int y = e[i];29 if(!w[i] || d[y]) continue;30 d[y] = d[x]+1;31 now[y] = h[y];32 q.push(y);33 if(y == t) return 1;34 }35 }36 return 0;37}38
39int dfs(int x,int flow){40 if(x == t) return flow;41 int res = 0;42 for(int i = now[x];~i && flow;i = ne[i]){43 now[x] = i;44 int y = e[i];45 if(!w[i] || d[y] != d[x]+1) continue;46 int k = dfs(y,std::min(flow,w[i]));47 if(!k) d[y] = 0;48 w[i] -= k;49 w[i^1] += k;50 flow -= k;51 res += k;52 }53 return res;54}55
56int main(){57 std::memset(h,-1,sizeof h);58 std::cin >> n1 >> n2 >> m;59 s = 0,t = n1+n2+1;60 for(int i = 1;i <= m;i++){61 int a,b;std::cin >> a >> b;62 add(a,b+n1,1);63 add(b+n1,a,0);64 }65 for(int i = 1;i <= n1;i++) add(s,i,1),add(i,s,0);66 for(int i = n1+1;i <= n1+n2;i++) add(i,t,1),add(t,i,0);//如果B集合每个点可以容下最多m个A集合中的点,则这里边权改为m即可67
68 while(bfs()) maxflow += dfs(s,INF);69
70 std::cout << maxflow << '\n';71// for(int i = 1;i < idx;i += 2){//具体方案,遍历所有反边72// if(w[i]) {//如果反边还剩流量且端点不为源点或汇点73// if(e[i] == s || e[i^1] == t) continue;74// std::cout << e[i] << ' ' << e[i^1] - n1 << '\n';75// }76// }77}
常见问题模型
二分图匹配的模型有两个要素:
节点能分成独立的两个集合,每个集合内部有0条边。
每个节点只能与1条匹配边相连。
给定一个 N 行 N列的棋盘,已知某些格子禁止放置。求最多能往棋盘上放多少块的长度为 2、宽度为 1 的骨牌,并且任意两张骨牌都不重叠。
将骨牌看作无向边,在相邻的两个格子连边。如果把棋盘黑白染色(行号加列号为偶数染成白色,否则染成黑色)。那么相邻的两个格子颜色不同,有连边。同色的两个格子不相邻,没有边相连。该图是一张二分图,二分图的最大匹配即为最多能放骨牌的个数。
xxxxxxxxxx42123using namespace std;4const int N = 105;5int n,t;6int a[N][N];7pair<int,int> match[N][N];8bool st[N][N];9int dx[] ={-1,1,0,0},dy[] = {0,0,-1,1};10
11bool find(pair<int,int>p){12 auto [x,y] = p;13 for(int i = 0;i < 4;i++){14 int nx = x + dx[i],ny = y + dy[i];15 if(nx < 1 || nx > n || ny < 1 || ny > n || a[nx][ny]) continue;16 if(!st[nx][ny]){17 st[nx][ny] = 1;18 if(!match[nx][ny].first || find(match[nx][ny])){19 match[nx][ny] = {x,y};20 return 1;21 }22 }23 }24 return 0;25}26
27int main(){28 cin >> n >> t;29 while(t--){30 int x,y;cin >> x >> y;31 a[x][y] = 1;32 }33 int ans = 0;34 for(int i = 1;i <= n;i++){35 for(int j = 1;j <= n;j++){36 if((i+j)&1 || a[i][j]) continue;37 memset(st,0,sizeof st);38 ans += find({i,j});39 }40 }41 cout << ans;42}
P10937 車的放置 - 洛谷 (luogu.com.cn)
给定一个 N 行 M 列的棋盘,已知某些格子禁止放置。问棋盘上最多能放多少个不能互相攻击的車(同行或同列只能有一个车)。
xxxxxxxxxx48123using namespace std;4const int N = 205,M = N*N;5int n,m,t;6int a[N][N];7int h[N],e[M],ne[M],idx;8bool st[N];9int match[N];10
11void add(int a,int b){12 e[idx] = b,ne[idx] = h[a],h[a] = idx++;13}14
15bool find(int x){16 for(int i = h[x];~i;i = ne[i]){17 int y = e[i];18 if(!st[y]){19 st[y] = 1;20 if(!match[y] || find(match[y])){21 match[y] = x;22 return 1;23 }24 }25 }26 return 0;27}28
29int main(){30 memset(h,-1,sizeof h);31 cin >> n >> m >> t;32 while(t--){33 int x,y;cin >> x >> y;34 a[x][y] = 1;35 }36
37 for(int i = 1;i <= n;i++){38 for(int j = 1;j <= m;j++){39 if(a[i][j] == 0) add(i,j);40 }41 }42 int ans = 0;43 for(int i = 1;i <= n;i++){44 memset(st,0,sizeof st);45 ans += find(i);46 }47 cout << ans;48}
最大权匹配
KM算法
时间复杂度
xxxxxxxxxx981//【模板】二分图最大权完美匹配 https://www.luogu.com.cn/problem/P65772//诺要求最小权,只需将结构体例的INF和-INF,min和max对换34using namespace std;5
6const long long INF = 1e18;7const int N = 505;8int n1,n2,m;9
10struct KM {11 int n;12 int pl[N], pr[N], fa[N];13 long long g[N][N], wl[N], wr[N], sl[N];14 bitset<N> vis;15 long long tot;16
17 void init(int _n) {18 n = _n;19 for (int i = 1; i <= n; i++) {20 fill(g[i] + 1, g[i] + n + 1, -INF);//本题含有负权边,初始化为-INF,否则初始化为021 wl[i] = wr[i] = sl[i] = 0;22 pl[i] = pr[i] = fa[i] = 0;23 }24 }25
26 void add(int x, int y, long long z) {27 g[x][y] = max(g[x][y],z);28 }29
30 void get(int x) {31 for (int i = 1; i <= n; i++) {32 sl[i] = INF;33 fa[i] = 0;34 }35 vis.reset();36 pr[0] = x;37 int cr = 0;38 while (pr[cr]) {39 long long mn = INF;40 vis[cr] = 1;41 int cl = pr[cr];42 for (int i = 1; i <= n; i++) {43 if (!vis[i]) {44 const long long t1 = wl[cl] + wr[i] - g[cl][i];45 if (sl[i] > t1) {46 fa[i] = cr;47 sl[i] = t1;48 }49 mn = min(mn, sl[i]);50 }51 }52 for (int i = 0; i <= n; i++) {53 if (vis[i]) {54 wr[i] += mn;55 wl[pr[i]] -= mn;56 } 57 else sl[i] -= mn;58 }59 cr = 0;60 for (int i = 1; i <= n; i++) {61 if (!vis[i] && sl[i] == 0) {62 cr = i;63 break;64 }65 }66 }67 while (cr) {68 pr[cr] = pr[fa[cr]];69 cr = fa[cr];70 }71 }72
73 void sol() {74 tot = 0;75 for (int i = 1; i <= n; i++) get(i);76 for (int i = 1; i <= n; i++) pl[pr[i]] = i;77 for (int i = 1; i <= n; i++) tot += g[i][pl[i]];78 }79} km;80
81int main(){82 ios::sync_with_stdio(0); cin.tie(0);83 std::cin >> n1 >> m; n2 = n1;84 km.init(max(n1,n2));85 while(m--){86 int a,b,c; cin >> a >> b >> c;87 km.add(a,b,c);88 }89
90 km.sol();91
92 cout << km.tot << '\n';93// for(int i = 1;i <= n1;i++){94// if(km.g[i][km.pl[i]] > -INF) {95// std::cout << i << "->" << km.pl[i] << ' ';96// }97// }98}xxxxxxxxxx981//非完美匹配下的最大权 https://uoj.ac/problem/8023using namespace std;4
5const long long INF = 1e18;6const int N = 505;7int n1,n2,m;8
9
10struct KM {11 int n;12 int pl[N], pr[N], fa[N];13 long long g[N][N], wl[N], wr[N], sl[N];14 bitset<N> vis;15 long long tot;16
17 void init(int _n) {18 n = _n;19 for (int i = 1; i <= n; i++) {20 fill(g[i] + 1, g[i] + n + 1, 0);//边权初始化为0,本题均为正权边21 wl[i] = wr[i] = sl[i] = 0;22 pl[i] = pr[i] = fa[i] = 0;23 }24 }25
26 void add(int x, int y, long long z) {27 g[x][y] = max(g[x][y],z);28 }29
30 void get(int x) {31 for (int i = 1; i <= n; i++) {32 sl[i] = INF;33 fa[i] = 0;34 }35 vis.reset();36 pr[0] = x;37 int cr = 0;38 while (pr[cr]) {39 long long mn = INF;40 vis[cr] = 1;41 int cl = pr[cr];42 for (int i = 1; i <= n; i++) {43 if (!vis[i]) {44 const long long t1 = wl[cl] + wr[i] - g[cl][i];45 if (sl[i] > t1) {46 fa[i] = cr;47 sl[i] = t1;48 }49 mn = min(mn, sl[i]);50 }51 }52 for (int i = 0; i <= n; i++) {53 if (vis[i]) {54 wr[i] += mn;55 wl[pr[i]] -= mn;56 } 57 else sl[i] -= mn;58 }59 cr = 0;60 for (int i = 1; i <= n; i++) {61 if (!vis[i] && sl[i] == 0) {62 cr = i;63 break;64 }65 }66 }67 while (cr) {68 pr[cr] = pr[fa[cr]];69 cr = fa[cr];70 }71 }72
73 void sol() {74 tot = 0;75 for (int i = 1; i <= n; i++) get(i);76 for (int i = 1; i <= n; i++) pl[pr[i]] = i;77 for (int i = 1; i <= n; i++) tot += g[i][pl[i]];78 }79} km;80
81int main(){82 ios::sync_with_stdio(0); cin.tie(0);83 cin >> n1 >> n2 >> m;84 km.init(max(n1,n2));//相当于在节点较少的集合添加了虚拟点85 while(m--){86 int a,b,c; cin >> a >> b >> c;87 km.add(a,b,c);88 }89
90 km.sol();91
92// cout << km.tot << '\n';93// for(int i = 1;i <= n1;i++){94// if(km.g[i][km.pl[i]]) {95// std::cout << i << "->" << km.pl[i] << '\n';96// }97// }98}
转为费用流模型
新建源点s1和汇点s2。
源点向每个左部节点连一条流量为1,费用为0的边。每个
右部节点向汇点连一条流量为1,费用为0的边。对于二分图中每条左部到右部的边,连一条流量为1,费用为边权的边。
另外如果考虑非完美匹配,对于每个
左部节点还需要向汇点连一条流量为1,费用为0的边。最大费用的前提是最大流。求这个网络的最大费用最大流即可,此时网络的最大流量一定为左部节点的数量,而最大流量下的最大费用即对应一个最大权匹配方案。
时间复杂度较高,比KM算法慢了大概一个数量级,如果数据范围较大建议换KM算法。
xxxxxxxxxx981//模板 二分图最大权完美匹配 https://www.luogu.com.cn/problem/P65772//基于Dinic实现,实际并不能通过本题,只作为实现参考模版,一般只能通过节点总数<=500的图3456
7const long long INF = 1e18;8const int N = 1005,M = 1000006;//N开n1+n2+5,M开n1*n2*39int n1,n2,m;10
11int h[N],e[M],ne[M],idx;12long long c[M],w[M];13void add(int a,int b,int x,int y){14 c[idx] = y,w[idx] = x,e[idx] = b,ne[idx] = h[a],h[a] = idx++;15}16
17int s1,s2;18long long maxflow,maxcost;19long long dist[N];20int now[N];21bool vis[N];22bool spfa(){23 std::memset(vis,0,sizeof vis);24 for(int i = 0;i <= n1+n2+1;i++) dist[i] = -INF;25 std::queue<int>q;26 q.push(s1);27 dist[s1] = 0;28 now[s1] = h[s1];29 while(q.size()){30 int x = q.front();31 q.pop();32 vis[x] = 0;33 for(int i = h[x];~i;i = ne[i]){34 int y = e[i];35 if(w[i] && dist[y] < dist[x] + c[i]) {36 dist[y] = dist[x] + c[i];37 now[y] = h[y];38 if(!vis[y]) {39 vis[y] = 1;40 q.push(y);41 }42 }43 }44 }45 return dist[s2] > -INF;46}47
48long long dinic(int x,long long flow){49 if(x == s2) return flow;50 long long res = 0;51 vis[x] = 1;52 for(int i = now[x];~i && flow;i = ne[i]){53 int y = e[i];54 now[x] = i;55 if(w[i] && dist[y] == dist[x] + c[i] && !vis[y]){56 long long k = dinic(y,std::min(flow,w[i]));57 if(!k) dist[y] = 0;58 w[i] -= k;59 w[i^1] += k;60 res += k;61 flow -= k;62 maxcost += k * c[i];63 }64 }65 vis[x] = 0;66 return res;67}68
69int main(){70 std::ios::sync_with_stdio(false);std::cin.tie(0);71 std::memset(h,-1,sizeof h);72 std::cin >> n1 >> m; n2 = n1;73 s1 = 0,s2 = n1+n2+1;74 for(int i = 1;i <= m;i++){75 int x,y,z;std::cin >> x >> y >> z;76 add(x,y+n1,1,z); add(y+n1,x,0,-z);77 }78
79 for(int i = 1;i <= n1;i++){80 add(s1,i,1,0); add(i,s1,0,0);81// add(i,s2,1,0); add(i,s2,0,0); //加上即为非完美匹配下的最大权82 }83
84 for(int i = 1;i <= n2;i++){85 add(i+n1,s2,1,0); add(s2,i+n1,0,0);86 }87
88 while(spfa()) maxflow += dinic(s1,INF);89
90 std::cout << maxcost << '\n';91
92// for(int i = 1;i < idx;i += 2){93// if(w[i]){94// if(e[i] == s1 || e[i^1] == s2) continue;95// std::cout << e[i] << "->" << e[i^1] - n1 << '\n';96// }97// }98}
最小点覆盖
求出一个最小的点集S,使得任意一条边都至少有一个端点属于S
König定理:最小点覆盖=最大匹配
二分图最小覆盖的模型特点是:每条边有2个端点,二者至少选择一个。
[P6062 USACO05JAN] Muddy Fields G - 洛谷 (luogu.com.cn)
给定一个N*M的网格,其中一些地面时泥泞的,你需要用一些宽度为1,长度任意的木板将所有泥地盖住,同时不能盖住干净的地面,木板可以重叠。N,M <= 50。
每块泥地要么被第i行的一个横着的木板盖住,要么被第j列的一个竖着的木板盖住,二者至少选择一个,满足二分图最小覆盖特点。把行木板作为左边,列木板作为右边,对于每块泥地,在它所属的行木板与列木板之间连边,求出二分图的最小覆盖,即为最少得木板覆盖所有泥地的答案。
xxxxxxxxxx591234using namespace std;5const int N = 55;6int n,m;7string s[N];8int r[N][N],c[N][N],cnt;9bool st[N*N];10int match[N*N];11vector<int>e[N*N];12
13void add(int a,int b){14 e[a].emplace_back(b);15}16
17bool find(int x){18 for(int y:e[x]){19 if(!st[y]){20 st[y] = 1;21 if(!match[y] || find(match[y])){22 match[y] = x;23 return 1;24 }25 }26 }27 return 0;28}29
30int main(){31 cin >> n >> m;32 for(int i = 1;i <= n;i++){33 cin >> s[i];s[i] = ' ' + s[i];34 }35 for(int i = 1;i <= n;i++){36 for(int j = 1;j <= m;j++){37 if(!r[i][j] && s[i][j] == '*'){38 if(r[i][j-1]) r[i][j] = r[i][j-1];39 else r[i][j] = ++cnt;40 }41 }42 }43 int cnt1 = cnt;44 for(int j = 1;j <= m;j++){45 for(int i = 1;i <= n;i++){46 if(!c[i][j] && s[i][j] == '*'){47 if(c[i-1][j]) c[i][j] = c[i-1][j];48 else c[i][j] = ++cnt;49 add(r[i][j],c[i][j]);50 }51 }52 }53 int ans = 0;54 for(int i = 1;i <= cnt1;i++){55 memset(st,0,sizeof st);56 ans += find(i);57 }58 cout << ans;59}
最大独立集
选最多的点,满足两两之间没有边相连。
定理:设G是有N个点的二分图,则G的最大独立集等于顶点数N-最大匹配数
求解一个图中的最大独立集等价于求解其反图的最大团(即两两之间都有连边)。
P10939 骑士放置 - 洛谷 (luogu.com.cn)
给点N*M的棋盘,其中有一些格子禁止放旗子,问棋盘上最多能放多少个互不攻击的骑士(与象棋的马类似,攻击‘日’字)。N,M <= 100。
将棋盘黑白染色后,可以发现,一匹马可以跳到的格子的颜色一定与当前所在格子颜色相反。于是我们可以将每个位置与能跳到的位置连边,这样就构成了一个二分图(所有白色格子是一部分,所有黑色格子是一部分)。如果想让所有的马都不能互相吃,那么这个二分图里一条边的两个点最多只能选一个点放马,所以这题就是让我们在一个二分图里相邻的两个点只能选一个,问最多能选多少个点。
xxxxxxxxxx43123using namespace std;4const int N = 105;5int n,m,t;6int g[N][N];7pair<int,int> match[N][N];8bool st[N][N];9int dx[] = {-2,-1,1,2,2,1,-1,-2},dy[] = {1,2,2,1,-1,-2,-2,-1};10
11bool find(pair<int,int>p){12 auto &[x,y] = p;13 for(int i = 0;i < 8;i++){14 int nx = x + dx[i],ny = y + dy[i];15 if(nx < 1 || nx > n || ny < 1 || ny > m || g[nx][ny]) continue;16 if(!st[nx][ny]){17 st[nx][ny] = 1;18 if(!match[nx][ny].first || find(match[nx][ny])){19 match[nx][ny] = {x,y};20 return 1;21 }22 }23 }24 return 0;25}26
27int main(){28 cin >> n >> m >> t;29 for(int i = 1;i <= t;i++){30 int x,y;cin >> x >> y;31 g[x][y] = 1;32 }33
34 int ans = 0;35 for(int i = 1;i <= n;i++){36 for(int j = 1;j <= m;j++){37 if(g[i][j] || (i+j)%2) continue;38 memset(st,0,sizeof st);39 ans += find({i,j});40 }41 }42 cout << n*m-t - ans;43}
最小路径覆盖
在一个有无环向图中,找出最少得路径,使得这些路径经过了所有点。特别的,每个点自己也可以称为是路径覆盖,只不过路径的长度是0。
最小不相交路径覆盖
每一条路径经过的顶点各不相同。
原图G中每个点如果
a能直接到达b,就加边a->b这样就得到了一个G的拆点二分图G2。定理:DAG图G的最小不相交路径覆盖包含的路径条数 = n - 拆点二分图G2的最大匹配数
一开始每个点都是独立的为一条路径,总共有n条不相交路径。我们每次在二分图里找一条匹配边就相当于把两条路径合成了一条路径,也就相当于路径数减少了1。所以找到了几条匹配边,路径数就减少了多少。所以最小路径覆盖=原图的结点数-新图的最大匹配数
xxxxxxxxxx441//街道清理 https://www.luogu.com.cn/problem/UVA11842345using namespace std;6const int N = 205;7int n,m;8vector<int>e[N];9int match[N];10bool st[N];11
12bool find(int x){13 for(int y:e[x]){14 if(!st[y]){15 st[y] = 1;16 if(!match[y] || find(match[y])){17 match[y] = x;18 return 1;19 }20 }21 }22 return 0;23}24
25void sol(){26 memset(match,0,sizeof match);27 cin >> n >> m;28 for(int i = 1;i <= n;i++) e[i].clear();29 for(int i = 1;i <= m;i++){30 int a,b;cin >> a >> b;31 e[a].emplace_back(b);32 }33 int ans = 0;34 for(int i = 1;i <= n;i++){35 memset(st,0,sizeof st);36 ans += find(i);37 }38 cout << n - ans << '\n';39}40
41int main(){42 int T;cin >> T;43 while(T--) sol();44}
最小可相交路径覆盖
每一条路径经过的顶点可以相同。
思路:先用floyd求出原图的传递闭包,即如果
a能直接/间接到达b,那么就加边a->b。得到有向无环图G2,再在G2上求最小不相交路径覆盖即可。
xxxxxxxxxx541//宝藏探索 https://vjudge.net/problem/OpenJ_Bailian-2594234
5const int N = 505;6int n,m;7int g[N][N];8
9void floyd(){10 for(int k = 1;k <= n;k++){11 for(int i = 1;i <= n;i++){12 for(int j = 1;j <= n;j++){13 g[i][j] |= g[i][k]&g[k][j];14 }15 }16 }17}18
19bool vis[N];20int match[N];21bool find(int x){22 for(int y = 1;y <= n;y++){23 if(g[x][y] && !vis[y]){24 vis[y] = 1;25 if(!match[y] || find(match[y])) {26 match[y] = x;27 return 1;28 }29 }30 }31 return 0;32}33
34void sol(){35 std::memset(g,0,sizeof g);36 for(int i = 1;i <= m;i++){37 int a,b;std::cin >> a >> b;38 g[a][b] = 1;39 }40 41 floyd();42 43 int ans = 0;44 std::memset(match,0,sizeof match);45 for(int i = 1;i <= n;i++){46 std::memset(vis,0,sizeof vis);47 ans += find(i);48 }49 std::cout << n - ans << '\n';50}51
52int main(){53 while(std::cin >> n >> m,n || m) sol();54}
一般图最大匹配
一般图匹配和二分图匹配不同的是,图可能存在奇环。一般图的最大匹配可以用带花树算法(blossom alogrithm)解决。
时间复杂度在
xxxxxxxxxx931//【模板】一般图最大匹配 https://www.luogu.com.cn/problem/P61132//模版来自sse34using namespace std;5
6namespace blossom_tree {7 const int N = 1003;8 vector<int> e[N];9 int lk[N],rt[N],f[N],dfn[N],typ[N],q[N];10 int id,h,t,n;11 int lca(int u,int v) {12 ++id;13 while(1) {14 if(u){15 if (dfn[u]==id) return u;16 dfn[u]=id;u=rt[f[lk[u]]];17 }18 swap(u,v);19 }20 }21 void blm(int u,int v,int a) {22 while (rt[u]!=a) {23 f[u]=v;24 v=lk[u];25 if (typ[v]==1) typ[q[++t]=v]=0;26 rt[u]=rt[v]=a;27 u=f[v];28 }29 }30 void aug(int u) {31 while (u) {32 int v=lk[f[u]];33 lk[lk[u]=f[u]]=u;34 u=v;35 }36 }37 void bfs(int root) {38 memset(typ+1,-1,n*sizeof typ[0]);39 iota(rt+1,rt+n+1,1);40 typ[q[h=t=1]=root]=0;41 while (h<=t) {42 int u=q[h++];43 for (int v:e[u]) {44 if (typ[v]==-1) {45 typ[v]=1;f[v]=u;46 if (!lk[v]) return aug(v);47 typ[q[++t]=lk[v]]=0;48 } else if (!typ[v]&&rt[u]!=rt[v]) {49 int a=lca(rt[u],rt[v]);50 blm(v,u,a);blm(u,v,a);51 }52 } 53 }54 }55 int max_general_match(int N,vector<pair<int,int>> edges){56 n=N;id=0;57 memset(f+1,0,n*sizeof f[0]);58 memset(dfn+1,0,n*sizeof dfn[0]);59 memset(lk+1,0,n*sizeof lk[0]);60 for (int i=1;i<=n;i++) e[i].clear();61 mt19937_64 rnd(114514);62 shuffle(edges.begin(),edges.end(),rnd);63 for (auto [u,v]:edges) {64 e[u].push_back(v),e[v].push_back(u);65 if (!(lk[u]||lk[v])) lk[u]=v,lk[v]=u;66 }67 int res = 0;68 for (int i=1;i<=n;i++) if (!lk[i]) bfs(i);69 for (int i=1;i<=n;i++) res += !!lk[i];70 return res/2;71 }72}73using blossom_tree::max_general_match,blossom_tree::lk;74
75int main(){76 int n,m; std::cin >> n >> m;77 std::vector<std::pair<int,int>>e;78 while(m--){79 int a,b;std::cin >> a >> b;80 e.push_back({a,b});81 }82
83 std::cout << max_general_match(n,e) << '\n';//N:1_idx; edges:0_idx84
85 std::vector<int>ans(n+1);86 for(int i = 1;i <= n;i++){//if(lk[i]) i <-> lk[i]87 ans[i] = lk[i];88 }89
90 for(int i = 1;i <= n;i++){91 std::cout << ans[i] << ' ';//if(ans[i] == 0) not_match92 }93}
一般图最大权匹配
时间复杂度
xxxxxxxxxx2561//https://www.luogu.com.cn/problem/P669923using namespace std;4
5namespace Graph {6 const int N = 403 * 2; //两倍点数7 typedef int T; //权值大小8 const T inf = numeric_limits<int>::max() >> 1;9 struct Q { int u, v; T w; } e[N][N];10 T lab[N];11 int n, m = 0, id, h, t, lk[N], sl[N], st[N], f[N], b[N][N], s[N], ed[N], q[N];12 vector<int> p[N];1314151617 void upd(int u, int v) {18 if (!sl[v] || dvd(e[u][v]) < dvd(e[sl[v]][v])) {19 sl[v] = u;20 }21 }22 void ss(int v) {23 sl[v] = 0;24 FOR(u, n) {25 if (e[u][v].w > 0 && st[u] != v && !s[st[u]]) {26 upd(u, v);27 }28 }29 }30 void ins(int u) {31 if (u <= n) { q[++t] = u; }32 else {33 for (int v : p[u]) ins(v);34 }35 }36 void mdf(int u, int w) {37 st[u] = w;38 if (u > n) {39 for (int v : p[u]) mdf(v, w);40 }41 }42 int gr(int u, int v) {43 v = find(ALL(p[u]), v) - p[u].begin();44 if (v & 1) {45 reverse(1 + ALL(p[u]));46 return (int)p[u].size() - v;47 }48 return v;49 }50 void stm(int u, int v) {51 lk[u] = e[u][v].v;52 if (u <= n) return;53 Q w = e[u][v];54 int x = b[u][w.u], y = gr(u, x);55 for (int i = 0; i < y; i++) {56 stm(p[u][i], p[u][i ^ 1]);57 }58 stm(x, v);59 rotate(p[u].begin(), y + ALL(p[u]));60 }61 void aug(int u, int v) {62 int w = st[lk[u]];63 stm(u, v);64 if (!w) return;65 stm(w, st[f[w]]), aug(st[f[w]], w);66 }67 int lca(int u, int v) {68 for (++id; u | v; swap(u, v)) {69 if (!u) continue;70 if (ed[u] == id) return u;71 ed[u] = id;72 if (u = st[lk[u]]) u = st[f[u]];73 }74 return 0;75 }76 void add(int u, int a, int v) {77 int x = n + 1, i, j;78 while (x <= m && st[x]) ++x;79 if (x > m) ++m;80 lab[x] = s[x] = st[x] = 0;81 lk[x] = lk[a];82 p[x].clear();83 p[x].push_back(a);84 for (i = u; i != a; i = st[f[j]]) {85 p[x].push_back(i);86 p[x].push_back(j = st[lk[i]]);87 ins(j);88 }89 reverse(1 + ALL(p[x]));90 for (i = v; i != a; i = st[f[j]]) {91 p[x].push_back(i);92 p[x].push_back(j = st[lk[i]]);93 ins(j);94 }95 mdf(x, x);96 FOR(i, m) {97 e[x][i].w = e[i][x].w = 0;98 }99 memset(b[x] + 1, 0, n * sizeof b[0][0]);100 for (int u : p[x]) {101 FOR(v, m) {102 if (!e[x][v].w || dvd(e[u][v]) < dvd(e[x][v])) {103 e[x][v] = e[u][v], e[v][x] = e[v][u];104 }105 }106 FOR(v, n) {107 if (b[u][v]) { b[x][v] = u; }108 }109 }110 ss(x);111 }112 void ex(int u) {113 for (int x : p[u]) mdf(x, x);114 int a = b[u][e[u][f[u]].u], r = gr(u, a);115 for (int i = 0; i < r; i += 2) {116 int x = p[u][i], y = p[u][i + 1];117 f[x] = e[y][x].u;118 s[x] = 1;119 s[y] = sl[x] = 0;120 ss(y), ins(y);121 }122 s[a] = 1, f[a] = f[u];123 for (int i = r + 1; i < p[u].size(); i++) {124 s[p[u][i]] = -1;125 ss(p[u][i]);126 }127 st[u] = 0;128 }129 bool on(const Q &e) {130 int u = st[e.u], v = st[e.v];131 if (s[v] == -1) {132 f[v] = e.u, s[v] = 1;133 int a = st[lk[v]];134 sl[v] = sl[a] = s[a] = 0;135 ins(a);136 } else if (!s[v]) {137 int a = lca(u, v);138 if (!a) {139 return aug(u, v), aug(v, u), 1;140 } else {141 add(u, a, v);142 }143 }144 return 0;145 }146 bool bfs() {147 ms(s, -1), ms(sl, 0);148 h = 1, t = 0;149 FOR(i, m) {150 if (st[i] == i && !lk[i]) {151 f[i] = s[i] = 0;152 ins(i);153 }154 }155 if (h > t) return 0;156 while (1) {157 while (h <= t) {158 int u = q[h++];159 if (s[st[u]] == 1) continue;160 FOR(v, n) {161 if (e[u][v].w > 0 && st[u] != st[v]) {162 if (dvd(e[u][v])) upd(u, st[v]);163 else if (on(e[u][v])) return 1;164 }165 }166 }167 T x = inf;168 for (int i = n + 1; i <= m; i++) {169 if (st[i] == i && s[i] == 1) {170 x = min(x, lab[i] >> 1);171 }172 }173 FOR(i, m) {174 if (st[i] == i && sl[i] && s[i] != 1) {175 x = min(x, dvd(e[sl[i]][i]) >> s[i] + 1);176 }177 }178 FOR(i, n) {179 if (~s[st[i]]) {180 if ((lab[i] += (s[st[i]] * 2 - 1) * x) <= 0) return 0;181 }182 }183 for (int i = n + 1; i <= m; i++) {184 if (st[i] == i && ~s[st[i]]) {185 lab[i] += (2 - s[st[i]] * 4) * x;186 }187 }188 h = 1, t = 0;189 FOR(i, m) {190 if (st[i] == i && sl[i] && st[sl[i]] != i && !dvd(e[sl[i]][i]) && on(e[sl[i]][i])) {191 return 1;192 }193 }194 for (int i = n + 1; i <= m; i++) {195 if (st[i] == i && s[i] == 1 && !lab[i]) ex(i);196 }197 }198 return 0;199 }200 template<typename TT> long long work(int N, const vector<tuple<int, int, TT>> &edges) {201 ms(ed, 0), ms(lk, 0);202 n = m = N; id = 0;203 iota(st + 1, st + n + 1, 1);204 T wm = 0; long long r = 0;205 FOR(i, n) FOR(j, n) {206 e[i][j] = {i, j, 0};207 }208 for (auto [u, v, w] : edges) {209 wm = max(wm, e[v][u].w = e[u][v].w = max(e[u][v].w, (T)w));210 }211 FOR(i, n) { p[i].clear(); }212 FOR(i, n) FOR(j, n) {213 b[i][j] = i * (i == j);214 }215 fill_n(lab + 1, n, wm);216 while (bfs()) {};217 FOR(i, n) if (lk[i]) {218 r += e[i][lk[i]].w;219 }220 return r / 2;221 }222 auto match() {223 vector<array<int, 2>> ans;224 FOR(i, n) if (lk[i]) {225 ans.push_back({i, lk[i]});226 }227 return ans;228 }229} // namespace Graph230using Graph::work, Graph::match;231
232void soviet(){233 int n,m; std::cin >> n >> m;234 std::vector<std::tuple<int,int,long long>>edges(m);235 for(auto &[a,b,c]:edges){236 std::cin >> a >> b >> c;237 }238 std::cout << work(n,edges) << '\n';//n:1_idx; edges:0_idx239
240 std::vector<int>ans(n+1);241 auto v = match();242
243 for(auto [x,y]:v){// x <-> y244 ans[x] = y;245 }246 for(int i = 1;i <= n;i++){247 std::cout << ans[i] << ' ';//if(ans[i] = 0) not_match248 }249}250
251int main() {252 int M_T = 1;std::ios::sync_with_stdio(false),std::cin.tie(0);253// std::cin >> M_T;254 while(M_T--){ soviet(); }255 return 0;256}
基环树
n个点n条边组成的无向连通图。若不保证连通,也有可能是内/外向树/森林。
找环
可以以拓扑排序或者dfs实现: P8655 发现环 - 洛谷
基环树的直径
直径有两种情况,二者取最大值即为答案:
直径处于以环上某一点为根节点,且不经过环边的子树中。对每一颗子树进行树形DP求直径,取最大值。
经过环边,链接两个根节点x,y的子树。ans = max{d1[x]+d1[y]+dis(x,y)}。d1[i]表式以i为根节点向下能达到的最大距离,dis(i,j)表示环上两点之间的最远距离,断链成环,复制拼接,再用前缀和+单调队列优化即可O(N)求得答案。
一般处理方法
找到唯一的环;
对环之外的部分按照若干棵树处理;
考虑与环一起计算。
负环
SPFA判负环
如果某点的最短路所包含的边数大于等于n,则说明存在负环
xxxxxxxxxx551//https://www.acwing.com/activity/content/problem/content/921/2//判断有向图中是否存在负环3456using namespace std;7const int N = 100005;8int n, m;9queue<int>q;10int h[N], e[N], ne[N], idx, w[N];11int dist[N], cnt[N];//dist记录1~k最短距离,cnt记录1~k边的数量12bool st[N];13
14void add(int a, int b, int c) {15 w[idx] = c, e[idx] = b, ne[idx] = h[a], h[a] = idx++;16}17
18int SPFA() {19 //判负环不需要初始化dist,如果存在负环,那么dist不管初始化为多少,都会被更新20 //将所有点进入队列。如果只加源点可能到不了有负环的点,只能说明从源点出发不能抵达负环,而不能说明图上不存在负环。21 for (int i = 1; i <= n;i++) { 22 st[i] = 1;23 q.push(i);24 }25
26 while (q.size()){27 int t = q.front();28 q.pop();29 st[t] = 0;30 for (int i = h[t]; i != -1;i = ne[i]) {31 int k = e[i];32 if (dist[k] > dist[t] + w[i]) {33 dist[k] = dist[t] + w[i];34 cnt[k] = cnt[t] + 1;//更新35 if (cnt[k] >= n) return 1;//如果到k经过的边的数量>=总点数说明存在负环36 if (!st[k]) {37 q.push(k);38 st[k] = 1;39 }40 }41 }42 }43 return 0;44}45
46int main() {47 memset(h, -1, sizeof h);48 cin >> n >> m;49 while (m--){50 int a, b, c; cin >> a >> b >> c;51 add(a, b, c);52 }53 if(SPFA()) puts("Yes");54 else puts("No");55}
bellman-ford判负环
bellman-ford判负环,据bellman-ford的性质最后得出的是通过不超过n-1条边的从起点到其他点的最短距离
但是如果在n-1次循环之后仍然存在边可以被松弛,那么就存在负环(因为如果没有负环n-1次就已经确定了最短距离,具体可参考bellman-ford证明,已经是最短距离了还能被松弛,必然是存在负环)
xxxxxxxxxx35123using namespace std;4const int N = 2003,M = 10004;5int n,m;6int dist[N];7
8struct Edge{9 int a,b,c;10}e[M];11
12bool bellman(){13 memset(dist,0x3f,sizeof dist); 14 dist[1] = 0;15
16 for(int i = 1;i <= n-1;i++){17 for(int j = 1;j <= m;j++){18 auto &[a,b,c] = e[j];19 dist[b] = min(dist[b],dist[a] + c);20 }21 }22 for(int i = 0;i <= m;i++){//如果在n-1次循环之后仍然存在边可以被松弛,那么就存在负环23 auto &[a,b,c] = e[i];24 if(dist[b] > dist[a] + c) return 1;25 }26 return 0;27}28int main(){29 cin >> n >> m;30 for(int i = 1;i <= m;i++){31 cin >> e[i].a >> e[i].b >> e[i].c;32 }33 if(bellman()) cout << "Yes";34 else cout << "No";35}
差分约束
求解差分约束系统,有
| 题意 | 转化 | 连边 |
|---|---|---|
add(a, b, -c); | ||
add(b, a, c); | ||
add(b, a, 0), add(a, b, 0); |
判断负环,如果存在负环,则无解。
建立一个虚拟的源点0,设
一般使用 SPFA 判断图中是否存在负环,最坏时间复杂度为
xxxxxxxxxx641//小 K 的农场 https://www.luogu.com.cn/problem/P19932345using namespace std;6const int N = 20004;7int n,m;8int h[N],e[N],ne[N],w[N],idx;9int dist[N],cnt[N];10bool st[N];11
12void add(int a,int b,int c){13 w[idx] = c,e[idx] = b,ne[idx] = h[a],h[a] = idx++;14}15
16bool spfa(){17 std::memset(dist,0x3f,sizeof dist);18 queue<int>q;19 dist[0] = 0;20 q.push(0);21 while(q.size()){22 int t = q.front();23 q.pop();24 st[t] = 0;25 for(int i = h[t];~i;i = ne[i]){26 int k = e[i];27 if(dist[k] > dist[t] + w[i]){28 dist[k] = dist[t] + w[i];29 cnt[k] = cnt[t] + 1;30 if(cnt[k] >= n+1) return 1;//(加入源点0后有n+1个点)31 if(!st[k]){32 q.push(k);33 st[k] = 1;34 }35 }36 }37 }38 return 0;39}40
41int main(){42 memset(h,-1,sizeof h);43 cin >> n >> m;44 for(int i = 1;i <= m;i++){45 int op,a,b,c;cin >> op;46 if(op == 1){47 cin >> a >> b >> c;48 add(a,b,-c);49 }50 else if(op == 2){51 cin >> a >> b >> c;52 add(b,a,c);53 }54 else{55 cin >> a >> b;56 add(a,b,0);add(b,a,0);57 }58 }59 for(int i = 1;i <= n;i++) add(0,i,0);//建立超级源点0,跑单源spfa60 //也可以不建源点,直接将所有结点入队跑全源spfa61
62 if(spfa()) cout << "No";//诺存在负环,则无解63 else cout << "Yes";//否则dist[]为一组解64}
连通性相关
割点和割边
dfn[x]为每个点dfs第一次被访问的时间戳
low[x]为以下节点的时间戳的最小值 1.x的子树中的节点 2.通过一条不在搜索树上的边,能够到达x的子树的节点
从根开始的一条路径上的 dfn 严格递增,low 非严格递增
xxxxxxxxxx111//Tarjan2void Tarjan(int u) {3 low[u] = dfn[u] = ++dn; // low 初始化为当前节点 dfn4 for (int v : G[u]) { // 遍历 u 的相邻节点5 if (!dfn[v]) { // 如果未访问过6 Tarjan(v); // 递归7 low[u] = std::min(low[u], low[v]); // 未访问的和 low 取 min8 } else9 low[u] = std::min(low[u], dfn[v]); // 已访问的和 dfn 取 min10 }11}
割点
对于一个无向图,如果把一个点删除后这个图的极大连通分量数增加了,那么这个点就是这个图的割点(又称割顶)。
诺x不是搜索树的根节点root(dfs的起点),则x是割点当且仅当搜索树上存在一个x的子节点满足dfn[x] <= low[y]
诺x是搜索树的根节点,则x是割点当且仅当搜索树上存在至少两个子节点y1,y2满足上式
xxxxxxxxxx461//P3388 【模板】割点(割顶) https://www.luogu.com.cn/problem/P3388234using namespace std;5const int N = 200005;6int n,m;7int h[N],e[N],ne[N],idx;8int dfn[N],low[N],cut[N],dn,root;9
10void add(int a,int b){11 e[idx] = b,ne[idx] = h[a],h[a] = idx++;12}13
14void tarjan(int x){15 dfn[x] = low[x] = ++dn;16 int son = 0;17 for(int i = h[x];~i;i = ne[i]){18 int y = e[i];19 if(!dfn[y]){20 tarjan(y);21 son++;22 low[x] = min(low[x],low[y]);23 if(x != root && dfn[x] <= low[y]) cut[x] = 1;24 }25 else low[x] = min(low[x],dfn[y]);26 }27 if(x == root && son >= 2) cut[x] = 1;28}29
30int main(){31 memset(h,-1,sizeof h);32 cin >> n >> m;33 for(int i = 1;i <= m;i++){34 int a,b;cin >> a >> b;35 add(a,b);add(b,a);36 }37 int cnt = 0;38 for(int i = 1;i <= n;i++){39 if(!dfn[i]) root = i,tarjan(i);40 if(cut[i]) cnt++;41 }42 cout << cnt << endl;43 for(int i = 1;i <= n;i++){44 if(cut[i]) cout << i << ' ';45 }46}
割边
对于一个无向图,如果删掉一条边后图中的连通分量数增加了,则称这条边为桥或者割边。
无向边(x,y)是割边,当且仅当搜索树上存在x的一个子节点y满足dfn[x] < low[y]
xxxxxxxxxx541//https://www.luogu.com.cn/problem/U631102//可处理重边345using namespace std;6const int N = 100005;7int n,m;8int h[N],e[N],ne[N],idx;9int dfn[N],low[N],cut[N],dn;10
11void add(int a,int b){//(0 1),(2 3),(4 5)...(x y) x和x^1为两条方向相反的边12 e[idx] = b,ne[idx] = h[a],h[a] = idx++;13}14
15void tarjan(int x,int edge){16 dfn[x] = low[x] = ++dn;17 for(int i = h[x];~i;i = ne[i]){18 int y = e[i];19 if(!dfn[y]){20 tarjan(y,i);21 low[x] = min(low[x],low[y]);22 if(dfn[x] < low[y]){23 cut[i] = cut[i^1] = 1;24 }25 }26 else if(i != (edge^1)) low[x] = min(low[x],dfn[y]);27 }28}29
30void sol(){31 memset(h,-1,sizeof h);32 for(int i = 1;i <= m;i++){33 int a,b;cin >> a >> b;34 add(a,b);add(b,a);//正反边一定要同时添加35 }36 for(int i = 1;i <= n;i++){37 if(!dfn[i]) tarjan(i,0);38 }39 int cnt = 0;40 for(int i = 0;i < idx;i+=2){41 if(cut[i]) cnt++;//e[i]到e[i^1]为一条割边42 }43 cout << cnt << '\n';44}45
46int main(){47 while(cin >> n >> m,n && m){48
49 for(int i = 1;i <= n;i++) dfn[i] = low[i] = 0;50 for(int i = 0;i < idx;i++) cut[i] = 0;//注意cut清空范围51 idx = dn = 0;52 sol();53 }54}
双连通分量
在一张连通的无向图中,对于两个点
在一张连通的无向图中,对于两个点
边双连通具有传递性,即,若
点双连通不具有传递性,反例如下图,
对于一个无向图中的 极大 边双连通的子图,我们称这个子图为一个 边双连通分量。
对于一个无向图中的 极大 点双连通的子图,我们称这个子图为一个 点双连通分量。
边双连通分量
不存在割边。
e-DCC的求法比较简单,先求出无向图中的所有割边,把割边删除后,剩下的图会分成若干个连通块,每一个连通块就是一个“边双连通分量”。
xxxxxxxxxx711//https://www.luogu.com.cn/problem/P84362345using namespace std;6const int N = 500005,M = 4000006;7int n,m;8int h[N],e[M],ne[M],idx;9int dfn[N],low[N],dn;10bool cut[M];11int cnt;12bool st[N];13vector<int>v[N];14
15void add(int a,int b){16 e[idx] = b,ne[idx] = h[a],h[a] = idx++;17}18
19void tarjan(int x,int edge){20 dfn[x] = low[x] = ++dn;21 for(int i = h[x];~i;i = ne[i]){22 int y = e[i];23 if(!dfn[y]){24 tarjan(y,i);25 low[x] = min(low[x],low[y]);26 if(dfn[x] < low[y]){27 cut[i] = cut[i^1] = 1;28 }29 }30 else if(i != (edge^1)) low[x] = min(low[x],dfn[y]);31 }32}33
34void dfs(int u){35 st[u] = 1;36 for(int i = h[u];~i;i = ne[i]){37 int k = e[i];38 if(st[k] || cut[i]) continue;39 dfs(k);40 v[cnt].emplace_back(k);41 }42}43
44int main(){45 memset(h,-1,sizeof h);46 cin >> n >> m;47 for(int i = 1;i <= m;i++){48 int a,b;scanf("%d %d",&a,&b);49 add(a,b);add(b,a);50 }51
52 for(int i = 1;i <= n;i++){53 if(!dfn[i]) tarjan(i,0);54 }55
56 for(int i = 1;i <= n;i++){57 if(!st[i]) {58 cnt++;59 v[cnt] = {i};60 dfs(i);61 }62 }63 cout << cnt << endl;64 for(int i = 1;i <= cnt;i++){65 printf("%d ",v[i].size());66 for(auto x:v[i]){67 printf("%d ",x);68 }69 printf("\n");70 }71}
点双连通分量
不存在割点
一张无向图是点双连通图,当且仅当满足下列两个条件之一:
图的顶点数不超过 2。
图中任意两点都同时包含在至少一个简单环中。其中“简单环”指的是不自交的环,也就是我们通常画出的环。
xxxxxxxxxx691//【模板】点双连通分量 https://www.luogu.com.cn/problem/P843523456using namespace std;7const int N = 500005,M = 4000006;8int n,m;9int h[N],e[M],ne[M],idx;10int dfn[N],low[N],dn,root;11bool cut[N];12stack<int>sk;13vector<int>v[N];14int cnt;15
16void add(int a,int b){17 e[idx] = b,ne[idx] = h[a],h[a]= idx++;18}19
20void tarjan(int x){21 dfn[x] = low[x] = ++dn;22 sk.emplace(x);23 if(x == root && h[x] == -1) {//处理孤立点24 v[++cnt].emplace_back(x);25 return;26 }27 int son = 0;28 for(int i = h[x];~i;i = ne[i]){29 int y = e[i];30 if(!dfn[y]){31 tarjan(y);32 son++;33 low[x] = min(low[x],low[y]);34 if(dfn[x] <= low[y]){35 if(x != root || son >= 2) cut[x] = 1;36 v[++cnt].emplace_back(x);37 v[cnt].emplace_back(y);38 while(sk.top() != y){39 v[cnt].emplace_back(sk.top());40 sk.pop();41 }42 sk.pop();43 }44 }45 else low[x] = min(low[x],dfn[y]);46 }47}48
49int main(){50 memset(h,-1,sizeof h);51 cin >> n >> m;52 for(int i = 1;i <= m;i++){53 int a,b;scanf("%d %d",&a,&b);54 if(a == b) continue;55 add(a,b);add(b,a);56 }57
58 for(int i = 1;i <= n;i++){59 if(!dfn[i]) root = i,tarjan(i);60 }61 cout << cnt << endl;62 for(int i = 1;i <= cnt;i++){63 printf("%d ",v[i].size());64 for(auto x:v[i]){65 printf("%d ",x);66 }67 printf("\n");68 }69}
强连通分量
有向图强连通分量的Tarjan算法 (byvoid.com)
给点一张有向图,诺对于图中任意两个节点相互可达,则称该有向图是“强连通图” 有向图的极大强连通子图被称为“强连通分量”,简记为SCC。
一个环一定是强连通图。Tarjan算法基本思路就是对于每个点,尽可能找到与它一起能构成环的所有节点。每个强连通分量为搜索树中的一棵子树。搜索时,把当前搜索树中未处理的节点加入一个栈,回溯时可以判断栈顶到栈中的节点是否为一个强连通分量
xxxxxxxxxx681//B3609 [图论与代数结构 701] 强连通分量 https://www.luogu.com.cn/problem/B3609234567using namespace std;8const int N = 10004,M = 200005;9int n,m;10
11int h[N],e[M],ne[M],idx;12void add(int a,int b){13 e[idx] = b,ne[idx] = h[a],h[a] = idx++;14}15
16vector<int>v[N];//第i个强连通分量包含的节点17int dfn[N],low[N],dn;18int id[N],idn;//节点i所在强连通分量scc19stack<int>sk;20void tarjan(int x){21 dfn[x] = low[x] = ++dn;22 sk.push(x);23 for(int i = h[x];~i;i = ne[i]){24 int y = e[i];25 if(!dfn[y]){//如果y未被搜索过,递归搜索y26 tarjan(y);27 low[x] = min(low[x],low[y]);28 }29 else if(!id[y]){//如果y被搜索过,且不属于任何一个强连通分量(则y在栈中),用dfn[y]更新low[x]30 low[x] = min(low[x],dfn[y]);31 }32 }33 if(dfn[x] == low[x]){34 id[x] = ++idn;35 v[idn].emplace_back(x);36 while(sk.top() != x){37 id[sk.top()] = idn;38 v[idn].emplace_back(sk.top());39 sk.pop();40 }41 sk.pop();42 }43}44
45int main(){46 memset(h,-1,sizeof h);47 cin >> n >> m;48 for(int i = 1;i <= m;i++){49 int a,b;cin >> a >> b;50 add(a,b);51 }52
53 for(int i = 1;i <= n;i++){54 if(!dfn[i]) tarjan(i);55 }56 57 for(int i = 1;i <= idn;i++) sort(v[i].begin(),v[i].end());58 cout << idn << '\n';59 vector<int>st(n+1);60 for(int i = 1;i <= n;i++){61 if(st[i]) continue;62 for(auto x:v[id[i]]){63 cout << x << ' ';64 st[x] = 1;65 }66 cout << '\n';67 }68}
常应用缩点消除环的影响,转化为DAG图解决
在缩点后的DAG中,强连通分量的标号顺序是其拓扑序的逆序,出度为0的点标号标号越小,即反图的拓扑序。
2-SAT
有N个变量,每个变量只有两种可能得取值。再给点M个条件,每个条件都是对两个变量的取值限制。求是否存在对N个变量的合法赋值,使得M个条件均得到满足。
设一个变量
2-SAT问题的判断方法如下(时间复杂度O(N+M)):
建立2N个节点的有向图,每个变量
对应两个节点i和i+n。考虑每个条件,形如“诺
的赋值为 ,则 的赋值必须为 ”, ,从i+p*n到j+q*n连一条有向边。 注意上述条件蕴含着它的逆否命题“诺 的赋值为 ,则 的赋值必须为 ”。如果在给点的M个限制条件中,原命题和逆否命题不一定成对出现,应该从j+(1-q)*n到i+(1-p)*n也连一条有向边。用Tarjan算法求出有向图中所有的强连通分量。
诺存在
满足i和i+n属于同一个强连通分量,则出现了矛盾:诺变量 赋值为 ,则变量 必须赋值为 ,说明问题无解。诺问题还要求具体解,Tarjan后同一元素拆点强连通分量编号小的点即为合法点,如果一个元素拆成的两个点之间没有任何路径相连,即使是有向路径,那么这两点都可以成为合法点,这两点的分量编号不同,选小的即可
P4782 【模板】2-SAT - 洛谷 (luogu.com.cn)
N个变量,M个条件:
xxxxxxxxxx741234using namespace std;5const int N = 2000006;6int n,m;7int h[N],e[N],ne[N],idx;8int dfn[N],low[N],dn;9int id[N],idn;10stack<int>sk;11
12void add(int a,int b){13 e[idx] = b,ne[idx] = h[a],h[a] = idx++;14}15
16void tarjan(int x){17 dfn[x] = low[x] = ++dn;18 sk.push(x);19 for(int i = h[x];~i;i = ne[i]){20 int y = e[i];21 if(!dfn[y]) {22 tarjan(y);23 low[x] = min(low[x],low[y]);24 }25 else if(!id[y]) low[x] = min(low[x],dfn[y]);26 }27 if(low[x] == dfn[x]){28 ++idn;29 id[x] = idn;30 while(sk.top() != x){31 id[sk.top()] = idn;32 sk.pop();33 }34 sk.pop();35 }36}37
38int main(){39 memset(h,-1,sizeof h);40 scanf("%d %d",&n,&m);41 while(m--){42 int a,x,b,y;scanf("%d %d %d %d",&a,&x,&b,&y);43 if(x == 0 && y == 0){44 add(a+n,b);45 add(b+n,a);46 }47 if(x == 0 && y == 1){48 add(a+n,b+n);49 add(b,a);50 }51 if(x == 1 && y == 0){52 add(a,b);53 add(b+n,a+n);54 }55 if(x == 1 && y == 1){56 add(a,b+n);57 add(b,a+n);58 }59 }60 for(int i = 1;i <= n << 1;i++){//2n层循环61 if(!dfn[i]) tarjan(i);62 }63
64 for(int i = 1;i <= n;i++){65 if(id[i] == id[i+n]) {//属于同一个强连通分量,无解66 cout << "IMPOSSIBLE";67 return 0;68 }69 }70 cout << "POSSIBLE\n";71 for(int i = 1;i <= n;i++){//这里a[i]为0,a[i+n]为1,选所在强连通分量小的为合法解72 cout << (id[i] > id[i+n]) << ' ';73 }74}P10969 Katu Puzzle - 洛谷 (luogu.com.cn)
网络流
一个网络G=(V,E),是一张有向图,图中每条边 c(x,y) ,称为边的容量,特别的诺c(x,y) = 0 。图中还有两个指定的特殊节点 源点S 和 汇点T (S != T)。
设f(x,y) 是定义在节点二元组
容量限制:
f(x,y) <= c(x,y)斜对称:
f(x,y) = -f(y,x)流量守恒:c
f()称为网络的流量函数,对于f(x,y)称为边的流量,c(x,y) - f(x,y)称为边的剩余容量
最大流
对于一个给定的网络,合法流函数f()有很多。其中使得整个网络的流量最大的流函数被称为网络的最大流。
建边时反边容量全为0
Edmondsd-Karp
时间复杂度
每次bfs遍历整个残量网络,找出任意一条增广路,同时计算路径上各边剩余流量的最小值minf,显然可以让一股流沿着增广路从S流向T,网络的流量就可以增加minf。直到网络上不存在增广路为止。
xxxxxxxxxx621//模版 https://www.luogu.com.cn/problem/P33762345using namespace std;6const int N = 205,M = 10004;7const int INF = 0x3f3f3f3f;8int n,m,s,t;9long long maxflow;10int h[N],e[M],ne[M],idx;11long long w[M];//容量12long long rest[N];//记录各边剩余容量的最小值13int pre[M];//记录点x是从哪条边过来的14bool vis[N];15
16void add(int x,int y,int z){17 w[idx] = z,e[idx] = y,ne[idx] = h[x],h[x] = idx++;18}19
20bool bfs(){21 memset(vis,0,sizeof vis);22 queue<int>q;23 q.push(s);24 vis[s] = 1;25 rest[s] = INF;26 while(q.size()){27 int x = q.front();28 q.pop();29 for(int i = h[x];~i;i = ne[i]){30 int y = e[i];31 if(vis[y] || w[i] == 0) continue;32 rest[y] = min(rest[x],w[i]);33 pre[y] = i;34 q.push(y);35 vis[y] = 1;36 if(y == t) return 1;37 }38 }39 return 0;40}41
42int update(){//将流过的路径容量减少minf,反边增加minf43 int x = t;44 while(x != s){45 int i = pre[x];46 w[i] -= rest[t];47 w[i^1] += rest[t];48 x = e[i^1];49 }50 return rest[t];//网络的流量可以增加最小值minf51}52
53int main(){54 memset(h,-1,sizeof h);55 cin >> n >> m >> s >> t;56 for(int i = 1;i <= m;i++){57 int x,y,z;cin >> x >> y >> z;58 add(x,y,z);add(y,x,0);59 }60 while(bfs()) maxflow += update();61 cout << maxflow;62}
Dinic
时间复杂度
EK算法每次只找出一条增广路,而Dinic可以一次找出多条。
在[残量网络]上bfs求出节点的层次,构造[分层图]。
在分层图上dfs寻找增广路,回溯时更新边权。
xxxxxxxxxx641234using namespace std;5const int INF = 0x3f3f3f3f;6const int N = 205,M = 10004;7int n,m,s,t;8long long maxflow;9int h[N],e[M],ne[M],w[M],idx;10int d[N];11int now[N];//当前弧优化12
13void add(int a,int b,int c){14 w[idx] = c,e[idx] = b,ne[idx] = h[a],h[a] = idx++;15}16
17bool bfs(){//在残量网络上构造分层图18 memset(d,0,sizeof d);19 queue<int>q;20 q.push(s);21 d[s] = 1;22 now[s] = h[s];23 while(q.size()){24 int x = q.front();25 q.pop();26 for(int i = h[x];~i;i = ne[i]){27 int y = e[i];28 if(!w[i] || d[y]) continue;29 d[y] = d[x] + 1;30 now[y] = h[y];31 q.push(y);32 if(y == t) return 1;33 }34 }35 return 0;36}37
38int dfs(int x,int flow){//flow表示经过该点的剩余流量39 if(x == t) return flow;40 int res = 0;//res表示经过该点的所有流量和41 for(int i = now[x];~i && flow;i = ne[i]){//只有当前还有流时才继续搜索42 now[x] = i;//当前弧优化43 int y = e[i];44 if(!w[i] || d[y] != d[x] + 1) continue;45 int k = dfs(y,min(flow,w[i]));46 if(k == 0) d[y] = 0;//剪枝,去掉增广完毕的点47 w[i] -= k;48 w[i^1] += k;49 res += k;50 flow -= k;51 }52 return res;53}54
55int main(){56 memset(h,-1,sizeof h);57 cin >> n >> m >> s >> t;58 for(int i = 1;i <= m;i++){59 int a,b,c;cin >> a >> b >> c;60 add(a,b,c);add(b,a,0);61 }62 while(bfs()) maxflow += dfs(s,INF);63 cout << maxflow;64}
最小割
诺删除一个边集
最大流最小割定理:任何一个网络的最大流量等于最小割中边的容量之和,即“最大流=最小割”
求具体方案
在求完最大流后,从源点S出发,BFS遍历残量网络中还能到达的点,这些点属于S集合,剩下无法到达的点属于T集合。所有连接S和T的边即为属于最小割的边。
xxxxxxxxxx231bool st[N] = {};//st[x]=1则属于S,否则属于T2void uuz(){3 queue<int>q;4 q.push(s);5 st[s] = 1;6 while(q.size()){7 int x = q.front();8 q.pop();9 for(int i = h[x];~i;i = ne[i]){10 int y = e[i];11 if(!st[y] && w[i]){12 q.push(y);13 st[y] = 1;14 }15 }16 }17 for(int i = 0;i < idx;i += 2){18 int x = e[i^1],y = e[i];19 if(st[x] && !st[y]){//此时w[i] == 020 cout << x << ' ' << y << endl;//边(x,y)属于最小割21 }22 }23}
求割边数量
如果要在最小割的前提下最小化个边数量,那么先求出最小割,然后把没有满流的边容量改为INF,满流的边容量改为1,再跑一边最小割就可以求出最小割边数量; 如果没有最小割的前提,直接把所有边的容量设为1,求一遍最小割即可。
常见问题模型
有n个物品和两个集合A,B,每个物品必须且只能放入一个集合里,如果一个物品没有放入A集合会花费
这是一个经典的二者选一的最小割题目。我们设立一个超级源点s和超级汇点t,对每个点i连边add(s,i,a[i])和add(i,t,b[i]),反边容量均为0。对每个限制条件连双向边add(u,v,w),add(v,u,w)。
注意到,但源点和汇点不相连时,代表这些点都选择了一个其中集合,最小割就是最小花费。如果要求最大花费,将所有花费减去最小割即可。
例题:P1361 小M的作物 - 洛谷 (luogu.com.cn)
最大权闭合图
给定一张有向图,每个点都有一个权值a[i](可能 <= 0),你需要选择权值和最大的一组点集,满足:每个点的后继节点都必须包含在点集中。
结论:建立超级源点s和超级汇点t,诺节点u权值为正,则连边(s,u,a[i]);诺节点u权值为负,则连边(u,t,-a[i]);权值为0不用连,原图上所有边权改为
例题:Get More Money(★7) - AtCoder typical90_an - Virtual Judge (vjudge.net)
也可用于处理不连通的有向森林
xxxxxxxxxx391234using namespace std;5const int INF = 0x3f3f3f3f;6const int N = 105,M = N*N;7int n,m;8int h[N],e[M],ne[M],idx,w[M];9int a[N];10
11int main(){12 memset(h,-1,sizeof h);13 cin >> n >> m;14 s = 0,t = n+1;15 for(int i = 1;i <= n;i++){16 cin >> a[i];17 a[i] -= m;18 if(a[i] > 0){19 ans += a[i];20 add(s,i,a[i]);21 add(i,s,0);22 }23 if(a[i] < 0){24 add(i,t,-a[i]);25 add(t,i,0);26 }27 }28
29 for(int i = 1;i <= n;i++){30 int k;cin >> k;31 while(k--){32 int j;cin >> j;33 add(j,i,INF);34 add(i,j,0);35 }36 }37 while(bfs()) maxflow += dfs(s,INF);//Dinic最大流板子省略38 cout << ans - maxflow;39}
费用流
给定一个网络c(x,y)外,还有一个单位流量的费用w(x,y),当(x,y)的流量为f(x,y)时,需要花费f(x,y) * w(x,y)的费用。w也满足斜对称性质,即w(x,y) = -w(y,x)。
使该网络中花费最小的最大流称为最小费用最大流。注意:费用流的前提是最大流。
SSP算法(Successive Shortest Path):每次寻找单位费用最小的增广路进行增广,直到图上不存在增广路为止。时间复杂度最坏为f表示网络的最大流。实现时,只需将 EK 算法或 Dinic 算法中找增广路的过程,替换为用最短路算法寻找单位费用最小的增广路即可。需要保证网络没有负环。
P3381 【模板】最小费用最大流 - 洛谷 (luogu.com.cn)
xxxxxxxxxx691//基于EK算法实现2345using namespace std;6const int INF = 0x3f3f3f3f;7const int N = 5003,M = 100005;8int n,m,s,t;9int maxflow,mincost;10int h[N],e[M],ne[M],w[M],c[M],idx;//w为容量,c为费用11bool vis[N];12int pre[M];13int rest[N];14int dist[N];15
16void add(int u,int v,int x,int y){17 c[idx] = y,w[idx] = x,e[idx] = v,ne[idx] = h[u],h[u] = idx++;18}19
20bool spfa(){21 memset(vis,0,sizeof vis);22 memset(dist,0x3f,sizeof dist);23 queue<int>q;24 q.push(s);25 vis[s] = 1;26 rest[s] = INF;27 dist[s] = 0;28 while(q.size()){29 int x = q.front();30 q.pop();31 vis[x] = 0;32 for(int i = h[x];~i;i = ne[i]){33 int y = e[i];34 if(w[i] && dist[y] > dist[x] + c[i]){35 dist[y] = dist[x] + c[i];36 rest[y] = min(rest[x],w[i]);37 pre[y] = i;38 if(!vis[y]){39 q.push(y);40 vis[y] = 1;41 }42 }43 }44 }45 return dist[t] < INF;46}47
48void update(){49 int x = t;50 while(x != s){51 int i = pre[x];52 w[i] -= rest[t];53 w[i^1] += rest[t];54 x = e[i^1];55 }56 mincost += rest[t] * dist[t];57 maxflow += rest[t];58}59
60int main(){61 memset(h,-1,sizeof h);62 cin >> n >> m >> s >> t;63 for(int i = 1;i <= m;i++){64 int u,v,x,y;cin >> u >> v >> x >> y;65 add(u,v,x,y);add(v,u,0,-y);//费用取反66 }67 while(spfa()) update();68 cout << maxflow << ' ' << mincost;69}xxxxxxxxxx741//基于Dinic算法实现2345using namespace std;6const int INF = 0x3f3f3f3f;7const int N = 5003,M = 100005;8int n,m,s,t;9int maxflow,mincost;10int h[N],e[M],ne[M],w[M],c[M],idx;11int now[N],dist[N];12bool vis[N];13
14void add(int a,int b,int x,int y){//w[] = x为容量,c[] = y为费用15 c[idx] = y,w[idx] = x,e[idx] = b,ne[idx] = h[a],h[a] = idx++;16}17
18bool spfa(){19 memset(vis,0,sizeof vis);20 memset(dist,0x3f,sizeof dist);21 queue<int>q;22 q.push(s);23 now[s] = h[s];24 dist[s] = 0;25 while(q.size()){26 int x = q.front();27 q.pop();28 vis[x] = 0;29 for(int i = h[x];~i;i = ne[i]){30 int y = e[i];31 if(w[i] && dist[y] > dist[x] + c[i]){32 dist[y] = dist[x] + c[i];33 now[y] = h[y];34 if(!vis[y]){35 q.push(y);36 vis[y] = 1;37 }38 }39 }40 }41 return dist[t] < INF;42}43
44int dfs(int x,int flow){45 if(x == t) return flow;46 vis[x] = 1;//47 int res = 0;48 for(int i = now[x];~i && flow;i = ne[i]){49 now[x] = i;50 int y = e[i];51 if(w[i] && dist[y] == dist[x] + c[i] && !vis[y]){52 int k = dfs(y,min(w[i],flow));53 if(!k) dist[y] = 0;54 w[i] -= k;55 w[i^1] += k;56 res += k;57 flow -= k;58 mincost += k*c[i];//最小费用 += 当前流量*当前费用59 }60 }61 vis[x] = 0;62 return res;63}64
65int main(){66 memset(h,-1,sizeof h);67 cin >> n >> m >> s >> t;68 for(int i = 1;i <= m;i++){69 int a,b,x,y;cin >> a >> b >> x >> y;70 add(a,b,x,y);add(b,a,0,-y);//费用取反71 }72 while(spfa()) maxflow += dfs(s,INF);73 cout << maxflow << ' ' << mincost;74}
欧拉图
欧拉路:给定一张无向图,诺存在一条从节点S到节点T的路径,恰好不重不漏地经过每条边一次,则称该路径为S到T的欧拉路
欧拉回路:特别地,诺存在一条从节点S出发,恰好不重不漏地经过每一条边一次(可以重复经过图中的节点),最终回到起点S,则称该路径为欧拉回路。存在欧拉回路的图被称为欧拉图。
欧拉路的存在性判断:一张无向图中存在欧拉路,当且仅当无向图连通,且起点和终点的度数为奇数,其它节点的度数为偶数。
使用DFS寻找欧拉路的基本思想如下:
DFS寻找到第一个无边可走的节点,则这个节点必定为终点。
接下来由于DFS的递归回溯,会退回终点的上一个节点,继续往下搜索,直到寻找到第二个无边可走的节点,则这个节点必定为欧拉路中终点前最后访问的节点。
于是当通过DFS遍历完整张图后,就可以倒序储存下整个欧拉路。该算法时间复杂度为O(NM),因为一个节点会被遍历多次,且递归层数为O(M)级别,容易栈溢出。可以使用栈模拟递归,且每次访问一条边后删除该边(修改表头,令其指向下一条边)
xxxxxxxxxx491//诺欧拉回路存在,则可用dfs和栈求出欧拉回路的一种具体方案23456using namespace std;7const int N = 10004,M = 100005;8int n,m;9int h[N],e[M],ne[M],idx;10bool vis[M];11stack<int>sk;12vector<int>ans;13
14void add(int a,int b){15 e[idx] = b,ne[idx] = h[a],h[a] = idx++;16}17
18void euler(){//栈模拟递归19 sk.emplace(1);20 while(sk.size()){21 int u = sk.top();22 int i = h[u];23 while(~i && vis[i]) i = ne[i];//找到一条尚未访问的边24 if(~i){//沿着这条边模拟递归过程,标记该边,并更新表头,避免重复检查已处理边25 sk.emplace(e[i]);26 vis[i] = vis[i^1] = 1;//诺将该行注释掉,则为每条边正反恰好各走一次的答案[Luogu_P6066]27 h[u] = ne[i];28 }29 else{//u相连的所有边均已访问,模拟回溯过程,记录答案30 sk.pop();31 ans.emplace_back(u);32 }33 }34}35
36int main(){37 memset(h,-1,sizeof h);38 cin >> n >> m;39 for(int i = 1;i <= m;i++){40 int a,b;cin >> a >> b;41 add(a,b);add(b,a);42 }43
44 euler();//这里假设1为起点45
46 for(int i = ans.size()-1;i >= 0;i--){47 cout << ans[i] << '\n';48 }49}
动态规划
「笔记」DP从入土到入门 - Luckyblock - 博客园 (cnblogs.com)
【题单】动态规划(入门/背包/状态机/划分/区间/状压/数位/树形/数据结构优化) - 力扣(LeetCode)
构造问题
拆解子问题
求解最简单子问题
通过子问题推当前问题,构建状态转移方程
判断复杂度
xxxxxxxxxx101memset()初始化2dp[0][0][...] = 边界值3for(状态1 :所有状态1的值){4 for(状态2 :所有状态2的值){5 for(...){6 //状态转移方程7 dp[状态1][状态2][...] = 求max/min/sum...8 }9 }10}
背包问题
01背包
每个物品最多只能用一次
xxxxxxxxxx261//一维优化2//dp[j] = max(dp[k], dp[k - v[i]] + w[i]);3//1. dp[i] 仅用到了dp[i-1]层, 4//2. k与k-v[i] 均小于k5//3.若用到上一层的状态时,从大到小枚举, 反之从小到大哦678using namespace std;9const int N = 1003;10int n, m;11int v[N], w[N];12int dp[N];//dp[i]表示背包容量不超过i下的最大价值13
14int main() {15 cin >> n >> m;16 for (int i = 0; i < n;i++) {17 cin >> v[i] >> w[i];18 }19 for (int i = 0; i < n;i++) {//枚举所有物品20 for (int k = m; k >= v[i];k--) {//k从m~v[i]能枚举所有体积21 dp[k] = max(dp[k], dp[k - v[i]] + w[i]);22 }23 }24 cout << dp[m];25 return 0;26}
xxxxxxxxxx91//二维未优化版2int dp[N][N];//dp[i][j]表示前i个物品,总体积不超过j的最大价值3
4for(int i = 1;i <= n;i++){5 for(int j = 1;j <= m;j++){6 if(j-v[i] >= 0) dp[i][j] = max(dp[i-1][j],dp[i-1][j-v[i]]+w[i]);7 else dp[i][j] = dp[i-1][j];8 }9}
完全背包
每个物品可以使用无数次
xxxxxxxxxx31//https://www.acwing.com/problem/content/3/2//未优化版 时间复杂度较高 最大为O(nmk)3//f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);xxxxxxxxxx31//二维优化 (i:1~n) (j:0~m)2if (j >= v[i]) f[i][j] = max(f[i - 1][j], f[i][j - v[i]] + w[i]);3else f[i][j] = f[i - 1][j];xxxxxxxxxx201//一维优化 O(NM)23using namespace std;4const int N = 1003;5int n,m;6int w[N],v[N],dp[N];7
8int main(){9 cin >> n >> m;10 for(int i = 1;i <= n;i++){11 cin >> v[i] >> w[i];12 }13 for(int i = 1;i <= n;i++){14 for(int k = v[i];k <= m;k++){//一维完全背包这里k从小到大枚举15 dp[k] = max(dp[k],dp[k-v[i]]+w[i]);16 }17 }18 cout << dp[m] << endl;19 return 0;20}
多重背包
每个物品使用有限次
xxxxxxxxxx251//https://www.acwing.com/problem/content/4/2//二维未优化 O(nms)345using namespace std;6const int N = 103;7int n, m;8int v[N], w[N], s[N];9int f[N][N];10
11int main() {12 cin >> n >> m;13 for (int i = 1; i <= n;i++) {14 cin >> v[i] >> w[i] >> s[i];15 }16 for (int i = 1; i <= n;i++) {17 for (int j = 0; j <= m;j++) {//j:0~m18 for (int k = 0; k <= s[i] && k * v[i] <= j;k++) {//k*v[i] <= j19 f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);//20 }21 }22 }23 cout << f[n][m];24 return 0;25}xxxxxxxxxx601//https://www.acwing.com/problem/content/5/2//二进制优化 O(nmlogs) 再用01背包问题求解3//任意一个正整数s都可拆成1+2+4+8+16+... = s45using namespace std;6const int N = 200005;7int n,m;8int v[N],w[N],dp[N],idx;9
10int main(){11 cin >> n >> m;12 for(int i = 1;i <= n;i++){13 int a,b,s;cin >> a >> b >> s;14 int k = 1;15 while(k <= s){16 idx++;//第cnt个物品体积为a*s,价值为b*s17 v[idx] = a*k;18 w[idx] = b*k;19 s-=k;20 k*=2;21 }22 if(s > 0){//补上剩下的s个物品23 idx++;24 v[idx] = a*s;25 w[idx] = b*s;26 }27 }28 for(int i = 1;i <= idx;i++){//再用01背包处理该idx个物品29 for(int k = m;k >= v[i];k--){30 dp[k] = max(dp[k],dp[k-v[i]]+w[i]);31 }32 }33 cout << dp[m];34}35
36//////////////////////////////////////////////////////37//简写,优化空间3839using namespace std;40const int N = 2003;41int dp[N];42
43int main(){44 int n,m;cin >> n >> m;45 for(int i = 1;i <= n;i++){46 int v,w,s;cin >> v >> w >> s;47 for(int k = 1;k <= s;k <<= 1){48 s -= k;49 for(int j = m;j >= k*v;j--){50 dp[j] = max(dp[j],dp[j-k*v]+k*w);51 }52 }53 if(s){54 for(int j = m;j >= s*v;j--){55 dp[j] = max(dp[j],dp[j-s*v]+s*w);56 }57 }58 }59 cout << dp[m];60}
xxxxxxxxxx321//单调队列优化 O(nm)234using namespace std;5const int N = 1003,M = 20004;6int n,m;7int v[N],w[N],s[N];8int dp[M],g[M];//对于第i层只用到第i-1层,可用拷贝/滚动数组优化9int q[M],hh,tt = -1;//单调队列滑动窗口10
11int main(){12 cin >> n >> m;13 for(int i = 1;i <= n;i++){14 cin >> v[i] >> w[i] >> s[i];15 }16 for(int i = 1;i <= n;i++){17 memcpy(g,dp,sizeof g);//g[]作为dp[]的i-1层18 int k = (s[i]+1)*v[i];//(s[i]+1)*v[i]为窗口最大长度,诺超出则窗口右移19 for(int r = 0;r < v[i];r++){//r为m%v[i]的偏移量20 hh = 0,tt = -1;//L(hh),R(tt)21 for(int j = r;j <= m;j += v[i]){22 if(hh <= tt && j-k >= q[hh]) hh++;23 while(hh <= tt && g[q[tt]]+(j-q[tt])/v[i]*w[i] <= g[j]) tt--;24 //把以队尾q[tt]为值时<=g[j]的数都弹出,保持队列单调递减25 q[++tt] = j; //队首q[hh]为滑动窗口最大值26 dp[j] = g[q[hh]] + (j-q[hh])/v[i]*w[i];27 //dp[i][j] = dp[i-1][mx] + (j-mx)/v[i]*w[i];28 }29 }30 }31 cout << dp[m];32}
分组背包
每组有若干个物品,同一组内的物品最多只能选一个。
xxxxxxxxxx271//https://www.acwing.com/problem/content/description/9/23using namespace std;4const int N = 105;5int n, m;6int dp[N];//只从前i组物品中选,当前体积小于等于j的最大值7int v[N][N], w[N][N], s[N];8int main() {9 cin >> n >> m;10 for (int i = 1; i <= n; i++) {11 cin >> s[i];//每组s[i]个物品12 for (int j = 1; j <= s[i]; j++) {13 cin >> v[i][j] >> w[i][j];14 }15 }16
17 for (int i = 1; i <= n; i++) {//枚举所有背包18 for (int k = m; k >= 0; k--) {//从m~0枚举所有体积19 for (int j = 0; j <= s[i]; j++) {//枚举所有选择20 if (k >= v[i][j]) {21 dp[k] = max(dp[k]/*不选*/, dp[k - v[i][j]] + w[i][j]/*选*/);22 }23 }24 }25 }26 cout << dp[m];27}
二维费用背包
xxxxxxxxxx231//二维费用01背包 https://www.acwing.com/problem/content/8/2//容量不超过V,重量不超过M34using namespace std;5const int MX = 1003;6int N,V,M;7int v[MX],m[MX],w[MX];8int dp[105][105];9
10int main(){11 cin >> N >> V >> M;12 for(int i = 1;i <= N;i++){13 cin >> v[i] >> m[i] >> w[i];14 }15 for(int i = 1;i <= N;i++){16 for(int j = V;j >= v[i];j--){17 for(int k = M;k >= m[i];k--){18 dp[j][k] = max(dp[j][k],dp[j-v[i]][k-m[i]] + w[i]);19 }20 }21 }22 cout << dp[V][M];23}xxxxxxxxxx231//潜水员 http://ybt.ssoier.cn:8088/problem_show.php?pid=12712//01背包,求满足氧气>=A,氮气>=B的同时,最小重量和345using namespace std;6int A,B,n;7int dp[25][80];8
9int main(){10 memset(dp,0x3f,sizeof dp);11 dp[0][0] = 0;12 cin >> A >> B >> n;13 for(int i = 1;i <= n;i++){14 int a,b,w;cin >> a >> b >> w;15 for(int j = A;j >= 0;j--){16 for(int k = B;k >= 0;k--){17 dp[j][k] = min(dp[j][k],dp[max(j-a,0)][max(k-b,0)]+w);18 }19 }20 }21 cout << dp[A][B];22}23//空间未优化版 dp[i][j][k] = min(dp[i-1][j][k],dp[i-1][max(j-a,0)][max(k-b,0)]+w);
求具体方案
求具体方案,空间一般不能优化成二维
xxxxxxxxxx281//01背包 https://www.acwing.com/problem/content/12/23using namespace std;4const int N = 1003;5int n,m;6int v[N],w[N];7int dp[N][N];8
9int main(){10 cin >> n >> m;11 for(int i = 1;i <= n;i++){12 cin >> v[i] >> w[i];13 }14
15 for(int i = n;i >= 1;i--){16 for(int j = 0;j <= m;j++){17 dp[i][j] = dp[i+1][j];18 if(j >= v[i]) dp[i][j] = max(dp[i+1][j],dp[i+1][j-v[i]]+w[i]);19 }20 }21 //dp[1][m]为终点状态22 for(int i = 1,j = m;i <= n;i++){23 if(j >= v[i] && dp[i][j] == dp[i+1][j-v[i]]+w[i]){24 cout << i << ' ';//字典序最小的方案,从前往后推25 j -= v[i];26 }27 }28}
xxxxxxxxxx411//分组背包 机器分配:https://www.acwing.com/problem/content/1015/23using namespace std;4const int N = 20;5int n,m;6int w[N][N];7int dp[N][N];8int ans[N];9
10int main(){11 cin >> n >> m;12 for(int i = 1;i <= n;i++){13 for(int j = 1;j <= m;j++){14 cin >> w[i][j];15 }16 }17
18 for(int i = 1;i <= n;i++){19 for(int k = 0;k <= m;k++){20 for(int j = 0;j <= m;j++){21 if(k >= j){22 dp[i][k] = max(dp[i][k],dp[i-1][k-j]+w[i][j]);23 }24 }25 }26 }27 28 cout << dp[n][m] << '\n';29 for(int i = n,k = m;i >= 1;i--){30 for(int j = 1;j <= m;j++){31 if(k >= j && dp[i][k] == dp[i-1][k-j]+w[i][j]){32 k -= j;33 ans[i] = j;34 break;35 }36 }37 }38 for(int i = 1;i <= n;i++){//第i组里选第ans[i]个物品(可能为0)39 cout << i << ' ' << ans[i] << '\n';40 }41}
求方案数
xxxxxxxxxx321//01背包求最优选的方案数 https://www.acwing.com/problem/content/11/2//使总价值最大的最优方案数 O(nm)34using namespace std;5const int N = 1003,mod = 1e9+7;6int n,m;7int v[N],w[N];8int dp[N],f[N];9
10int main(){11 cin >> n >> m;12 for(int i = 1;i <= n;i++){13 cin >> v[i] >> w[i];14 }15
16 for(int i = 0;i <= m;i++) {//什么都不选也是一种方案17 f[i] = 1;18 }19
20 for(int i = 1;i <= n;i++){21 for(int j = m;j >= v[i];j--){22 if(dp[j] < dp[j-v[i]]+w[i]){23 dp[j] = dp[j-v[i]]+w[i];24 f[j] = f[j-v[i]];25 }26 else if(dp[j] == dp[j-v[i]]+w[i]){27 f[j] = (f[j]+f[j-v[i]])%mod;28 }29 }30 }31 cout << f[m];32}xxxxxxxxxx231//01背包求价值恰好为m的方案数 https://www.acwing.com/problem/content/description/280/2//O(nm)34using namespace std;5const int N = 105,M = 10004;6int n,m;7int a[N];8int dp[M];9
10int main(){11 cin >> n >> m;12 for(int i = 1;i <= n;i++){13 cin >> a[i];14 }15
16 dp[0] = 1;17 for(int i = 1;i <= n;i++){18 for(int j = m;j >= a[i];j--){19 dp[j] += dp[j-a[i]];20 }21 }22 cout << dp[m];23}
xxxxxxxxxx311//完全背包求最优选的方案数(待验证) O(nm)234using namespace std;5const int N = 1003,mod = 1e9+7;6int n,m;7int v[N],w[N];8int dp[N],f[N];9
10int main(){11 cin >> n >> m;12 for(int i = 1;i <= n;i++){13 cin >> v[i] >> w[i];14 }15
16 for(int i = 0;i <= m;i++){17 f[i] = 1;18 }19 for(int i = 1;i <= n;i++){20 for(int j = v[i];j <= m;j++){21 if(dp[j] < dp[j-v[i]]+w[i]){22 dp[j] = dp[j-v[i]]+w[i];23 f[j] = f[j-v[i]];24 }25 else if(dp[j] == dp[j-v[i]]+w[i]){26 f[j] = (f[j]+f[j-v[i]])%mod;27 }28 }29 }30 cout << f[m];31}xxxxxxxxxx221//完全背包求价值恰好为m的方案数 https://www.acwing.com/problem/content/1023/2// 给你一个n种面值的货币系统,求组成面值为m的货币有多少种方案。 O(nm)34using namespace std;5const int N = 20,M = 3003;6int n,m;7int a[N];8long long dp[M];9
10int main(){11 cin >> n >> m;12 for(int i = 1;i <= n;i++){13 cin >> a[i];14 }15 dp[0] = 1;16 for(int i = 1;i <= n;i++){17 for(int j = a[i];j <= m;j++){18 dp[j] += dp[j-a[i]];19 }20 }21 cout << dp[m];22}
有依赖的背包问题
有 N 个物品和一个容量是 V 的背包。物品之间具有依赖关系,且依赖关系组成一棵树的形状。如果选择一个物品,则必须选择它的父节点。
xxxxxxxxxx381//树上分组背包 dp与类似于洛谷P2014选课234using namespace std;5const int N = 105;6int n,m;7int v[N],w[N],p[N];8int h[N],e[N],ne[N],idx;9int dp[N][N];10
11void add(int a,int b){12 e[idx] = b,ne[idx] = h[a],h[a] = idx++;13}14
15void dfs(int u){16 for(int i = h[u];~i;i = ne[i]){//物品组17 dfs(e[i]);18 for(int j = m;j >= 0;j--){//体积19 for(int k = 0;k <= j-v[u];k++){//决策20 dp[u][j] = max(dp[u][j],dp[u][j-k]+dp[e[i]][k]);21 }22 }23 }24}25
26int main(){27 memset(h,-1,sizeof h);28 cin >> n >> m;29 int root;30 for(int i = 1;i <= n;i++){31 cin >> v[i] >> w[i] >> p[i];32 for(int j = v[i];j <= m;j++) dp[i][j] = w[i];//初始状态33 if(p[i] == -1) root = i;34 else add(p[i],i);35 }36 dfs(root);37 cout << dp[root][m];38}
线性DP
数字三角形
xxxxxxxxxx351//https://www.acwing.com/problem/content/900/23using namespace std;4const int INF = 0x3f3f3f3f;5const int N = 505;6int arr[N][N],dp[N][N];7int n;8
9int main() {10 cin >> n;11 for (int i = 1; i <= n;i++) {12 for (int j = 1; j <= i;j++) {13 cin >> arr[i][j];14 }15 }16
17 for (int i = 0; i <= n;i++) {//初始化多一层18 for (int j = 0; j <= i + 1;j++) {19 dp[i][j] = -INF;20 }21 }22 dp[1][1] = arr[1][1];23
24 for (int i = 2; i <= n;i++) {25 for (int j = 1; j <= i;j++) { 26 dp[i][j] = arr[i][j] + max(dp[i-1][j-1], dp[i-1][j]);27 }28 }29 30 int ans = -INF;31 for (int i = 1; i <= n;i++) {//寻找最后一层的最大值32 ans = max(ans, dp[n][i]);33 }34 cout << ans;35}
xxxxxxxxxx361//方格取数:https://www.luogu.com.cn/problem/P10042//左上到右下走两次34using namespace std;5const int N = 12;6int n;7int arr[N][N];8int dp[N+N][N][N];//dp[k][i1][i2]:k = i1+j1 = i2+j29//从(1,1)走到(i1,j2)和(i2,j2)能得到的最大数10
11int main(){12 cin >> n;13 int a,b,c;14 while(cin >> a >> b >> c,a&&b&&c){15 arr[a][b] = c;16 }17
18 for(int k = 2;k <= n+n;k++){19 for(int i1 = 1;i1 <= n;i1++){20 for(int i2 = 1;i2 <= n;i2++){21 int j1 = k-i1,j2 = k-i2;22 if(j1 >= 1 && j1 <= n && j2 >= 1 && j2 <= n){23 int t = arr[i1][j1];24 if(i1 != i2) t += arr[i2][j2];//i1 != i2 说明当前不在同一个格子25 int &x = dp[k][i1][i2];26 //所有从上个状态转移过来的最大值27 x = max(x,dp[k-1][i1-1][i2-1]+t);28 x = max(x,dp[k-1][i1-1][i2] + t);29 x = max(x,dp[k-1][i1][i2-1] + t);30 x = max(x,dp[k-1][i1][i2] + t);31 }32 }33 }34 }35 cout << dp[n+n][n][n];36}
最长上升子序列
LIS Longest Increasing Subsequence,最长递增子序列
xxxxxxxxxx261//https://www.luogu.com.cn/problem/B36372//O(n^2) -- DP34using namespace std;5const int N = 5005;6int n,ans;7int arr[N],dp[N];//dp[i]为以第i个点的a[i]结尾的最大的上升序列8
9int main() {10 cin >> n;11 for (int i = 1; i <= n;i++) {12 cin >> arr[i];13 }14
15 for (int i = 1; i <= n; i++) {16 dp[i] = 1;//初始化dp[i] = 1;17 for (int j = 1; j < i;j++) {18 if (arr[i] > arr[j]) {19 dp[i] = max(dp[i], dp[j] + 1);20 //前一个小于自己的数结尾的最大上升子序列加上自己,即+121 } 22 }23 ans = max(ans, dp[i]);24 }25 cout << ans;26}
xxxxxxxxxx271//https://www.acwing.com/problem/content/898/2//O(nlogn) -- 模拟栈 + 二分3//诺求最长单调非递减子序列 lb改为ub,>改>=即可:https://ac.nowcoder.com/acm/contest/83252/D456using namespace std;7
8const int N = 100005;9int n,arr[N];10vector<int>v;//模拟堆栈11
12int main() {13 cin >> n;14 for (int i = 1; i <= n;i++) {15 cin >> arr[i];16 } 17
18 for (int i = 1; i <= n;i++) {19 if (v.empty()||arr[i] > v.back()) { //如果arr[i]大于栈顶元素,将该元素入栈20 v.push_back(arr[i]); 21 }22 else {//否则替换掉第栈中一个大于或者等于arr[i]的那个数23 *lower_bound(begin(v), end(v), arr[i]) = arr[i]; 24 }25 }26 cout << v.size();27}
最长公共子序列
LCS Longest Common Subsequence,最长公共子序列
xxxxxxxxxx361//https://www.acwing.com/problem/content/899/2//O(nm)34using namespace std;5const int N = 1005;6int n, m, dp[N][N];//dp[i][j]表示a的前i个字母和b的前j个字母最长公共子序列长度7string a, b;8int main() {9 cin >> n >> m >> a >> b;10 a = ' ' + a, b = ' ' + b;11 for (int i = 1; i <= n;i++) {12 for (int j = 1; j <= m;j++) {13 if(a[i] == b[j]){//情况一:a[i]在b[j]在(dp[i][j])14 dp[i][j] = max(dp[i][j],dp[i-1][j-1]+1);15 }16 else {//情况二:a[i]在b[j]不在(dp[i][j-1])、情况三:a[i]不在b[j]在(d[i-1][j])17 //(情况四:a[i]不在b[j]不在(dp[i-1][j-1]),已经包含在情况二、三中18 dp[i][j] = max(dp[i-1][j],dp[i][j-1]);19 }20 }21 }22 cout << dp[n][m];23 /* 具体序列方案24 string s;25 for(int i = n,j = m;i >= 1 && j >= 1;){26 if(dp[i][j] == dp[i-1][j-1]+1){27 s += s1[i];28 i--;j--;29 }30 else if(dp[i][j] == dp[i-1][j]) i--;31 else if(dp[i][j] == dp[i][j-1]) j--;32 }33 for(int i = s.size()-1;i >= 0;i--){34 cout << s[i];35 }*/36}
最长公共上升子序列LCIS
xxxxxxxxxx271//https://www.acwing.com/problem/content/274/23using namespace std;4const int N = 3003;5int n;6int a[N],b[N];7int dp[N][N];//a[]中前i个数字,b[]中前j个数字,..且当前以b[j]结尾的子序列的最长方案8
9int main(){10 cin >> n;11 for(int i = 1;i <= n;i++) cin >> a[i];12 for(int i = 1;i <= n;i++) cin >> b[i];13
14 for(int i = 1;i <= n;i++){15 int nmax = 1;16 for(int j = 1;j <= n;j++){17 dp[i][j] = dp[i-1][j];18 if(b[j] == a[i]) dp[i][j] = max(dp[i][j],nmax);19 if(b[j] < a[i]) nmax = max(nmax,dp[i-1][j] + 1);20 }21 }22 23 int ans = 0;24 for(int i = 0;i <= n;i++){ans = max(ans,dp[n][i]);}25 cout << ans;26 return 0;27}
编辑距离
将字符串a通过以下操作变为字符串b的最小操作次数
删除一个字符
插入一个字符
修改一个字符
xxxxxxxxxx91if (a[i] == b[j]) dp[i][j] = dp[i-1][j-1]; //无需修改2else{3dp[i][j] = min({dp[i-1][j-1]+1,dp[i-1][j]+1,dp[i][j-1]+1});4/*min{5a[1~i-1] = b[1~j-1],修改a[i]为b[j]6a[1~i-1] = b[1~j],删除a[i]7a[1~i] = b[1~j-1],增加b[j]8}*/9}
xxxxxxxxxx281//https://www.luogu.com.cn/problem/P2758234using namespace std;5const int N = 2003;6int n,m;7string a,b;8int dp[N][N];//dp[i][j]为将a[1~i]变为b[1~j]的最小操作次数9
10int main(){11 cin >> a >> b;n = a.size();m = b.size();12 a = ' ' + a,b = ' ' + b;13
14 for(int i = 1;i <= n;i++) dp[i][0] = i;//初始化15 for(int j = 1;j <= m;j++) dp[0][j] = j;16
17 for(int i = 1;i <= n;i++){18 for(int j = 1;j <= m;j++){19 if(a[i] == b[j]){20 dp[i][j] = dp[i-1][j-1];21 }22 else{23 dp[i][j] = min({dp[i-1][j-1]+1,dp[i-1][j]+1,dp[i][j-1]+1});24 }25 }26 }27 cout << dp[n][m];28}
区间DP
定义
区间类动态规划是线性动态规划的扩展,它在分阶段地划分问题时,与阶段中元素出现的顺序和由前一阶段的哪些元素合并而来有很大的关系。
令状态
性质
区间 DP 有以下特点:
合并:即将两个或多个部分进行整合,当然也可以反过来;
特征:能将问题分解为能两两合并的形式;
求解:对整个问题设最优值,枚举合并点,将问题分解为左右两个部分,最后合并两个部分的最优值得到原问题的最优值。
一维
石子合并
n个数a[1]~a[n]排成一排,进行n-1次合并操作,每次操作将相邻的两堆合并成一堆,并获得新的一堆中的石子数量的和的得分。你需要最小化你的得分。
诺要求环形的,只需将两个a[ ]接在一起。枚举所有长度为n的区间取最值
xxxxxxxxxx441//https://www.luogu.com.cn/problem/P17752//O(n^3)345using namespace std;6const int N = 305;7int n;8int s[N];//a[]的前缀和9int dp[N][N];//dp[i][j]表示将i到j这一段石子合并成一堆的方案的集合,属性Min10
11int main() {12 cin >> n;13 for (int i = 1; i <= n;i++) {14 cin >> s[i];15 s[i] += s[i-1];16 }17 18 memset(dp,0x3f,sizeof dp);19 for(int i = 1;i <= n;i++) dp[i][i] = 0;20 21 for (int len = 2; len <= n;len++) {//枚举区间长度,可以跳过1,从2开始22 for (int l = 1; l + len - 1 <= n;l++) {//枚举区间左右端点23 int r = l + len - 1;24 for (int k = l; k < r;k++) {// 枚举分割点,构造状态转移方程25 dp[l][r] = min(dp[l][r], dp[l][k]+dp[k+1][r] + s[r]-s[l-1]);26 }27 }28 }29 cout << dp[1][n];30}31///////////////////////////////////////////////////32//记忆化搜索写法33int sol(int l,int r){34 if(l >= r) return 0; 35 if(dp[l][r] != -1) return dp[l][r];36 dp[l][r] = 0x3f3f3f3f;37 for(int k = l;k <= r-1;k++){38 dp[l][r] = min(dp[l][r],sol(l,k)+sol(k+1,r) + s[r]-s[l-1]);39 }40 return dp[l][r];41}42
43memset(dp,-1,sizeof dp);44cout << sol(1,n);
当n很大时,用Garsia–Wachs 算法求解
每次找到第一个a[i-1] < a[i+1]的位置,把a[i-1]和a[i]合并为now,再将now插入到从i开始左边第一个a[k] > now的a[k]后面
xxxxxxxxxx321//https://www.luogu.com.cn/problem/P55692//需要开启O2优化,或使用平衡树等数据结构34using namespace std;5const int N= 40004;6const long long INF = 1e18;7long long ans;8
9int main(){10 int n;cin >> n;11 vector<long long>v(n+2);12 for(int i = 1;i <= n;i++){ cin >> v[i]; }13 v[0] = v[n+1] = INF;//在两端设置哨兵14 n++;15
16 while(n-- && n >= 2){17 int i,k;18 for(i = 1;i <= n;i++){19 if(v[i-1] < v[i+1]) break;20 }21 int now = v[i-1]+v[i];22 ans += now;23 for(k = i-1;k >= 0;k--){24 if(v[k] > now) break;25 }26
27 v.erase(v.begin()+i-1);28 v.erase(v.begin()+i-1);29 v.insert(v.begin()+k+1,now);30 }31 cout << ans;32}
二维
棋盘分割
将一个
的棋盘进行如下分割:将原棋盘割下一块矩形棋盘并使剩下部分也是矩形,再将剩下的两部分中的任意一块继续如此分割,这样割了 次后,连同最后剩下的矩形棋盘共有 块矩形棋盘。(每次切割都只能沿着棋盘格子的边进行)。原棋盘上每一格有一个分值,一块矩形棋盘的总分为其所含各格分值之和。现在需要把棋盘按上述规则分割成 块矩形棋盘,并使各矩形棋盘总分的平方和最小,给定n求最小值时多少?
xxxxxxxxxx431234using namespace std;5const long long INF = 1e18;6const int N = 20,M = 10;7int n,m = 8;8long long a[M][M],s[M][M];9long long dp[M][M][M][M][N];10//dp[x1][y1][x2][y2][k]表示该矩阵且切割了k次的最小平方和11
12long long get(int x1,int y1,int x2,int y2){13 long long sum = s[x2][y2] - s[x2][y1-1] - s[x1-1][y2] + s[x1-1][y1-1];14 return sum*sum;15}16
17long long dfs(int x1,int y1,int x2,int y2,int k){//记忆化搜索dp18 long long &now = dp[x1][y1][x2][y2][k];19 if(now >= 0) return now;//如果该区间已经求过,直接返回20 if(k == 1) return now = get(x1,y1,x2,y2);//k=1不分割,直接返回该区间和21 now = INF;22 for(int i = x1;i < x2;i++){//枚举横着分割23 now = min(now,dfs(x1,y1,i,y2,k-1)+get(i+1,y1,x2,y2));//选上半段24 now = min(now,dfs(i+1,y1,x2,y2,k-1)+get(x1,y1,i,y2));//选下半段25 }26 for(int i = y1;i < y2;i++){//枚举竖着分割27 now = min(now,dfs(x1,y1,x2,i,k-1)+get(x1,i+1,x2,y2));//选左半段28 now = min(now,dfs(x1,i+1,x2,y2,k-1)+get(x1,y1,x2,i));//选右半段29 }30 return now;31}32
33int main(){34 cin >> n;35 for(int i = 1;i <= m;i++){36 for(int j = 1;j <= m;j++){37 cin >> a[i][j];38 s[i][j] += s[i-1][j] + s[i][j-1] - s[i-1][j-1] + a[i][j];39 }40 }41 memset(dp,-0x3f,sizeof dp);42 cout << dfs(1,1,8,8,n);43}
计数DP
整数划分
一个正整数 n 可以表示成若干个正整数之和,形如:n=n1+n2+…+nk,其中 n1≥n2≥…≥nk,k≥1。现在给定一个正整数 n(1<=n<=1000),请你求出 n共有多少种不同的划分方法。
完全背包解法:把1,2,3, … n分别看做n个物体的体积,这n个物体均无使用次数限制,问恰好能装满总体积为n的背包的总方案数
xxxxxxxxxx191//https://www.acwing.com/problem/content/902/2//O(N^2)34using namespace std;5const int N = 1005, mod = 1e9 + 7;6int n, dp[N];7
8int main() {9 cin >> n;10
11 dp[0] = 1;//容量为0时,前i个物品全不选也是一种方案12
13 for (int i = 1; i <= n;i++) {14 for (int j = i; j <= n;j++) {15 dp[j] = (dp[j] + dp[j - i]) % mod;16 }17 }18 cout << dp[n];19}
另一种解法:dp[i] [j]表示为总和为i,总个数为j的方案数
xxxxxxxxxx2212using namespace std;3const int N = 1005, mod = 1e9 + 7;4int n,ans, dp[N][N];5
6int main() {7 cin >> n;8 9 dp[0][0] = 1;10
11 for (int i = 1; i <= n;i++) {12 for (int j = 1; j <= i;j++) {13 dp[i][j] = (dp[i - 1][j - 1] + dp[i - j][j]) % mod;14 }15 }16 17 for (int i = 1; i <= n;i++) {18 ans = (ans + dp[n][i]) % mod;19 }20 21 cout << ans;22}
https://codeforces.com/contest/560/problem/E
给定一个H*W的棋盘,其中有n个黑格子不能走,每次只能向下或右走,问从左上走到右下角共有多少种走法? 排序后,令dp[i]表示从左上走到排序后的第i个格子且不经过其他黑格子的走法,则有:
xxxxxxxxxx59123using namespace std;4const int N = 200005;5int h,w,n;6const int mod = 1e9+7;7long long dp[N];8
9struct node{10 int x,y;11 bool operator < (const auto&e2){12 if(x != e2.x) return x < e2.x;13 return y < e2.y;14 }15}a[N];16
17long long qmi(long long a,long long b,long long p){18 long long ans = 1;19 while(b){20 if(b&1) ans = ans*a%mod;21 b >>= 1;22 a = a*a%mod;23 }24 return ans%mod;25}26
27long long fact[N],infact[N];28void init(){29 fact[0] = infact[0] = 1;30 for(int i = 1;i < N;i++){31 fact[i] = i*fact[i-1]%mod;32 }33 infact[N-1] = qmi(fact[N-1],mod-2,mod);34 for(int i = N-2;i >= 1;i--){35 infact[i] = infact[i+1]*(i+1)%mod;36 }37}38
39long long C(int a,int b){40 return fact[a]*infact[b]%mod*infact[a-b]%mod;41}42
43int main(){44 init();45 cin >> h >> w >> n;46 for(int i = 1;i <= n;i++){ cin >> a[i].x >> a[i].y; }47 sort(a+1,a+n+1);48 a[n+1] = {h,w};49 for(int i = 1;i <= n+1;i++){50 auto [x,y] = a[i];51 dp[i] = C(x+y-2,x-1);52 for(int j = 1;j < i;j++){53 if(x < a[j].x || y < a[j].y) continue;54 dp[i] -= dp[j]*C(x+y - a[j].x-a[j].y,x-a[j].x)%mod;55 }56 dp[i] = (dp[i]%mod+mod)%mod;57 }58 cout << dp[n+1];59}
数位DP
求区间[l,r]中答案的个数,可以转化为求 F(r) - F(l-1),其中F(x)为区间[1,x]中答案的个数
[P2602 ZJOI2010] 数字计数 - 洛谷 (luogu.com.cn)
题目大意:给定两个正整数a,b,求在[a,b]中的所有整数中,每个数码(digit)各出现了多少次。
xxxxxxxxxx32123
4const int N = 13;5int nums[N],len;6long long dp[N][N][10];//dp[pos][sum]表示pos位置,当前已经统计了sum个目标数字的方案数7
8long long dfs(int pos,bool lim,bool zero,int dig,int sum){9 if(!pos) return sum;10 if(!lim && !zero && ~dp[pos][sum][dig]) return dp[pos][sum][dig];11 int up = lim ? nums[pos] : 9;12 long long ans = 0;13 for(int i = 0;i <= up;i++){14 bool count = (!(zero&(!i)) && (i == dig));//当前i==dig且不存在前导015 ans += dfs(pos-1,lim&(i==up),zero&(!i),dig,sum+count);16 }17 return (lim || zero) ? ans : dp[pos][sum][dig] = ans;18}19
20long long f(long long x,int dig){21 len = 0;22 while(x) nums[++len] = x % 10, x /= 10;23 return dfs(len,1,1,dig,0);24}25
26int main(){27 long long l,r; std::cin >> l >> r;28 std::memset(dp,-1,sizeof dp);29 for(int i = 0;i <= 9;i++){30 std::cout << f(r,i) - f(l-1,i) << ' ';31 }32}
xxxxxxxxxx521//度的数量 https://ac.nowcoder.com/acm/contest/973/A2//求给定区间[X,Y]中满足下列条件的整数个数:这个数恰好等于K个互不相等的B的整数次幂之和。345using namespace std;6const int N = 35;7int k,b;8int c[N][N];9
10void init(){11 for(int i = 0;i < N;i++){12 for(int j = 0;j <= i;j++){13 if(!j) c[i][j] = 1;14 else c[i][j] = c[i-1][j] + c[i-1][j-1];15 }16 }17}18
19int f(int n){20 if(n == 0) return 0;21 vector<int>v;22 while(n) v.emplace_back(n%b),n/=b;//将n转化为b进制数23 for(int i = v.size()-1;i >= 0;i--){24 cout << v[i];25 }26 cout << endl;27
28 int ans = 0;29 int last = 0;//记录前面已经占用多少位30 for(int i = v.size()-1;i >= 0;i--){31 int x = v[i];32 if(x > 0){33 if(i >= k-last) ans += c[i][k-last];//当前位(i)填0,则剩下位(0~i-1)可以任意填,共有C(i,k-last)种填法34 if(x >= 2){//当前位(i)填1,则剩下位(0~i-1)可以任意填,共有C(i,k-last-1)种填法,直接break35 if(i >= k-last-1 && k-last-1 >= 0) ans += c[i][k-last-1];36 break;37 }38 else {//如果x==1,则当前位只能填1,已用位数last++,防止加上后面填的数会超过n,继续处理下一位39 last++;40 if(last > k) break;41 }42 }43 if(i == 0 && last == k) ans++;//特殊处理最后一位右分支44 }45 return ans;46}47
48int main(){49 init();50 int l,r;cin >> l >> r >> k >> b;51 cout << f(r) - f(l-1);52}xxxxxxxxxx451//非下降数 https://ac.nowcoder.com/acm/problem/505172//求给定区间内有多少个非下降数345using namespace std;6const int N = 15;7int dp[N][N];//dp[i][j]表示共有i位,且最高位数字是j的所有非下降数的个数8
9void init(){10 for(int i = 1;i <= 9;i++){ dp[1][i] = 1; }11 for(int i = 2;i < N;i++){12 for(int j = 0;j <= 9;j++){13 for(int k = j;k <= 9;k++){14 dp[i][j] += dp[i-1][k];15 }16 }17 }18}19
20int f(int n){21 if(n == 0) return 1;22 vector<int>v;23 while(n) v.emplace_back(n%10),n/=10;24
25 int ans = 0;26 int last = 0;//保存前面位的最大值27 for(int i = v.size()-1;i >= 0;i--){28 int x = v[i];29 for(int j = last;j < x;j++){//左边分支,因为要保持不降序,所以j>=last30 ans += dp[i+1][j];31 }32 if(x < last) break;//如果上一位最大值大于x的话,不构成降序,所以右边分支结束33 else last = x;34 if(i == 0) ans++;//全部枚举完了,说明n本身构成一个方案35 }36 return ans;37}38
39int main(){40 init();41 int l,r;42 while(cin >> l >> r){43 cout << f(r) - f(l-1) << '\n';44 }45}
记忆化搜索
建议数位DP使用记忆化搜索做,大部分情况下比递推简单一点。
update(state): 12300->12340
xxxxxxxxxx191//常规写法2//zero根据前导零是否会影响答案添加3//state可能需要设计多个4ll dfs(int pos,bool lim,bool zero ,int state){5 if(!pos) return check(state);6 if(!lim && !zero && ~dp[pos][state]) return dp[pos][state];7 int up = lim ? nums[pos] : 9;8 ll ans = 0;9 for(int i = 0;i <= up;i++){10 ans += dfs(pos-1,lim&(i==up),zero&(!i),update(state));11 }12 return (lim || zero) ? ans : dp[pos][state] = ans;13}14
15ll f(ll x){16 len = 0;17 while(x) nums[++len] = x % 10, x /= 10;18 return dfs(len,1,1,0);19}
XHXJ's LIS - HDU 4352 - Virtual Judge (vjudge.net)
T组询问,每次求区间[L,R]内有多少整数的最长上升子序列长度为K?
考虑到最长上升子序列值域为不会超过9,可以用一个二进制状态压缩表示当前LIS的状态。前导0不能算入LIS,所以参数要传入前导0状态。多开一维K可以节省初始化DP数组的时间
xxxxxxxxxx48123
4const int N = 22;5int k;6long long dp[N][1 << 10][10];//dp[pos][state][k]表示第pos位,LIS状态压缩为state,LIS为k的方案数7int nums[N],len;8
9int update(int state,int x,int zero){10 if(zero && (!x)) return state;//前导零不计入序列11 for(int i = x;i <= 9;i++){//用x替换state中第一个>=x的数12 if(state >> i & 1) {13 return state & (~(1 << i)) | (1 << x);14 }15 }16 return state | 1 << x;17}18
19long long dfs(int pos,int state,int lim,int zero){//lim判断当前位是否有限制,zero判断是否有前导020 if(!pos) return __builtin_popcount(state) == k;//递归终点,判断LIS长度21 if(~dp[pos][state][k] && !lim) return dp[pos][state][k];//记忆化结果(无限制时复用)22 int up = lim ? nums[pos] : 9;//当前位上限,无限制则可任意填23 long long ans = 0;24 for(int i = 0;i <= up;i++){25 ans += dfs(pos-1,update(state,i,zero),lim && (i == up),zero && (!i));26 }27 return lim ? ans : dp[pos][state][k] = ans;//无限制时结果记忆化28}29
30long long f(long long x){31 len = 0;32 while(x) nums[++len] = x % 10, x /= 10;33 return dfs(len,0,1,1);34}35
36void sol(){37 long long l,r; std::cin >> l >> r >> k;38 std::cout << f(r) - f(l-1) << '\n';39}40
41int main(){42 std::memset(dp,-1,sizeof dp);43 int t; std::cin >> t;44 for(int i = 1;i <= t;i++){45 printf("Case #%d: ",i);46 sol();47 }48}
试填法
P10958 启示录 - 洛谷 (luogu.com.cn)
只要某数字的十进制表示中有三个连续的 6,即认为这是个魔鬼的数,比如 666,1666,6663,16666,6660666 等等。
给定n,求第n个魔鬼数是多少。
当然也可以使用二分+记忆化搜索:记录详情 (luogu.com.cn)
xxxxxxxxxx4912using namespace std;3const int N = 12;4int n;5int dp[N][3],g[N];//dp[i][j]表示共有i位,且有连续j(0~2)个6,g[i]表示i位共有多少个魔鬼数6
7void init(){8 dp[0][0] = 1;9 dp[0][1] = dp[0][2] = 0;10 for(int i = 1; i < N;i++){11 dp[i][0] = 9*(dp[i-1][0]+dp[i-1][1]+dp[i-1][2]);12 dp[i][1] = dp[i-1][0];13 dp[i][2] = dp[i-1][1];14 g[i] = 10*g[i-1] + dp[i-1][2];15 }16}17
18void sol(){19 cin >> n;20 int m = 3;21 while(g[m] < n) m++;//确定位数,第n个魔鬼数有m位22 for(int i = m,k = 0;i >= 1;i--){23 for(int j = 0;j <= 9;j++){//当前第i位诺填j24 long long cnt = g[i-1];//后面i-1位还有cnt种填法能让整个数是魔鬼数25 if(j == 6 || k == 3){26 //当前位i开始,左边已经有了连续k+(j==6)个6,还差3-(k+(j==6))个627 for(int l = max(3-k-(j==6),0);l <= 2;l++){28 cnt += dp[i-1][l];29 }30 }31 if(cnt >= n){32 cout << j;33 if(k < 3){34 if(j == 6) k++;35 else k = 0;36 }37 break;38 }39 else n -= cnt;40 }41 }42 cout << '\n';43}44
45int main(){46 init();47 int t;cin >> t;48 while(t--) sol();49}
状压DP
长方形摆放
求把 N×M的棋盘分割成若干个 1×2 的长方形,有多少种方案。
用二进制j表示第i列状态,如第1列状态为10010,第2列状态为01001,st[1|2] = 11011
xxxxxxxxxx551//https://www.acwing.com/problem/content/description/293/2345using namespace std;6const int N = 12, M = 1 << N;//M = 2^N7int n, m;8bool st[M];9long long dp[N][M];//第一维表示列, 第二维表示所有可能的状态(1表示当前位置放了横着的长方形的左半边)10vector<int>v[M];//二维数组记录合法的状态11
12int main() {13 while (cin >> n >> m, n && m) {//n行,m列14 //预处理1:筛掉连续奇数个0的状态(不能竖着放满长方形)15 for (int j = 0; j < (1 << n); j++) {//j < 2^n16 st[j] = 1;17 int cnt = 0;//cnt记录连续0的个数18 for (int k = 0; k < n; k++) {19 if ((j >> k) & 1) {//如果当前位为1,且前有连续奇数个0则状态不合法20 if (cnt & 1)st[j] = 0;21 cnt = 0;22 }23 else cnt++;24 }25 if (cnt & 1)st[j] = 0;//判断最后一节连续0个数26 }27
28 //预处理2:看第i列能使用那些状态而不会和第i-1列冲突29 for (int j = 0; j < (1 << n); j++) {//枚举第i列的所有状态j30 v[j].clear();//初始化清空上次操作遗留的状态,防止影响本次状态。31 for (int k = 0; k < (1 << n); k++) {//枚举第i-1列的所有状态k32 if ((j & k) == 0 && st[j | k]) {33 //j&k按位与判断i-1列伸到i列和第i列状态是否重合冲突34 //j|k按位或判断i-1列伸出去的方块是否会导致i列有连续奇数个035 v[j].emplace_back(k);36 //j表示第i列的可行状态,k表示第i列状态为j的情况下第i-1列所有可行状态37 }38 }39 }40
41 memset(dp, 0, sizeof dp);42 dp[0][0] = 1;43 //第1列由虚构的第0列推导,第0列不可能放横条,只能全摆薯条,故摆法只有一种,无需考虑奇偶性44 for (int i = 1; i <= m; i++) {45 for (int j = 0; j < (1 << n); j++) {46 for (auto& k : v[j]) {47 dp[i][j] += dp[i - 1][k];48 }49 }50 }51 cout << dp[m][0] << endl;52 //dp[m][0]表示 前m-1列都处理完,并且第m-1列没有伸出来的所有方案数53 //既整个棋盘处理完的方案数54 }55}
最短Hamilton路径
给定一张 n(n≤20) 个点的带权无向图,点从0∼n−1标号,求起点 0 到终点 n-1 的最短Hamilton路径。 Hamilton路径的定义是从 0 到 n-1 不重不漏地经过每个点恰好一次。
xxxxxxxxxx351//https://ac.nowcoder.com/acm/problem/509092345using namespace std;6const int N = 21, M = 1 << N;7int n;8int a[N][N];//带权无向图9int dp[M][N];//dp[i][j],i表示当前走过的点的集合,j表示当前停在了哪个点10//i为二进制表示路径,如走0,1,2,4点则i为(10111)=2311
12int main() {13 cin >> n;14 for (int i = 0; i < n;i++) {15 for (int j = 0; j < n;j++) {16 cin >> a[i][j];17 }18 }19 memset(dp, 0x3f, sizeof dp);//因为要求最小值,所以初始化为无穷大20 dp[1][0] = 0;//0为起点,所以dp[1][0] = 0;21
22 for (int i = 1; i < 1 << n;i++) {//枚举所有路径23 for (int j = 0; j < n;j++) {//枚举所有终点24 if (i >> j & 1) {//合法终点j25 for (int k = 0; k < n;k++) {//枚举所有终点26 if (i >> k & 1) {//合法终点k27 dp[i][j] = min(dp[i][j], dp[i - (1 << j)][k] + a[k][j]);28 //dp[i][j] 对 (dp[i中不包含点j的所有子路径][终点为k] + a[k][j]) 取最小值29 }30 }31 }32 }33 }34 cout << dp[(1 << n) - 1][n - 1];//[1111111...][n-1]表示所有点都走过,终点为n-135}
bitset优化
Many Graph Queries(★7) - AtCoder typical90_bg - Virtual Judge (vjudge.net)
给定N个点,M条边的有向图。回答Q个询问:从点a能否到达点b?
将询问分成
xxxxxxxxxx361234using namespace std;5
6int main(){7 int n,m,q;cin >> n >> m >> q;8 vector<vector<int>>e(n);9 for(int i = 0;i < m;i++){10 int a,b;cin >> a >> b;11 a--;b--;12 e[a].emplace_back(b);13 }14
15 vector<int>a(q),b(q);16 for(int i = 0;i < q;i++){17 cin >> a[i] >> b[i];18 a[i]--;b[i]--;19 }20
21 for(int i = 0;i < q;i += 316){22 vector<bitset<316>>dp(n+1);23 for(int j = 0;j < 316 && i+j < q;j++){24 dp[a[i+j]][j] = 1;25 }26 for(int x = 0;x < n;x++){27 for(auto& y:e[x]){28 dp[y] |= dp[x];29 }30 }31 for(int j = 0;j < 316 && i+j < q;j++){32 if(dp[b[i+j]][j]) cout << "Yes\n";33 else cout << "No\n";34 }35 }36}
重复覆盖问题
给定n个集合,和一个目标集合,选择最少的集合,使得所选集合的并集=目标集合
时间复杂度
,诺复杂度过高,可以考虑Dancing Links解决
xxxxxxxxxx281//糖果 https://www.luogu.com.cn/problem/P86872//dp[i]表示从状态0000走到状态i的最少步数345using namespace std;6const int N = 105,M = 1 << 21;7int a[N];8int dp[M];9
10int main(){11 int n,m,k;cin >> n >> m >> k;12 for(int i = 1;i <= n;i++){13 for(int j = 1;j <= k;j++){14 int x;cin >> x;15 a[i] |= 1 << (x-1);16 }17 }18 memset(dp,0x3f,sizeof dp);19 dp[0] = 0;20 for(int i = 1;i <= n;i++){21 for(int j = 0;j < 1 << m;j++){22 dp[a[i]|j] = min(dp[a[i]|j],dp[j]+1);//核心代码23 }24 }25 int ans = dp[(1<<m)-1];26 if(ans == 0x3f3f3f3f) cout << -1;27 else cout << ans;28}xxxxxxxxxx131//dfs+记忆化写法 dp[i]表示从状态i走到目标状态1111的最少步数,如果dp[i]=0(且i!=n)则没有访问过该状态2//memset(dp,0,sizeof dp);3//int ans = dfs(0);4int dfs(int u){5 if(u == (1 << m)-1) return 0;//目标状态6 if(dp[u]) return dp[u];//诺状态u到之前达过,则直接返回7 int ans = 0x3f3f3f3f;//可以用dp[u] = 0x3f3f3f3f替换;8 for(int i = 1;i <= n;i++){9 if((u|a[i]) == u) continue;//只有状态发生改变才转移,防止死循环10 ans = min(ans,dfs(u|a[i])+1);11 }12 return dp[u] = ans;13}
树形DP
求解一颗有向树的最大权独立集,每条边最多选一个点
我们设
代表以 为根的子树的最优解(第二维的值为 0 代表 不参加舞会的情况,1 代表 参加舞会的情况)。 对于每个状态,都存在两种决策(其中下面的
都是 的儿子):
上司不参加舞会时,下属可以参加,也可以不参加,此时有
; 上司参加舞会时,下属都不会参加,此时有
。 我们可以通过 DFS,在返回上一层时更新当前结点的最优解。
xxxxxxxxxx421//https://www.luogu.com.cn/problem/P1352234using namespace std;5const int N = 6006;6int n,w[N];7int h[N], e[N], ne[N], idx;8int dp[N][2];//dp[i][0]表示i节点不参加,dp[i][1]表示i节点参加9bool st[N];//标记节点是否有父节点10
11void add(int a, int b) {12 e[idx] = b, ne[idx] = h[a], h[a] = idx++;13}14
15void dfs(int u) {16 dp[u][1] = w[u];17 for (int i = h[u]; i != -1;i = ne[i]) {18 int k = e[i];19 dfs(k);20 dp[u][0] += max(dp[k][0], dp[k][1]);//父节点参加时子节点可参加可不参加21 dp[u][1] += dp[k][0];//父节点参加时子节点都不能参加22 }23}24
25int main() {26 memset(h, -1, sizeof h);27 cin >> n;28 for (int i = 1; i <= n;i++) {29 cin >> w[i];30 }31 for (int i = 1; i < n;i++) {32 int a, b; cin >> a >> b;33 add(b, a);34 st[a] = 1;//标记a有父节点35 }36
37 int root = 1;38 while (st[root]) root++;39 dfs(root);//从根节点搜索40
41 cout << max(dp[root][0], dp[root][1]);42}
每个节点至少要连接一个被选择的点,并使所有选择的点总花费最小
dp[i,0] 表示未选择当前点,且选择了其父节点。 dp[i,1] 表示未选择当前点,且选择了至少一个子节点。 dp[i,2] 表示选择了当前点。
xxxxxxxxxx451234using namespace std;5const int N = 1505;6int n;7int h[N],e[N],ne[N],w[N],idx;8int dp[N][N];9bool st[N];10
11void add(int a,int b){12 e[idx] = b,ne[idx] = h[a],h[a] = idx++;13}14
15void dfs(int u){16 dp[u][2] = w[u];17 for(int i = h[u];~i;i = ne[i]){18 int k = e[i];19 dfs(k);20 dp[u][0] += min(dp[k][1],dp[k][2]);21 dp[u][2] += min({dp[k][0],dp[k][1],dp[k][2]});22 }23 dp[u][1] = 0x3f3f3f3f;24 for(int i = h[u];~i;i = ne[i]){25 int k = e[i];26 dp[u][1] = min(dp[u][1],dp[u][0]+dp[k][2]-min(dp[k][1],dp[k][2]));27 }28}29
30int main(){31 memset(h,-1,sizeof h);32 cin >> n;33 for(int i = 1;i <= n;i++){34 int a,k;cin >> a >> w[a] >> k;35 while(k--){36 int b;cin >> b;37 add(a,b);38 st[b] = 1;39 }40 }41 int root = 1;42 while(st[root]) root++;43 dfs(root);44 cout << min(dp[root][1],dp[root][2]);45}
树上背包
xxxxxxxxxx381//选课 https://www.luogu.com.cn/problem/P20142//O(N*M^2) 有依赖关系的背包问题3//对于森林,可以将每一棵树的根节点都接到一个"虚拟根节点0"上,以0作为根节点,dp[0][m+1]即为答案456using namespace std;7const int N = 305;8int n,m;9int s[N],k[N];10int h[N],e[N],ne[N],idx;11int dp[N][N];//选到节点i,价值不超过j的最大价值12
13void add(int a,int b){14 e[idx] = b,ne[idx] = h[a],h[a] = idx++;15}16
17void dfs(int u){//分组背包处理18 for(int i = h[u];~i;i = ne[i]){//枚举物品组19 dfs(e[i]);20 for(int j = m+1;j >= 0;j--){//枚举体积 j(m+1~0)21 for(int k = 0;k <= j-1;k++){//枚举决策 k(0~j-v[u])22 dp[u][j] = max(dp[u][j],dp[u][j-k]+dp[e[i]][k]);23 }24 }25 }26}27
28int main(){29 memset(h,-1,sizeof h);30 cin >> n >> m;31 for(int i = 1;i <= n;i++){32 cin >> k[i] >> s[i];33 for(int j = 1;j <= m;j++) dp[i][j] = s[i]; //初始状态34 add(k[i],i);35 }36 dfs(0);37 cout << dp[0][m+1];38}
换根DP
树形 DP 中的换根 DP 问题又被称为二次扫描,通常不会指定根结点,并且根结点的变化会对一些值,例如子结点深度和、点权和等产生影响。
通常需要两次 DFS,第一次 DFS 自底向上预处理诸如深度,子树大小,点权和之类的信息,在第二次 DFS 自顶向下开始运行换根动态规划。
xxxxxxxxxx521//https://www.luogu.com.cn/problem/P34782//给定一个n个点的树,请求出一个结点,使得以这个结点为根时,所有结点的深度之和最大。345using namespace std;6const int N = 2000006;7int n;8int h[N],e[N],ne[N],idx;9long long f[N],siz[N],deep[N];10
11void add(int a,int b){12 e[idx] = b,ne[idx] = h[a],h[a] = idx++;13}14
15void dfs_d(int u,int fa){16 siz[u] = 1;17 deep[u] = deep[fa]+1;18 for(int i = h[u];~i;i = ne[i]){19 int k = e[i];20 if(k == fa) continue;21 dfs_d(k,u);22 siz[u] += siz[k];23 }24}25
26void dfs_u(int u,int fa){27 for(int i = h[u];~i;i = ne[i]){28 int k = e[i];29 if(k == fa) continue;30 f[k] = f[u]-siz[k]+n-siz[k];31 dfs_u(k,u);32 }33}34
35int main(){36 memset(h,-1,sizeof h);37 cin >> n;38 for(int i = 1;i < n;i++){39 int a,b;cin >> a >> b;40 add(a,b);add(b,a);41 }42
43 dfs_d(1,0);//一般第一遍dfs由底回溯到根统计信息44 for(int i = 1;i <= n;i++) f[1] += deep[i];//任选一个点(一般为1)作为根45 dfs_u(1,0);//再dfs由根到底考虑将根不断转移到子节点,更新每个节点作为根时的答案46
47 long long ans = 0,p = 0;48 for(int i = 1;i <= n;i++){49 if(ans < f[i]){ ans = f[i]; p = i; }50 }51 cout << p;52}
xxxxxxxxxx611//hdu2196 computer https://acm.hdu.edu.cn/showproblem.php?pid=21962//求每个节点距离其它节点的最远距离345using namespace std;6const int N = 20004;7int n;8int h[N],e[N],ne[N],idx,w[N];9int d1[N],d2[N],p1[N],p2[N],up[N];//d1和d2为节点u向下的最大值和次大值,up为向上的最大值10
11void add(int a,int b,int c){12 w[idx] = c,e[idx] = b,ne[idx] = h[a],h[a] = idx++;13}14
15void dfs_d(int u,int fa){16 for(int i = h[u];~i;i = ne[i]){17 int k = e[i];18 if(k == fa) continue;19 dfs_d(k,u);20 int t = d1[k] + w[i];21 if(t > d1[u]){22 d2[u] = d1[u];d1[u] = t;23 p2[u] = p1[u];p1[u] = k;24 }25 else if(t > d2[u]){26 d2[u] = t;27 p2[u] = k;28 }29 }30}31
32void dfs_u(int u,int fa){33 for(int i = h[u];~i;i = ne[i]){34 int k = e[i];35 if(k == fa) continue;36 if(p1[u] == k){ up[k] = max(up[u],d2[u]) + w[i]; }37 else { up[k] = max(up[u],d1[u]) + w[i];}38 dfs_u(k,u);39 }40}41
42int main(){43 while(cin >> n){44 for(int i = 1;i <= n;i++){45 h[i] = -1; d1[i] = d2[i] = up[i] = 0;46 }47 idx = 0;48
49 for(int i = 2;i <= n;i++){50 int b,c;cin >> b >> c;51 add(i,b,c);add(b,i,c);52 }53 dfs_d(1,0);54 dfs_u(1,0);55
56 for(int i = 1;i <= n;i++){57 int now = max(d1[i],up[i]);58 cout << now << '\n';59 }60 }61}
记忆化搜索
记忆化搜索是一种通过记录已经遍历过的状态的信息,从而避免对同一状态重复遍历的搜索实现方式。
因为记忆化搜索确保了每个状态只访问一次,它也是一种常见的动态规划实现方式。
滑雪
xxxxxxxxxx381//https://ac.nowcoder.com/acm/problem/235954234using namespace std;5const int N = 305;6int n, m;7int a[N][N];8int dp[N][N];//dp[i][j]表示从i,j开始滑的最长路径9int px[] = { 0,0,-1,1 }, py[] = { 1,-1,0,0 };10
11int dfs(int x, int y) {12 if (dp[x][y]) return dp[x][y];//如果已经计算过了,就可以直接返回答案,避免重复计算13 dp[x][y] = 1;//注意dp[x][y]至少为1(四个方向都不能滑)14 for (int i = 0; i < 4;i++) {//枚举上下左右四个方向15 int dx = x + px[i], dy = y + py[i];16 if (dx >= 1 && dx <= n && dy >= 1 && dy <= m && a[x][y]>a[dx][dy]) {//判断能否从x,y滑向滑dx,dy17 dp[x][y] = max(dp[x][y], dfs(dx, dy) + 1);//更新dp[x][y]18 }19 }20 return dp[x][y];21}22
23int main() {24 cin >> n >> m;25 for (int i = 1; i <= n;i++) {26 for (int j = 1; j <= m;j++) {27 cin >> a[i][j];28 }29 }30
31 int ans = 0;//因为可以在任意一点开始滑,所以要遍历一遍滑雪场,取最大值32 for (int i = 1; i <= n;i++) {33 for (int j = 1; j <= m;j++) {34 ans = max(ans, dfs(i, j));35 }36 }37 cout << ans;38}
DP优化
单调队列优化
加入所需元素:向单调队列重复加入元素直到当前元素达到所求区间的右边界,这样就能保证所需元素都在单调队列中。
弹出越界队首:单调队列本质上是维护的是所有已插入元素的最值,但我们想要的往往是一个区间最值。于是我们弹出在左边界外的元素,以保证单调队列中的元素都在所求区间中。
获取最值:直接取队首作为答案即可。
xxxxxxxxxx241//烽火传递 https://loj.ac/p/101802//给定数组a[N],每连续m个元素至少有一个被选中,最小选中代价34using namespace std;5const int N = 200005;6int n,m,a[N];7int dp[N];//dp[i]表示选择第i个时的最小代价8int q[N],hh,tt;9
10int main(){11 cin >> n >> m;12 for(int i = 1;i <= n;i++) cin >> a[i];13
14 for(int i = 1;i <= n;i++){15 dp[i] = dp[q[hh]] + a[i];//队首即为最值 dp[i] = min(dp[i-m]~dp[i-1]) + a[i]16 if(hh <= tt && i - q[hh] >= m) hh++;//弹出越界队首17 while(hh <= tt && dp[i] <= dp[q[tt]]) tt--;//加入所需元素,并保持队列单调递增18 q[++tt] = i;19 }20
21 int ans = 0x3f3f3f3f;22 for(int i = n-m+1;i <= n;i++) ans = min(ans,dp[i]);23 cout << ans;24}
xxxxxxxxxx321//修剪草坪 https://loj.ac/p/101772//给定a[N],不能选择连续m个元素,求最大代价3//dp[i] = max(dp[i-1],max(dp[j-1]+sum(j+1,i))) //选/不选第i个4//其中dp[j-1]+sum(j+1,i) = dp[j-1] + s[i] - s[j] 只需要单调队列求出最大的dp[j-1]-s[j]56using namespace std;7const int N = 100005;8int n,m;9long long a[N],dp[N];10int q[N],hh,tt;11
12long long g(int i){13 return dp[i-1] - a[i];14}15
16int main(){17 cin >> n >> m;18 for(int i = 1;i <= n;i++){19 cin >> a[i]; a[i] += a[i-1];20 }21
22 for(int i = 1;i <= n;i++){23 dp[i] = max(dp[i-1],g(q[hh]) + a[i]);24 //区间[i-m,i-1],窗口大小为m (不包括i)25 if(hh <= tt && i - q[hh] >= m) hh++;26 //区间[i-m+1,i-1],窗口大小为m-1 (不包括i)27 while(hh <= tt && g(i) >= g(q[tt])) tt--;28 q[++tt] = i;29 //区间[i-m+1,i],窗口大小为m (包括i)30 }31 cout << dp[n];32}
数据结构优化
给定n个区间以及其花费{ [ l , r ] , c },选择合理的区间使其能够完全覆盖区间[st,ed],并使得总花费最小
将所有区间按右端点排序后,遍历n个区间,对于当前区间l,r,c
dp[r] = min(dp[r] , dp[l-1~r-1] + c)
考虑线段树优化,单点修改,求区间最小值
xxxxxxxxxx831234using namespace std;5const int N = 100005;6const long long INF = 0x3f3f3f3f3f3f3f3f;7int n,st,ed;8long long dp[N];9
10struct node{11 int l,r;12 long long c;13 bool operator < (const auto &e2){14 return r < e2.r;15 }16}a[N];17
18struct ST{19 int l,r;20 long long dat;21}t[N<<2];22
23void pushup(ST &p,ST &pl,ST &pr){24 p.dat = min(pl.dat,pr.dat);25}26
27void pushup(int p){28 pushup(t[p],t[p<<1],t[p<<1|1]);29}30
31void build(int p,int l,int r){32 t[p] = {l,r};33 if(l == r){34 t[p].dat = INF;35 return;36 }37 int mid = l + r >> 1;38 build(p<<1,l,mid);build(p<<1|1,mid+1,r);39 pushup(p);40}41
42void modify(int p,int l,int r,long long x){43 if(l <= t[p].l && r >= t[p].r){44 t[p].dat = x;45 return;46 }47 int mid = t[p].l + t[p].r >> 1;48 if(l <= mid) modify(p<<1,l,r,x);49 if(r > mid) modify(p<<1|1,l,r,x);50 pushup(p);51}52
53ST query(int p,int l,int r){54 if(l <= t[p].l && r >= t[p].r){55 return t[p];56 }57 int mid = t[p].l + t[p].r >> 1;58 if(r <= mid) return query(p<<1,l,r);59 if(l > mid) return query(p<<1|1,l,r);60 ST pl = query(p<<1,l,r),pr = query(p<<1|1,l,r),ans;61 pushup(ans,pl,pr);62 return ans;63}64
65int main(){66 cin >> n >> st >> ed;67 build(1,0,ed);68 for(int i = 1;i <= n;i++){69 cin >> a[i].l >> a[i].r >> a[i].c;70 }71 sort(a+1,a+n+1);72 memset(dp,0x3f,sizeof dp);73
74 for(int i = 1;i <= n;i++){75 auto [l,r,c] = a[i];76 if(l - 1 <= st - 1) dp[r] = min(dp[r],c);77 else dp[r] = min(dp[r],query(1,l-1,r-1).dat + c);78 modify(1,r,r,dp[r]);79 }80
81 if(dp[ed] == INF) cout << -1;82 else cout << dp[ed];83}
树状数组优化 UVA12983 (luogu.com.cn)
在长度为n的数列a[ ]中,求长度为m的严格上升子序列的个数
dp[i,j]表示以a[i]结尾,且长度为j的上升子序列个数
dp[i,j] =
其中1 <= k < i && a[k] < a[i] 考虑将a[ ]离散化后建立树状数组,query(id(a[i])-1) 为1~id(a[i])中的
,插入的先后顺序保证了不会重复计算到后面的值
xxxxxxxxxx571234using namespace std;5const int N = 1005,mod = 1e9+7;6int n,m;7int a[N],hs[N];8long long t[N],dp[N][N];9
10void add(int p,long long x){11 while(p <= n){12 t[p] = (t[p] + x)%mod;13 p += p&-p;14 }15}16
17long long query(int p){18 long long ans = 0;19 while(p > 0){20 ans = (ans + t[p]) % mod;21 p -= p&-p;22 }23 return ans;24}25
26void sol(){27 memset(dp,0,sizeof dp);28 cin >> n >> m;29
30 for(int i = 1;i <= n;i++){31 scanf("%d",&a[i]);hs[i] = a[i];32 }33 sort(hs+1,hs+n+1);34 for(int i = 1;i <= n;i++){ a[i] = lower_bound(hs+1,hs+n+1,a[i])-hs; }35
36 for(int i = 1;i <= n;i++){ dp[i][1] = 1;}37
38 for(int j = 2;j <= m;j++){39 memset(t,0,sizeof t);40 for(int i = 1;i <= n;i++){41 dp[i][j] = query(a[i]-1);42 add(a[i],dp[i][j-1]);43 }44 }45 long long ans = 0;46 for(int i = 1;i <= n;i++){47 ans = (ans + dp[i][m]) % mod;48 }49 cout << ans << '\n';50}51
52int main(){53 int T;cin >> T;54 for(int o = 1;o <= T;o++){55 cout << "Case #" << o << ": ";sol();56 }57}
斜率优化
【学习笔记】动态规划—斜率优化DP(超详细)——辰星凌的博客QAQ
先写出DP方程,例如:
写出DP方程后,判断能否使用斜率优化,即是否存在
通过大小于符号或者换
P10979 任务安排 2 - 洛谷 (luogu.com.cn)
把
以
由于本题中
xxxxxxxxxx33123using namespace std;4const int N = 300005;5long long n,s;6long long dp[N];7long long c[N],t[N],sc[N],st[N];8int q[N],hh,tt;9
10long long X(int i){return sc[i];}11long long Y(int i){return dp[i];}12long long up(int a,int b) {return Y(a) - Y(b);}//分子13long long down(int a,int b) {return X(a) - X(b);}//分母14
15
16int main(){17 cin >> n >> s;18 for(int i = 1;i <= n;i++){19 cin >> t[i] >> c[i];20 st[i] = st[i-1] + t[i];21 sc[i] = sc[i-1] + c[i];22 }23
24 for(int i = 1;i <= n;i++){25 int k = st[i] + s;26 while(hh <= tt-1 && up(q[hh+1],q[hh]) <= k * down(q[hh+1],q[hh])) hh++;27 int j = q[hh];28 dp[i] = dp[j] - (s+st[i])*sc[j] + st[i]*sc[i] + s*sc[n];29 while(hh <= tt-1 && up(i,q[tt])*down(q[tt],q[tt-1]) <= up(q[tt],q[tt-1])*down(i,q[tt]))tt--;30 q[++tt] = i;31 }32 cout << dp[n];33}
[P5785 SDOI2012] 任务安排 - 洛谷 (luogu.com.cn)
本题,诺k不满足单调性,则需要维护整个凸包,每次决策时只需要在队列中用二分/CDQ/平衡树找到点 j 满足:点j左边的斜率都小于k,右边的斜率都大于k。
xxxxxxxxxx47123using namespace std;4const int N = 300005;5long long n,s;6long long dp[N];7long long c[N],t[N],sc[N],st[N];8int q[N],hh,tt;9
10struct node{11 long long x,y;12};13
14int lb(long long k,int bl,int br){15 auto check = [&](int x)->bool{16 node p1 = {sc[q[x]],dp[q[x]]},p2 = {sc[q[x+1]],dp[q[x+1]]};17 return (p2.y - p1.y) >= k*(p2.x - p1.x);18 };19 int l = bl,r = br;20 while(l < r){21 int mid = l + r >> 1;22 if(check(mid)) r = mid;23 else l = mid + 1;24 }25 return q[r];26}27
28int main(){29 cin >> n >> s;30 for(int i = 1;i <= n;i++){31 cin >> t[i] >> c[i];32 st[i] = st[i-1] + t[i];33 sc[i] = sc[i-1] + c[i];34 }35 36 for(int i = 1;i <= n;i++){37 int j = lb(st[i]+s,hh,tt);38 dp[i] = dp[j] - (s+st[i])*sc[j] + st[i]*sc[i] + s*sc[n];39 while(hh <= tt - 1){40 node p1 = {sc[q[tt-1]],dp[q[tt-1]]},p2 = {sc[q[tt]],dp[q[tt]]},p3 = {sc[i],dp[i]};41 if(__int128(p3.y-p2.y)*(p2.x-p1.x) <= __int128(p2.y-p1.y)*(p3.x-p2.x)) tt--;42 else break;43 }44 q[++tt] = i;45 }46 cout << dp[n];47}
对于每只小猫所需要的早出发时间为
令dp[i][j]表示前i个人运输前j只小猫所需要的最小等待次数。第i个人运输第k+1~j只猫。则有:
去掉min,移项得:
斜率优化O(1)求出k,以k为横轴,dp[i-1][k]+s[k]为纵轴建立坐标系,当前斜率为a[j],坐标轴上每个点为(k,dp[i-1][k]+s[k])
xxxxxxxxxx531234using namespace std;5const int N = 100005;6int n,m,p;7long long d[N];8long long h[N],t[N],a[N],s[N];9long long dp[105][N];10int q[N],hh,tt;11
12struct node{13 long long x,y;14};15
16int main(){17 cin >> n >> m >> p;18 for(int i = 2;i <= n;i++){19 cin >> d[i];20 d[i] += d[i-1];21 }22 for(int i = 1;i <= m;i++){23 cin >> h[i] >> t[i];24 a[i] = t[i] - d[h[i]];25 }26 sort(a+1,a+m+1);27
28 for(int i = 1;i <= m;i++){29 s[i] = s[i-1] + a[i];30 }31
32 memset(dp,0x3f,sizeof dp);33 dp[0][0] = 0;34 for(int i = 1;i <= p;i++){35 hh = 0,tt = 0;36 for(int j = 1;j <= m;j++){37 while(hh <= tt - 1){//由于斜率a[j]递增,每次维护队首即可38 node p1 = {q[hh],dp[i-1][q[hh]]+s[q[hh]]},p2 = {q[hh+1],dp[i-1][q[hh+1]]+s[q[hh+1]]};39 if((p2.y-p1.y) <= a[j]*(p2.x-p1.x)) hh++;40 else break;41 }42 dp[i][j] = dp[i-1][q[hh]] + a[j]*(j-q[hh])-s[j]+s[q[hh]];43 while(hh <= tt-1){44 node p1 = {q[tt-1],dp[i-1][q[tt-1]]+s[q[tt-1]]},p2 = {q[tt],dp[i-1][q[tt]]+s[q[tt]]};45 node p3 = {j,dp[i-1][j]+s[j]};//插入的点p3,取值应为dp[i-1]46 if((p3.y-p2.y)*(p2.x-p1.x) <= (p2.y-p1.y)*(p3.x-p2.x)) tt--;47 else break;48 }49 q[++tt] = j;50 }51 }52 cout << dp[p][m];53}
其他
环形处理
断环成链,复制拼接
P10957 环路运输 - 洛谷 (luogu.com.cn)
xxxxxxxxxx2712using namespace std;3const int N = 2000006;4int n;5long long a[N];6int q[N],hh,tt;7
8long long g(int i){9 return a[i] - i;10}11
12int main(){13 cin >> n;14 for(int i = 1;i <= n;i++){15 cin >> a[i];16 a[i+n] = a[i];17 }18
19 long long ans = 0;20 for(int i = 1;i <= n << 1;i++){//单调队列优化21 ans = max(ans,a[i] + i + g(q[hh]));22 if(hh <= tt && i - q[hh] >= n/2) hh++;23 while(hh <= tt && g(i) >= g(q[tt])) tt--;24 q[++tt] = i;25 }26 cout << ans;27}
二次DP
一次选择N和1断开,另一次选择N和1连接。答案取两次DP的最值
[P6064 USACO05JAN] Naptime G - 洛谷 (luogu.com.cn)
xxxxxxxxxx381234using namespace std;5const int N = 3900;6int n,a[N],m;7int dp[2][N][2];8
9int main(){10 cin >> n >> m;11 for(int i = 1;i <= n;i++){12 cin >> a[i];13 }14
15 memset(dp,-0x3f,sizeof dp);16 for(int i = 1;i <= n;i++) dp[i&1][0][0] = 0;17 dp[1&1][1][1] = 0;18 for(int i = 2;i <= n;i++){19 dp[i&1][0][0] = dp[(i-1)&1][0][0];20 for(int j = 1;j <= m;j++){21 dp[i&1][j][0] = max(dp[(i-1)&1][j][0],dp[(i-1)&1][j][1]);22 dp[i&1][j][1] = max(dp[(i-1)&1][j-1][0],dp[(i-1)&1][j-1][1]+a[i]);23 }24 }25 int ans = max(dp[n&1][m][0],dp[n&1][m][1]);26
27 memset(dp,-0x3f,sizeof dp);28 for(int i = 1;i <= n;i++) dp[i&1][0][0] = 0;29 dp[1][1][1] = a[1];30 for(int i = 2;i <= n;i++){31 for(int j = 1;j <= m;j++){32 dp[i&1][j][0] = max(dp[(i-1)&1][j][0],dp[(i-1)&1][j][1]);33 dp[i&1][j][1] = max(dp[(i-1)&1][j-1][0],dp[(i-1)&1][j-1][1]+a[i]);34 }35 }36 ans = max(ans,dp[n&1][m][1]);37 cout << ans;38}
滚动数组
简要来说就是通过观察dp方程来判断需要使用哪些数据,可以抛弃哪些数据,一旦找到关系,就可以用新的数据不断覆盖旧的没用的数据来减少空间的使用。还要注意根据数据是否可以直接继承考虑是否需要情况数组
利用对自然数取模的周期性
xxxxxxxxxx31//例:递推求斐波那契数列2for (int i=3;i<=n;i++) f[i%3]=f[(i-1)%3]+f[(i-2)%3];3cout << f[n%3];
xxxxxxxxxx31dp[i][s1][s2] = max(dp[i][s1][s2],dp[i-1&1][s2][s3]+1);2//只用到了i-1项,可以优化为3dp[i&1][s1][s2] = max(dp[i&1][s1][s2],dp[i-1&1][s2][s3]+1);
xxxxxxxxxx231for(int i = 1;i <= n+1;i++){//dp[N][M]2 for(auto s1:st[i-1]){3 for(auto s2:st[i]){4 if(s1&s2) continue;5 dp[i][s2] = (dp[i][s2]+dp[i-1][s1])%mod;6 }7 }8}9//可以优化为10for(int i = 1;i <= n+1;i++){//dp[2][M]11 for(auto s1:st[i-1]){12 for(auto s2:st[i]){13 if(s1&s2) continue;14 dp[i&1][s2] = 0;15 }16 }17 for(auto s1:st[i-1]){18 for(auto s2:st[i]){19 if(s1&s2) continue;20 dp[i&1][s2] = (dp[i&1][s2]+dp[i-1&1][s1])%mod;21 }22 }23}
状态机DP
给定m个DNA序列片段,计算有多少长度为n的DNA序列不包含上述m个片段
xxxxxxxxxx13812345
6namespace ACAM{7 const int N = 105,M = 4;8 int son[N][M],cnt[N],fail[N],idx;9
10 struct Trie{11
12 Trie(){idx = 0;init(idx);};13
14 void init(int p){15 fail[p] = cnt[p] = 0;16 std::memset(son[p],0,sizeof son[p]);17 }18
19 int get(char x){20 if(x == 'A') return 0;21 if(x == 'C') return 1;22 if(x == 'T') return 2;23 if(x == 'G') return 3;24 return 0;25 }26
27 void insert(const std::string &s){28 int p = 0;29 for(int i = 0;i < s.size();i++){30 int u = get(s[i]);31 if(!son[p][u]){32 son[p][u] = ++idx;33 init(idx);34 }35 p = son[p][u];36 }37 cnt[p]++;38 }39
40 void get_fail(){41 std::queue<int>q;42 for(int u = 0;u < M;u++){43 if(son[0][u]) {44 fail[son[0][u]] = 0;45 q.push(son[0][u]);46 }47 }48 while(q.size()){49 int p = q.front();50 q.pop();51 cnt[p] |= cnt[fail[p]];//将当前节点的禁止状态与fail指针的禁止状态合并52 for(int u = 0;u < M;u++){53 if(son[p][u]) {54 fail[son[p][u]] = son[fail[p]][u];55 q.push(son[p][u]);56 }57 else{58 son[p][u] = son[fail[p]][u];59 }60 }61 }62 }63 };64}65using ACAM::Trie;66using ACAM::fail,ACAM::son,ACAM::cnt,ACAM::idx;67
68const int mod = 100000;69template<typename T>70struct Mat{71 int n;72 std::vector<std::vector<T>>a;73
74 Mat(int _n,T val){75 n = _n;76 a = std::vector<std::vector<T>>(n,std::vector<T>(n,val));77 }78
79 Mat<T> operator * (const Mat<T> &m2){80 Mat<T> ans(n,0);81 for(int i = 0;i < n;i++){82 for(int j = 0;j < n;j++){83 for(int k = 0;k < n;k++){84 ans.a[i][j] = (ans.a[i][j] + a[i][k] * m2.a[k][j]) % mod;85 }86 }87 }88 return ans;89 }90
91 void norm(){92 for(int i = 0;i < n;i++) a[i][i] = 1;93 }94
95 Mat<T> qmi(long long b){96 auto base = *this;97 Mat<T> ans(n,0);98 ans.norm();99 while(b){100 if(b&1) ans = ans * base;101 b >>= 1;102 base = base * base;103 }104 return ans;105 }106};107
108const int N = 12;109std::string s[N];110
111int main(){112 int m,n; std::cin >> m >> n;113 Trie t;114 for(int i = 1;i <= m;i++){115 std::cin >> s[i];116 t.insert(s[i]);117 }118 t.get_fail();119
120 Mat<long long> base(idx+1,0);121
122 for(int i = 0;i <= idx;i++){123 for(int u = 0;u < 4;u++){124 int p = son[i][u];125 if(cnt[p]) continue;126 base.a[p][i]++;//状态i通过字符c转移至状态p //dp[k][p] += dp[k-1][i]127 }128 }129
130 base = base.qmi(n);//矩阵快速幂加速递推131
132 long long res = 0;133 for(int i = 0;i <= idx;i++){134 res = (res + base.a[i][0]) % mod;135 }136
137 std::cout << res;138}
贪心
区间问题
区间选点
给定 N 个闭区间 [ai,bi] ,请你在数轴上选择尽量少的点,使得每个区间内至少包含一个选出的点。 输出选择的点的最小数量。(位于区间端点上的点也算作区间内)
xxxxxxxxxx311//https://www.acwing.com/problem/content/907/234using namespace std;5const int N = 100005;6int n;7
8struct Edge{9 int l,r;10}e[N];11
12int main(){13 cin >> n;14 for(int i = 1;i <= n;i++){ cin >> e[i].l >> e[i].r; }15 16 sort(e+1,e+n+1,[](auto&e1,auto&e2){return e1.l < e2.l;});//按左端点从小到大排序17
18 int ans = 1;19 int ed = e[1].r;20
21 for(int i = 2;i <= n;i++){22 if(e[i].l > ed){23 ans++;24 ed = e[i].r;25 }26 else{27 ed = min(ed,e[i].r);28 }29 }30 cout << ans;31}xxxxxxxxxx491//畜栏预定 https://www.acwing.com/problem/content/113/2//给定N段区间[l,r],当两个区间相交时(包括端点),这两头牛不能安排在同一个畜栏吃草3//求需要的最小畜栏数目和每头牛对应的畜栏方案4567using namespace std;8const int N = 100005;9using pii = pair<int,int>;10int n;11int id[N];12
13struct Edge{14 int l,r,rk;15}e[N];16
17int main(){18 cin >> n;19 for(int i = 1;i <= n;i++){20 cin >> e[i].l >> e[i].r;21 e[i].rk = i;22 }23
24 sort(e+1,e+n+1,[](auto &e1,auto &e2){return e1.l < e2.l;});25 //按左端点将区间排序26
27 priority_queue<pii,vector<pii>,greater<pii>>pq;28 //堆中存放的元素为<当前分组内区间右端点的最小值,畜栏编号>29
30 for(int i = 1;i <= n;i++){31 if(pq.empty() || pq.top().first >= e[i].l){//新建堆32 //当前堆为空 或 堆顶最小值右端点r与当前牛左端点l重合33 pii t = {e[i].r,pq.size()};34 id[e[i].rk] = t.second;35 pq.push(t);36 }37 else{//使用原来的堆38 auto t = pq.top();39 pq.pop();//弹出堆顶40 t.first = e[i].r;//让当前牛使用该畜栏41 id[e[i].rk] = t.second;//记录答案42 pq.push(t);43 }44 }45 cout << pq.size() << endl;46 for(int i = 1;i <= n;i++){47 cout << id[i]+1 << endl;48 }49}
区间合并
现给定 𝑛个闭区间 𝑎𝑖,𝑏𝑖(1≤𝑖≤𝑛)。这些区间的并可以表示为一些不相交的闭区间的并。在这些表示方式中找出包含最少区间的方案。
xxxxxxxxxx411//https://www.luogu.com.cn/problem/P243423using PII = std::pair<int, int>;4using namespace std;5
6struct P {7 int l,r;8}p[50005];9
10queue<PII>q;11
12bool cmp(P p1, P p2) {13 return p1.l < p2.l;14}15
16int main() {17 int n; cin >> n;18 for (int i = 0; i < n; i++) {19 cin >> p[i].l >> p[i].r;20 }21 22 sort(p, p + n, cmp);23 24 int op = p[0].l, ed = p[0].r;25 for (int i = 0; i < n; i++) {26 if (ed >= p[i].l) {27 ed = max(ed, p[i].r);28 }29 else {30 q.push({ op,ed });31 op = p[i].l;32 ed = p[i].r;33 }34 }35 q.push({ op,ed });36
37 while (q.size()) {38 cout << q.front().first << ' ' << q.front().second << endl;39 q.pop();40 }41}
不相交区间
给定 𝑁 个闭区间 [𝑎𝑖,𝑏𝑖],请你在数轴上选择若干区间,使得选中的区间之间互不相交(包括端点)。 输出可选取区间的最大数量。
xxxxxxxxxx351//https://www.acwing.com/problem/content/910/234using namespace std;5const int N = 100005;6int n;7struct Point {8 int x, y;9}p[N];10
11bool cmp(Point p1, Point p2) {12 return p1.x < p2.x;13}14
15int main() {16 cin >> n;17 for (int i = 1; i <= n;i++) {18 cin >> p[i].x >> p[i].y;19 }20
21 sort(p + 1, p + n + 1,cmp);22
23 int ans = 1;24 int r = p[1].y;25 for (int i = 2; i <= n; i++) {26 if (p[i].y < r) {27 r = p[i].y;28 }29 if (p[i].x > r) {30 ans++;31 r = p[i].y;32 }33 }34 cout << ans;35}
区间覆盖
给定 N 个闭区间 [𝑎𝑖,𝑏𝑖] 以及一个线段区间 [𝑠,𝑡],请你选择尽量少的区间,将指定线段区间完全覆盖。 输出最少区间数,如果无法完全覆盖则输出 −1。
核心思想:在左端点l都小于a的情况下,取右端点最大的小区间
xxxxxxxxxx471//https://www.acwing.com/problem/content/description/909/234using namespace std;5const int N = 100005,INF = 0x3f3f3f3f;6
7struct P {8 int x, y;9}p[N];10
11bool cmp(P p1, P p2) {12 return p1.x < p2.x;13}14
15int main() {16 int a, b; cin >> a >> b;17 int n; cin >> n;18 for (int i = 1; i <= n;i++) {19 cin >> p[i].x >> p[i].y;20 }21
22 sort(p + 1, p + n + 1, cmp);23 int ans = 0;24 bool flag = 0;25 for (int i = 1; i <= n;i++) {26 int j = i, r = -INF;27 while (j <= n && p[j].x <= a){28 r = max(r, p[j].y);29 j++;30 }31
32 if (r < a) {33 ans = -1;34 break;35 }36 ans++;37 if (r >= b) {38 flag = 1;39 break;40 }41 a = r;42 i = j - 1;43 }44 if (!flag) ans = -1;45 cout << ans;46 return 0;47}
区间分组
(某个点)最多同时重叠区间个数
给定 𝑁 个闭区间 [𝑎𝑖,𝑏𝑖],请你将这些区间分成若干组,使得每组内部的区间两两之间(包括端点)没有交集,并使得组数尽可能小。输出最小组数。
我们可以把所有开始时间和结束时间排序,遇到开始时间就把需要的教室数cnt加1,遇到结束时间就把需要的教室数cnt减1,在一系列需要的教室个数cnt变化的过程中,cnt的峰值就是多同时进行的活动数,也是我们至少需要的教室数。
如果值域较小,可以写差分
xxxxxxxxxx321//https://www.acwing.com/problem/content/908/23456using namespace std;7const int N = 100005;8vector<pair<int,int>>v;9
10struct Edge{11 int l,r;12}e[N];13
14int main(){15 fastio;16 int n;cin >> n;17 for(int i = 1;i <= n;i++){18 auto&[l,r] = e[i];19 cin >> l >> r;20 v.emplace_back(l,0);//0代表开始21 v.emplace_back(r,1);//1代表结束22 }23 sort(v.begin(),v.end());24 int ans = 0;25 int cnt = 0;26 for(int i = 0;i < v.size();i++){27 if(v[i].second == 0) cnt++;28 else cnt--;29 ans = max(ans,cnt);30 }31 cout << ans;32}
(任意长度为d的区间)最多/最少包含不同区间个数(重叠区间的长短并不重要)
xxxxxxxxxx271//https://codeforces.com/contest/2014/problem/D234using ll = long long;5using namespace std;6
7void sol(){8 ll n,d,k;cin >> n >> d >> k;9 vector<ll>a(n+5);10 for(int i = 1;i <= k;i++){//差分,让区间[l-d+1,r]加111 int l,r;cin >> l >> r;12 a[max(l-d+1,(ll)0)]++;13 a[r+1]--;14 }15 for(int i = 1;i <= n;i++){ a[i] += a[i-1];}//a[i]表示从点i开始,长为d的一段区间,包含不同区间的个数16 ll kb = 0,pb = 0,km = INF,pm = 0;17 for(int i = 1;i <= n-d+1;i++){18 if(a[i] > kb){ kb = a[i];pb = i;}19 if(a[i] < km){ km = a[i];pm = i;}20 }21 cout << pb << ' ' << pm << endl;22}23
24int main() {25 int T = 1;cin >> T;26 while(T--){ sol(); }27}
区间包含
xxxxxxxxxx761//https://acm.hdu.edu.cn/showproblem.php?pid=74972345678using namespace std;9int n,m;10
11struct Edge{12 ll l,r;13 ll len;14};15
16bool cmp(ll x,ll y){17 return x < y;18}19
20bool cmp1(pair<ll,ll>x,pair<ll,ll>y){21 if(x.first != y.first) return x.first < y.first;22 else{ return x.second < y.second; }23}24
25void sol(){26 cin >> n >> m;27 vector<Edge>a(n),b(m);28 vector<ll>all;29 for(int i = 0;i < n;i++){30 cin >> a[i].l >> a[i].r;31 all.emplace_back(a[i].l*2+1);32 all.emplace_back(a[i].r*2);33 }34 for(int i = 0;i < m;i++){35 cin >> b[i].l >> b[i].r; 36 b[i].len = 2*(b[i].r-b[i].l);37 all.emplace_back(b[i].l*2+1);38 all.emplace_back(b[i].r*2);39 }40 sort(all.begin(),all.end(),cmp);41 int cnt = 0;42 for(int i = 0;i < all.size();i++){//判断A组区间与B组区间中是否存在区间香蕉(区间分组)43 if(all[i]&1){ cnt++; }44 else { cnt--; }45 if(cnt > 1){ cout << "No" << endl; return; }46 }47 48 vector<pair<ll,ll>>al;49 for(int i = 0;i < n;i++){50 al.emplace_back(a[i].l,2);51 al.emplace_back(a[i].r,3);52 }53 for(int i = 0;i < m;i++){54 al.emplace_back(b[i].r,0);55 al.emplace_back(b[i].r+b[i].len,4);56 }57 sort(al.begin(),al.end(),cmp1);58
59 int ok = 0,cla = 0;//判断A组区间是否完全包含于C组区间(区间分组加强版)60 for(int i = 0;i < al.size();i++){61 if(al[i].second == 0) ok++;//0代表清醒状态开始62 if(al[i].second == 4) ok--;//4代表清醒状态结束63 if(al[i].second == 2) cla++;//2代表上课开始64 if(al[i].second == 3) cla--;//3代表上课结束65 if(cla >= 1 && ok <= 0){//如果出现当前正在上课,并且状态不清醒,返回No66 cout << "No" << endl; return;67 }68 }69 cout << "Yes" << endl;70}71
72int main() {73 fastio; int T = 1;74 cin >> T;75 while(T--){sol();}76}
绝对值不等式
货仓选址
在一条数轴上有 𝑁 家商店,它们的坐标分别为 𝐴1∼𝐴𝑁。 现在需要在数轴上(任意一点)建立一家货仓,每天清晨,从货仓到每家商店都要运送一车商品。 为了提高效率,求把货仓建在何处,可以使得货仓到每家商店的距离之和最小。
在中位数处建点可以使得答案最小
xxxxxxxxxx201//https://www.acwing.com/problem/content/106/2345using namespace std;6const int N = 100005;7int n,a[N];8
9int main(){10 cin >> n;11 for(int i = 1;i <= n;i++){12 cin >> a[i];13 }14 sort(a+1,a+n+1);15 long long ans = 0;16 for(int i = 1;i <= n;i++){17 ans+=abs(a[i]-a[n+1 >> 1]);18 }19 cout << ans;20}
均分纸牌
n个人坐在一排,每人有a[i]张纸牌,每次可将任意数量纸牌给相邻的一个人,求使所有人纸牌数相等的最小操作次数
xxxxxxxxxx241//https://www.acwing.com/problem/content/1538/23using namespace std;4const int N = 10004;5int a[N];6int ans,sum;7
8int main(){9 int n;cin >>n;10 for(int i = 1;i <= n;i++){11 cin >> a[i];12 sum += a[i];13 }14 int avg = sum/n;15
16 for(int i = 1;i < n;i++) {17 a[i] -= avg;//最终所有人的纸牌一定变为平均数18 if(a[i]){//不够的从i+1取,多的给i+119 a[i+1] += a[i];20 ans++;21 }22 } 23 cout << ans;//每次可移动任意张,最小次数24}
环形均分纸牌
n个人围在一圈,每人有a[i]张纸牌,每次可将1张纸牌给相邻的一个人,求使所有人纸牌数相等的最小操作次数
xxxxxxxxxx311//https://www.luogu.com.cn/problem/P2512234using namespace std;5using ll = long long;6const int N = 1000006;7ll a[N],s[N];8int n;9ll sum,ans;10
11int main(){12 cin >> n;13 for(int i = 1;i <= n;i++) cin >> a[i],sum+=a[i];14
15 ll avg = sum/n;16
17 for(int i = 1;i <= n;i++){18 s[i] = s[i-1] + a[i] - avg;19 }20 //s为a的所差的前缀和数组21 //选取s的中位数最优,问题变为对s数组的货仓选址问题22
23 sort(s+1,s+n+1);24
25 ll mid = s[(n+1)/2];26
27 for(int i = 1;i <= n;i++){28 ans += abs(s[i] - mid);29 } 30 cout << ans;//每次移动一张,最小次数31}
排序不等式
邻项交换法 证明在任意局面下,任何对局部最优策略的微小改变都会造成整体结果变差。经常用于以“排序”为贪心策略的证明。
诺交换相邻两项不会影响其他项的值,单独分析这两项,找出排序策略cmp
耍杂技的牛
奶牛们站在彼此的身上,形成一个高高的垂直堆叠。 这 𝑁 头奶牛中的每一头都有着自己的重量 𝑊𝑖 以及自己的强壮程度 𝑆𝑖。 一头牛支撑不住的可能性取决于它头上所有牛的总重量(不包括它自己)减去它的身体强壮程度的值,现在称该数值为风险值,风险值越大,这只牛撑不住的可能性越高。 确定奶牛的排序,使得所有奶牛的风险值中的最大值尽可能的小,求出该值。
将所有牛按w+s值从小到大排序时,最大的风险值一定最小
相邻两牛交换不会影响其它牛的风险值,所以只需要分析这两头牛即可
风险值 i牛 i+1牛 交换前: w[1]+....w[i-1]-s[i] w[1]+...+w[i-1]+w[i]-s[i+1] 交换后: w[1]+...+w[i-1]-s[i+1] w[1]+...+w[i-1]+w[i+1]-s[i] 交换前(化简): s[i+1] w[i]+s[i] 交换后(化简): s[i] w[i+1]+s[i+1] i牛风险值 -= s, i+1牛风险值 += s+w
xxxxxxxxxx291//https://www.acwing.com/problem/content/127/234using namespace std;5const int N = 50004;6int n;7struct S{8 int sum,w,s;9}s[N];10long long suf[N];11
12bool cmp(S x,S y){13 return x.sum < y.sum;14}15
16int main(){17 cin >> n;18 for(int i = 1;i <= n;i++){19 int a,b;cin >> a >> b;20 s[i] = {a+b,a,b};21 }22 sort(s+1,s+n+1,cmp);23 long long ans = -0x3f3f3f3f;24 for(int i = 1;i <= n;i++){25 suf[i] = suf[i-1]+s[i].w;26 ans = max(ans,suf[i-1]-s[i].s);27 }28 cout << ans;29}
xxxxxxxxxx21//栈压缩:https://qoj.ac/problem/93792//国王游戏:https://www.acwing.com/problem/content/description/116/
后悔解法
无论当前的选项是否最优都接受,然后进行比较,如果选择之后不是最优了,则反悔,舍弃掉这个选项;否则,正式接受。如此往复。
给定N个工作,每个工作截止日期为
,如果能在截止日期前完成则会获得 的报酬,每天只能完成一项工作。
xxxxxxxxxx431//https://www.luogu.com.cn/problem/P29492345using namespace std;6using ll = long long;7using pii = pair<ll,ll>;8const int N = 100005;9ll ans;10
11struct Edge{12 ll d,p;13}e[N];14
15
16int main(){17 int n;cin >> n;18 for(int i = 1; i <= n;i++){ cin >> e[i].d >> e[i].p; }19
20 sort(e+1,e+n+1,[](auto &e1,auto &e2){return e1.d < e2.d;});21 //将每一项工作按截止时间从小到大排序22 23 priority_queue<ll,vector<ll>,greater<ll>>pq;//小根堆存决定做的工作的报酬p24 //pq.size()即为做这些工作的最少安排时间25 for(int i = 1;i <= n;i++){26 if(e[i].d <= pq.size()){//如果当前工作截止时间与已经安排的时间冲突27 if(e[i].p > pq.top()){//且选择当前工作比之前决定的工作报酬更高,则反悔之前报酬最低的决定28 //ans += e[i].p - pq.top();29 pq.pop();30 pq.push(e[i].p);31 }32 }33 else{//否则安排当前决定34 //ans += e[i].p;35 pq.push(e[i].p);36 }37 }38 while(pq.size()){39 ans += pq.top();40 pq.pop();41 }42 cout << ans;43}
xxxxxxxxxx281//https://codeforces.com/problemset/problem/1526/C22//给你一个长度为n的序列A[],要求你找出最长的一个子序列使得这个子序列任意前缀和都非负。34int T = 1;5using ll = long long;6using namespace std;7
8void sol(){9 int n;cin >>n;10 unsigned ans = 0;11 ll sum = 0;12 priority_queue<ll,vector<ll>,greater<ll>>pq;13 while(n--){14 int x;cin >> x;15 pq.push(x);//无论当前选择是否最优,先接受16 sum += x;17 while(sum < 0){//诺接受后不是最优,则反悔之前最差的决定18 sum -= pq.top();19 pq.pop();20 }21 ans = max(ans,pq.size());22 }23 cout << ans << endl;24}25
26int main() {27 while(T--){ sol(); }28}
其它
字符与数字转换
ASCII码范围为0~127,注意不要越界,否则会乱码
字符串 <=> 数字
xxxxxxxxxx101//字符串->数字2//atoi整型 atol长整型 atoll长长整型 atof浮点型 默认参数为const char*类型3//stoi类似,默认参数为const string*类型,且超出int范围会报错4
5string s1 = "123456";6int n = stoi(s1); //string类型字符串7int n1 = atoi(s1.c_str()); //c_str() 函数可以将 const string* 类型 转化为 cons char* 类型8
9char s2[] = "123456";10int n2 = atoi(s2); //char[]类型字符串xxxxxxxxxx31//数字->字符串2int n = 541;3string a = to_string(n);//#include<string>xxxxxxxxxx71//数字<=>字符串2//使用sstream 输入流3stringstream ss;4int n = 123456;5string str;6ss << n;7ss >> str;
字符c <=> 数字n
xxxxxxxxxx21//字符c 转 数字n2n = c - '0' ; //或者 -48xxxxxxxxxx21//数字n 转 字符c2c = n + '0';
大小写转换
xxxxxxxxxx11s[i] ^= ' ';//大小写互转 空格' '为char(32)xxxxxxxxxx21s[i] = tolower(s[i]);//转小写 s[i] |= ' '2s[i] = toupper(s[i]);//转大写 s[i] &= ~' '
进制转换
stoi k进制转十进制
stoi(字符串, 0, k进制):将一串k进制的字符串转换为 int 型数字。stoll,stoull,stod,stold 同理。
xxxxxxxxxx81//s为0~9 + 'a'~'z'23using namespace std;4int main(){5 string s = "100";6 int n = stoi(s,0,16);//将16进制的s转为十进制7 cout << n << endl;8}xxxxxxxxxx101//s为数字2ll calc(vector<int> s,int k){3 ll ans = 0;4 ll base = 1;5 for(int i = s.size()-1;i >= 0;i--){6 ans += s[i]*base;7 base *= k;8 }9 return ans;10}
十进制转k进制
建议直接手写进制转换函数
xxxxxxxxxx241//n是待转换的十进制数,m是待转换成的进制数2//以数字+字母表示3string intToA(int n,int k){4 string ans="";5 do{ //使用do循环防止n为0的情况6 int t =n %k;7 if(t>=0&&t<=9) ans+=(t+'0');8 else ans+=(t-10+'a');9 n/=k;10 }while(n);11 reverse(ans.begin(),ans.end());12 return ans; 13}14
15//以数字表示16vector<int> calc(int n , int m){17 vector<int>ans;18 do{19 ans.push_back(n%m);20 n /= m;21 }while(n);22 reverse(ans.begin(),ans.end());23 return ans;24}
xxxxxxxxxx161//sprintf十进制转8/16进制23using namespace std;4
5int main() {6 int n; cin >> n;7 char s[100];8 9 //x/X -16进制(大小写)10 //o -8进制11 //d -10进制 12 sprintf(s, "%x", n);13 cout << s << endl;14
15 return 0;16}
itoa
【注意】 不建议使用 itoa并不是一个标准的C函数,它是Windows特有的,如果要写跨平台的程序,需要用spraint。
【函数原型】char *itoa(int value, char *string, int radix);
【参数说明】 value:要转换的数据。 string:目标字符串的地址。 radix:转换后的进制数,可以是10进制、16进制等,范围必须在 2-36。
xxxxxxxxxx912using namespace std;3int main() {4 int n;cin >> n; 5 char str[100];6 _itoa(n, str,36 );//例:10进制转36进制存于字符串str中7 cout << str;8 return 0;9}
精度/溢出问题
xxxxxxxxxx51double n = 3.8;2double ans = n - int(n);3ans*=10;4int ans1 =int(ans);5cout << ans1 << endl;//预期为8,输出结果却是7xxxxxxxxxx612double n = 3.8;3double ans = n - int(n);4ans*=10;5int ans1 =int(ans + eps);//对于负数的情况,只需要把ans + eps改为ans - eps即可6cout << ans1 << endl;//正确输出8现在考虑一种情况,题目要求输出保留两位小数。有个case的正确答案的精确值是0.005,按理应该输出0.01,但你的结果可能是0.005000000001(恭喜),也有可能是0.004999999999(悲剧),如果按照printf(“%.2lf”, a)输出,那你的遭遇将和括号里的字相同。
解决办法是,如果a为正,则输出a+eps, 否则输出a-eps
两个浮点数之间的比较,eps缩写自epsilon,表示一个小量,但这个小量又要确保远大于浮点运算结果的不确定量。eps最常见的取值是1e-8左右。引入eps后,我们判断两浮点数a、b相等的方式如下:
定义三出口函数如下: int sgn(double a){return a < -eps ? -1 : a < eps ? 0 : 1;}
则各种判断大小的运算都应做如下修正:
| 传统意义 | 修正写法1 | 修正写法2 |
|---|---|---|
| a == b | sgn(a - b) == 0 | fabs(a – b) < eps |
| a != b | sgn(a - b) != 0 | fabs(a – b) > eps |
| a < b | sgn(a - b) < 0 | a – b < -eps |
| a <= b | sgn(a - b) <= 0 | a – b < eps |
| a > b | sgn(a - b) > 0 | a – b > eps |
| a >= b | sgn(a - b) >= 0 | a – b > -eps |
除法之间比较尽量转换为乘法之间比较,如
判断改为 或者利用乘法逆元判断
xxxxxxxxxx501//https://ac.nowcoder.com/acm/contest/93218/D2//求平面上有多少个不同的直线34int T = 1; using ll = long long;5const int mod = 1e9+7;6using namespace std;7const int N = 1003;8int n;9ll x[N],y[N];10
11ll qmi(ll a,ll b,ll p){12 ll ans = 1;13 while(b){14 if(b&1) ans = ans*a%p;15 b>>=1;16 a = a*a%p;17 }18 return ans%p;19}20
21pair<ll,ll> uuz(ll x1,ll y1,ll x2,ll y2){22 ll k;23 if(y1 == y2) k = 1e18;//特判斜率不存在的情况24 else if(x1 == x2) k = 0;25 else k = (x1-x2)*qmi(y2-y1,mod-2,mod)%mod;//除法转乘法逆元26 ll b = (y2*y2-y1*y1+x2*x2-x1*x1)*qmi(2*(y2-y1),mod-2,mod)%mod;27 return {k,b};28}29
30void sol(){31 cin >> n;32 for(int i = 1;i <= n;i++){cin >> x[i];}33 for(int i = 1;i <= n;i++){cin >> y[i];}34
35 map<pair<ll,ll>,int>mp;36
37 for(int i = 1;i <= n;i++){38 for(int j = i+1;j <= n;j++){39 auto [k,b] = uuz(x[i],y[i],x[j],y[j]);40 if(k == 1e18) mp[{k,x[i]+x[j]}]++;41 else mp[{k,b}]++;42 }43 }44 cout << mp.size() << '\n';45}46
47int main() {48 cin >> T;49 while(T--){ sol(); }50}
sqrt
xxxxxxxxxx61int ans1 = 0,ans2 = 0,ans3 = 0;2for(int i = 1;i <= 100;i++){3 if(i == sqrt(i)*sqrt(i)) ans1++;//534 if(i - sqrt(i)*sqrt(i) < 1e-9) ans2++;//1005 if(i == (int)sqrt(i)*sqrt(i)) ans3++;//106}sqrtl返回值为long double类型,精度更高。Yet Another Simple Math Problem - Problem - QOJ.ac
pow/ceil/floor/round参数类型和返回值类型均为浮点型,可能会导致输出与预期不符而wrong answer
xxxxxxxxxx31int a = 1234;2cout << a*a << endl;//15227563cout << pow(a, 2) << endl;//1.52276e+06 可以用int n = pow(a,2)类型转换再输出n
有符号整型 - 无符号整型
xxxxxxxxxx71vector<int>v(3);2int n = 2;3while(n - v.size() > 0){4 //预期2-3 = -15 //实际却是4294967295导致死循环6 //应改写为while(n > s.size()) 或 while(n > (int)v.size())7}
输入输出优化
使用cin/cout会比scanf/printf慢
xxxxxxxxxx41//关闭cstdio和iostream的同步后,cin/cout不要与printf/scanf/puts混用,也不要再使用endl,否则会造成输入输出混乱2ios::sync_with_stdio(false);3cin.tie(0);4cout.tie(0);
xxxxxxxxxx21//endl每次会清空缓冲区,效率比'\n'慢很多2
xxxxxxxxxx201//快读快写 不要与ios::sync_with_stdio(false)同时使用2inline int read(){3 int x = 0,f = 1;4 char ch = getchar();//linux可以用更快的getchar_unlocked()5 while(ch < '0' || ch > '9'){//跳过空格回车等其他字符+判断正负6 if(ch == '-') f = -1;7 ch = getchar();8 }9 while(ch >= '0' && ch <= '9'){//读取数字10 x = x * 10 + ch - '0';11 ch = getchar();12 } 13 return x * f;14}15
16void write(int x) {17 if (x < 0) putchar('-'), x = ~x + 1;18 if (x > 9) write(x / 10);19 putchar(x % 10 + '0');20}
xxxxxxxxxx21//O2优化,程序开头加入2
xxxxxxxxxx481//火车头?23456789101112131415161718192021222324252627282930313233343536373839404142434445464748
交互题
输入由评测机输入,根据输入的内容输出询问,直到确定正确答案,这一类题目,往往限制你交流(或询问)的次数,让你猜出一个东西来,此类问题比较经典的技巧有二分答案、随机化
需要注意的是,如果输出了一些数据,这些数据可能被放置于内部缓存区里,而且或许没有被直接传输给interactor,出现Idleness limit exceeded。为了避免这种情况的发生,需要每次输出时用一种特殊的清除缓存操作。
xxxxxxxxxx41fflush(stdout);//方式一2cout << flush; //方式二3//endl换行时也会清除缓存,但'\n'不会4//最好不要使用快读、关同步流等
xxxxxxxxxx231//https://codeforces.com/contest/1999/problem/G123using namespace std;4
5void sol() {6 int l = 2, r = 1000;7 while (l < r) {8 int mid = l + r >> 1;9 cout << "? 1 " << mid <<endl;10 int res; cin >> res;11 if (mid != res) r = mid;12 else l = mid + 1;13 }14 cout << "! " << l << endl;15}16
17int main() {18 int T = 1;19 cin >> T;20 while (T--) {21 sol();22 }23}
一些语法/标准
重载运算符
一些可重载运算符的列举
一元运算:
+(正号);-(负号);~(按位取反);++;--;!(逻辑非);*(取指针对应值);&(取地址);->(类成员访问运算符)等。二元运算:
+;-;&(按位与);[](取下标);=(赋值);><>=<===+=&=等。其它:
()(函数调用);""(后缀标识符,C++11 起);new(内存分配);,(逗号运算符);<=>(三路比较(C++20 起)等。
实现
重载运算符分为两种情况,重载为成员函数或非成员函数。
当重载为成员函数时,因为隐含一个指向当前成员的 this 指针作为参数,此时函数的参数个数与运算操作数相比少一个。
而当重载为非成员函数时,函数的参数个数与运算操作数相同。
其基本格式为(假设需要被重载的运算符为 @):
xxxxxxxxxx71class Example {2 // 成员函数的例子3 返回类型 operator @ (除本身外的参数) { /* ... */ }4};5
6// 非成员函数的例子7返回类型 operator @ (所有参与运算的参数) { /* ... */ }
xxxxxxxxxx831234using namespace std;5const int P = 1e9 + 7;6
7struct Test {8 int k;9};10
11struct Edge {12 int a, b, w;13 bool operator < (const Edge& e2) const {//set内容无法修改需要加const14 if (a != e2.a) return a < e2.a;15 if (b != e2.b) return b < e2.b;16 return w < e2.w;17 }18 bool operator > (Edge& e2) {19 if (a != e2.a) return a > e2.a;20 if (b != e2.b) return b > e2.b;21 return w > e2.w;22 }23 long long operator * (Edge& e2) {24 return a * e2.a + b * e2.b + w * e2.w;25 }26 Edge operator += (Edge& e2){27 a += e2.a; b += e2.b; w += e2.w;28 return *this;//隐含了一个指向当前成员的this指针29 }30 long long operator * (Test& t) { //参数可以为其它类31 return a * t.k + b * t.k + w * t.k;32 }33};34
35Edge operator += (Edge& e, Test& t) {//非成员函数重载36 return /*Edge*/{e.a - t.k, e.b - t.k, e.w - t.k};37}38
39struct cmp{40 bool operator () (auto &e1,auto &e2)const{//cmp函数调用重载41 return e1.a > e2.b;//pq小根堆,堆顶为最小值42 return e1.a < e2.b;//pq大根堆,堆顶为最大值43 }44};45
46int main() {47 int n = 10; //cin >> n;48 multiset<Edge>se;49 priority_queue<Edge>pq;//优先队列默认重载<50 //priority_queue<Edge,vector<Edge>,cmp>pq; //cmp函数调用重载51 for (int i = 1; i <= n; i++) {52 int x = abs((P * i * i) % 11) + 1;53 Edge a = { x,x * i % 10,x * i * i % 10 };54 se.insert(a);55 pq.push(a);56 }57
58 se.insert({ 9,4,4 });59 se.erase({ 9,4,4 });60 cout << "multiset:" << endl;61 for (const Edge& x : se) { cout << x.a << ' ' << x.b << ' ' << x.w << endl; }62 cout << "\npq:" << endl;63 while (pq.size()) {64 Edge x = pq.top();65 cout << x.a << ' ' << x.b << ' ' << x.w << endl;66 pq.pop();67 }68 cout << "\ne1 = {1,2,3},e2 = {4,5,6}\n";69 cout << "e1 * e2 = ";70 Edge e1 = { 1,2,3 }, e2 = { 4,5,6 };71 cout << e1 * e2 << endl;72 73 cout << "e1 += e2 => ";74 e1 += e2;75 cout << e1.a << ' ' << e1.b << ' ' << e1.w << endl;76 Test t = { 10 };77 cout << "e1 * t = ";78 cout << e1 * t << endl;79 cout << "e1 = e1-t => ";80 e1 = e1 += t;81 cout << e1.a << ' ' << e1.b << ' ' << e1.w << endl;82 return 0;83}
Lambda表达式
xxxxxxxxxx11[capture list] (parameter list) -> return type { function body }
capture list是捕获列表,用于指定 Lambda表达式可以访问的外部变量,以及是按值还是按引用的方式访问。捕获列表可以为空,表示不访问任何外部变量,也可以使用默认捕获模式&或=来表示按引用或按值捕获所有外部变量,还可以混合使用具体的变量名和默认捕获模式来指定不同的捕获方式。
parameter list是参数列表,用于表示 Lambda表达式的参数,可以为空,表示没有参数,也可以和普通函数一样指定参数的类型和名称,还可以在 c++14 中使用auto关键字来实现泛型参数。
return type是返回值类型,用于指定 Lambda表达式的返回值类型,可以省略,表示由编译器根据函数体推导,也可以使用->符号显式指定,还可以在 c++14 中使用auto关键字来实现泛型返回值。
function body是函数体,用于表示 Lambda表达式的具体逻辑,可以是一条语句,也可以是多条语句,还可以在 c++14 中使用constexpr来实现编译期计算。
xxxxxxxxxx161int n,x;cin >> n >> x;2for(int i = 1;i <= n;i++){3 cin >> a[i];4}5
6auto check = [&](int mid){//使用例子7 return a[mid] >= x;8};9
10int l = 1,r = n;11while(l < r){12 int mid = l + r >> 1;13 if(check(mid)) r = mid;14 else l = mid + 1;15}16cout << l << endl;
递归写法
xxxxxxxxxx131//参数调用自己2auto fib = [&](auto &fib,int x){//递归求斐波那契3 if(x == 1 || x == 2) return 1;4 return fib(fib,x-1) + fib(fib,x-2);5};6cout << fib(fib,n) << endl;7
8auto dfs = [&](auto &dfs,int u,int fa)->void{//需要声明返回值类型9 for(auto x:e[u]){10 if(x == fa) continue;11 dfs(dfs,x,u);12 }13};
值捕获
值捕获的变量在 Lambda 表达式定义时就已经确定,不会随着外部变量的变化而变化。值捕获的变量默认不能在 Lambda 表达式中修改
xxxxxxxxxx41int x = 10;2auto f = [x](auto a) -> int{ return a*x; };3x = 1;4cout << f(3) << endl;//结果为30,不随x值的变化而改变
引用捕获
在捕获列表中使用
&加变量名,表示将该变量的引用传递到 Lambda 表达式中,作为一个数据成员。引用捕获的变量在 Lambda 表达式调用时才确定,会随着外部变量的变化而变化。引用捕获的变量可以在 Lambda 表达式中被修改
xxxxxxxxxx41int x = 10;2auto f = [&x](auto a) -> int{ x = 100;return a*x; };3cout << f(3) << endl;//结果为300,随x值的变化而改变4cout << x << endl;//x=100
隐式捕获
在捕获列表中使用
=或&,表示按值或按引用捕获 Lambda 表达式中使用的所有外部变量。这种方式可以简化捕获列表的书写,避免过长或遗漏。隐式捕获可以和显式捕获混合使用,但不能和同类型的显式捕获一起使用
xxxxxxxxxx61int x = 10;2int y = 20;3auto f = [=, &y] (int z) -> int { return x + y + z; }; // 隐式按值捕获 x,显式按引用捕获 y4x = 30;5y = 40;6cout << f(5) << endl; //输出55,不受外部 x 的影响,受外部 y 的影响
初始化捕获(init capture):C++14 引入的一种新的捕获方式,它允许在捕获列表中使用初始化表达式,从而在捕获列表中创建并初始化一个新的变量,而不是捕获一个已存在的变量。这种方式可以使用 auto 关键字来推导类型,也可以显式指定类型。这种方式可以用来捕获只移动的变量,或者捕获 this 指针的值。
xxxxxxxxxx41int x = 10;2auto f = [z = x + 5] (int y) -> int { return z + y; }; // 初始化捕获 z,相当于值捕获 x + 53x = 20; // 修改外部的 x4cout << f(5) << endl; // 输出 20,不受外部 x 的影响
使用例子
作为函数参数,自定义排序准则
xxxxxxxxxx16123using namespace std;4
5struct Edge{6 int a,b;7}e[100005];8
9int main(){10 int n;cin >> n;11 for_each(e+1,e+n+1,[](auto &x){cin >> x.a >> x.b;});//Edge可换用auto12 sort(e+1,e+n+1,[](auto &x,auto &y){if(x.a != y.a) return x.a > y.a;else return x.b > y.b;});13 for_each(e+1,e+n+1,[](auto &x){cout << x.a << ' ' << x.b << endl;});14 15 return 0;16}xxxxxxxxxx31int a,b,c; cin >> a >> b >> c;2auto add = [&](){w[idx] = c,e[idx] = b,ne[idx] = h[a],h[a] = idx++;};3add();
预编译头文件
通过预编译头文件<bits/stdc++.h>,加快编译万能头的时间,以windows下的gcc14.2.0编译器为例
xxxxxxxxxx61#进入头文件所在目录 例如 C:\mingw64\include\c++\14.2.0\x86_64-w64-mingw32\bits2#打开cmd输入以下指令 其中-x c++-header可以告诉编译器这是头文件,确保编译器正确处理文件3g++ stdc++.h -O2 -x c++-header -o stdc++.h.gch4#进入源cpp文件所在目录5#预编译头文件的编译器版本、编译选项(如-std=c++23、-O2等)必须与后续编译完全一致。6g++ 1.cpp -O2 -o -1.exe
gcc标准
__int128
__int128 仅仅是 GCC 编译器内的东西,不在 C++标准内,且仅 GCC4.6 以上64位版本支持
数据范围
__int128数据范围 :大于1.7e38
unsigned __int128数据范围 :
输入输出
由于不在 C++ 标准内,没有配套的
printfscanfcincout等输入输出,只能手写,请不要与ios::sync_with_stdio(false)同时使用,否则会造成输入输出混乱。
使用方法
与int、long long 等整型类似,支持各种运算,如
+-*/%、移位<<>>、&|!^、==><等多种操作,可以直接与int进行运算
初始化
可以使用整型范围内的值直接赋初值 如
__int128 a = 114514;可以使用浮点数赋值,但超出浮点数有效精度的部分赋值不准确,
__int128 a = 1e30结果为 1000000000000000019884624838656__int128 a = 1e40诺超出__int128数据范围,则会赋值为最大值,即如果要自定义赋值为一个很大的数,可以用
__int128 a = (__int128(1) << 126)的方式 也可以写一个函数,将字符串转为__int128xxxxxxxxxx91__int128 sto__int128(const string &s){2__int128 x = 0,f = 1;3for(int i = 0;i < s.size();i++){4if(s[i] == '-') { f = -1;}5else{x *= 10;x += s[i] - '0';}6}7return x*f;8}9__int128 n = sto__int128("-114514191981000");
xxxxxxxxxx411//使用例题 https://vjudge.net/contest/233969#problem/I23using namespace std;4
5inline __int128 read(){6 __int128 x = 0,f = 1;7 char ch = getchar();8 while(ch<'0'||ch>'9'){9 if(ch == '-') f=-1;10 ch = getchar();11 }12 while(ch>='0' && ch<='9'){13 x = x*10 + ch - '0';14 ch = getchar();15 }16 return x*f;17}18
19inline void write(__int128 x){20 if(x < 0){ putchar('-'),x *= -1;}21 if(x > 9) write(x/10);22 putchar(x % 10 + '0');23}24
25void soviet(){26 vector<__int128>a(4);27 __int128 ans = 0;28 for(int i = 0;i < 4;i++){29 a[i] = read();30 ans += a[i];31 }32 write(ans);33 cout << '\n';34}35
36int main() {37 int M_T = 1;//std::ios::sync_with_stdio(false),std::cin.tie(0);38 std::cin >> M_T;39 while(M_T--){ soviet(); }40 return 0;41}
__cplusplus
xxxxxxxxxx11cout << __cplusplus << endl;//可以直接输出预处理器宏参数| 不同参数对应的默认c++标准 | |
|---|---|
| 1 | C++ pre-C++98 |
| 199711L | C++98 |
| 201103L | C++11 |
| 201402L | C++14 |
| 201703L | C++17 |
| 202002L | C++20 |
| 202302L | C++23 |
编译时如果存在类似 -std=c++17 的选项,则显式指定了标准;如果未指定,则使用 g++ 默认的标准(取决于版本)。
不同版本的 g++ 默认使用不同的 C++ 标准:使用bash指令gcc --version可以查看g++版本
所有Gcc版本对C和C++的支持情况(超详细版本)-CSDN博客
文件读写
xxxxxxxxxx71//正式提交时请务必注释掉2int main() {3 freopen("in.txt","r",stdin);4// freopen("out.txt","w",stdout);5 int a,b; cin >> a >> b;6 cout << a + b << endl;7}xxxxxxxxxx51#windows cmd控制台读写方式2a.exe < in.txt > out.txt3
4#linux 控制台5./a.out < in.txt > out.txt
时间复杂度
| 数据范围 | ||
|---|---|---|
| 20 | 指数级别 | dfs+剪枝、状压dp |
| 100 | O(n^3) | floyd、dp、高斯消元、矩阵 |
| 1000 | O(n^2) ~ O(n^2logn) | dp、二分,朴素版Dijkstra、Bellman-Ford、二分图 |
| 1e4 | O( | 块状链表、分块、莫队、双指针 |
| 1e5 | nlogn | sort、线段树、树状数组、set/map、heap、拓扑排序、dijkstra、prim+heap、Kruskal、spfa、求凸包、求半平面交、二分、CDQ分治、整体二分、后缀数组、树链剖分、动态树 |
| 1e6 | O(n)~O(nlogn) | 单调队列、hash、双指针扫描、并查集,kmp、AC自动机,常数比较小的O(nlogn)的做法:sort、树状数组、heap、dijkstra、spfa |
| 1e7 | O(n) | 双指针扫描、kmp、AC自动机、线性筛素数 |
| 1e9 | O( | 判断质数、求欧拉函数 |
| 1e18 | O(logn) | 最大公约数、快速幂、数位DP |
| 1e1000 | O((logn)^2) | 高精度加减乘除 |
| 1e100000 | O(logk*loglogk) | 高精度加减、FFT/NTT |
xxxxxxxxxx81//(n+n/2+n/3+...+n/n) = nlogn 调和级数2//https://codeforces.com/contest/1996/problem/D3//ab+bc+ac <= n,a+b+c <= x,0 < a,b,c4for(int i = 1;i <= n;i++){//枚举所有a5 for(int j = 1;i*j <= n;j++){//枚举所有b6 //则0 < c <= min((b-ab)/(a+b),x-(a+b))7 }8}xxxxxxxxxx51//https://codeforces.com/contest/1991/problem/F21 1 2 3 5 8 ...3fib(26) 约 1.2e54fib(45) 约 1.1e95//诺a[i] <= 1e9,则长度>=45的序列一定能构成一个三角形xxxxxxxxxx31//阶乘28! 约 4.0e4312! 约 4.7e8xxxxxxxxxx51//质数个数21e8 约 5.7e6个31e7 约 6.6e5个41e6 约 7.8e4个51e5 约 9.5e3个xxxxxxxxxx11C[n][0] + C[n][1] + C[n][2] + ... + C[n][n] = 2^n
xxxxxxxxxx201//1~n内数位非递减的数,如123,223等2//https://atcoder.jp/contests/typical90/tasks/typical90_y3//n = 1e18,cnt = 5e64//n = 1e9,cnt = 5e456using namespace std;7long long n,cnt = 0;8void dfs(long long u){9 if(u > n) return;10 cnt++;11 for(int i = u%10;i <= 9;i++){12 dfs(u*10+i);13 }14}15
16int main(){17 cin >> n;18 for(int i = 1;i <= 9;i++){ dfs(i); }19 cout << cnt << endl;20}xxxxxxxxxx41//1~根号n枚举2for(int i = 1;i*i <= n;i++); //建议使用,大概3周期,需要注意数据范围3for(int i = 1;i <= sqrt(n);i++);//大概10周期(需要开启O2优化或循环外提前计算sqrt(n)+eps)4for(int i = 1;i <= n/i;i++); //大概40周期,如果时间紧不建议使用
握手定理
n个人握手,每人握手m次,握手总次数
给定无向图G=(V,E),有 ,图中所有结点的度数之和等于边数的两倍
xxxxxxxxxx21//https://codeforces.com/gym/104337/problem/H2//a1+a2+a3+... = n 不同的a最多约为√2n种
分块打表
Harmonic Number - LightOJ 1234 - Virtual Judge (vjudge.net)
求
, ,最多 组测试样例。
如果直接用前缀和预处理,数组需要开到10^8但空间不允许
我们可以每
xxxxxxxxxx351//O(N)预处理,O(len)查询,其中len为每块的大小23
4const int N = 100005;5double s[N];//统计的为块的前缀和信息6int len = 1000;7
8void init(int n){9 double ans = 0;10 for(int i = 1;i <= n*len;i++){11 ans += 1.0/i;12 if(i%len == 0){13 s[i/len] += ans;14 }15 }16}17
18void sol(){19 int n; std::cin >> n;20 int k = n/len;21 double ans = s[k];22 for(int i = k*len+1;i <= n;i++){23 ans += 1.0/i;24 }25 printf("%.10lf\n",ans);26}27
28int main(){29 init(N-1);30 int t; std::cin >> t;31 for(int i = 1;i <= t;i++){32 printf("Case %d: ",i);33 sol();34 }35}
随机数生成
xxxxxxxxxx131//mt199372345using namespace std;6
7auto SEED = chrono::steady_clock::now().time_since_epoch().count();//利用时间生成随机种子8mt19937_64 rng(SEED);9
10int main() {11 cout << rng() << endl;//每次调用rng()时,生成的数都不同12 //cout << mt19937_64(1 + SEED)() << endl; //生成种子为1下的固定随机数13}
xxxxxxxxxx121//xor_shift2//异或和移位每次都在上一次产生的值上产生新的值,因为在很大的值中舍弃了一些值,所以每次产生的值看起来就象是随机值,也就是多次shift下的伪随机数,常用于树哈希3//这种哈希十分好些且比mt19937_64(seed)()快,但随机性不强4const unsigned long long mask = mt19937_64(time(0))();//随机一个数作为固定种子5unsigned long long shift(unsigned long long x){//x在该种子下得到的随机数6 x ^= mask;7 x ^= x << 13;8 x ^= x >> 7;9 x ^= x << 17;10 x ^= mask;11 return x;12}
xxxxxxxxxx81//更强的哈希函数2const auto RAND = std::chrono::steady_clock::now().time_since_epoch().count();3static long long splitmix64(long long x) {4 x += 0x9e3779b97f4a7c15;5 x = (x ^ (x >> 30)) * 0xbf58476d1ce4e5b9;6 x = (x ^ (x >> 27)) * 0x94d049bb133111eb;7 return (x ^ (x >> 31)) + RAND;8}
.vimrc
xxxxxxxxxx131imap jk <Esc>2nmap <space> :3
4set cin5set sw=46set ts=47
8map <F5> :w <cr> :!clear & g++ % -O2 <cr>9map <F6> :!clear & ./a.out <cr>10map <C-F6> :!clear & ./a.out < in <cr>11"map <C-F6> :!clear & time ./a.out < in \| tee out <cr>12"set mouse=a13"syntax on
OI
