一、基础算法
1-排序
(1) 快速排序
第一步:确定分界点 x = q [ l ] , q [ (l+r)/2 ] , q[ r ]
第二步:调整顺序 小于 x 的放在前面, 大于x 的放在后面
- 1.开辟一个额外空间
- 2.不需要额外空间()
调整的暴力做法:
优化做法:
1.快排模板
快排思路总结
#include<iostream>
using namespace std;
const int N = 1e6 + 10;
int n;
int q[N];
void quick_sort(int q[], int l, int r) //l,r想以x为参照的大小排列的两端
{
if (l >= r) return; //区间里面只剩一个值或者没有值的时候返回
int i = l - 1, j = r + 1, x = q[r+l>>1] ; // i=l-1,j=r+1 为了适应下面的do 先i++、j++
while (i < j) //直到 i ,j 重合到一起,即扫描结束
{
do i ++ ; while (q[i] < x);
do j -- ; while (q[j] > x);
if (i < j) swap(q[i], q[j]);
}
/* 或者
int i = l, j = r, x = q[r+l>>1] ;
while (i < j) //直到 i ,j 重合到一起,即扫描结束
{
while (q[i] < x) i++;
while (q[j] > x) j--;
if (i < j){
swap(q[i], q[j]);
i++,j--;
}
}
*/
quick_sort(q, l, j), quick_sort(q, j + 1, r); //递归处理左右两端,此时的i>=j,用j更安全
}
int main()
{
scanf("%d",&n);
for(int i = 0; i<n ; i++)
scanf("%d",&q[i]);
quick_sort(q,0,n-1);
for(int i = 0 ; i<n ; i++)
printf("%d ",q[i]);
return 0;
}
(2) 快速选择算法
#include<iostream>
#include<algorithm>
using namespace std;
const int N= 100010;
int n ;
int k;
int q[N];
int quick_sort(int l,int r,int k)
{
if(l >= r ) return q[l];
int x = q[l],i=l-1,j=r+1;
while(i<j)
{
do i++; while(q[i] < x);
do j--; while(q[j] > x);
if (i < j) swap(q[i],q[j]);
}
int s1 = j-l+1;
if(k<=s1) //在前段
quick_sort(l,j,k);
else //在后段
quick_sort(j+1,r,k-s1);
}
int main()
{
scanf("%d%d",&n,&k);
for(int i=0;i<n;i++)
scanf("%d",&q[i]);
cout << quick_sort(0,n-1,k) <<endl;
return 0;
}
(3) sort函数(STL)
-
sort() 只对 array、vector、deque 这 3 个容器提供支持
-
sort() 函数位于
<algorithm>
头文件中#include<algorithm>
用法:
//对 [first, last) 区域内的元素做默认的**升序**排序
void sort (first, last);
//按照指定的 comp 排序规则,对 [first, last) 区域内的元素进行排序
bool cmp(first , last)
{
return fisrt < last ; //升序
}
void sort (first, last, cmp);
(4) 归并排序
模板
双路归并,合二为一
(左边排好序,右边排好序,然后连起来)
时间复杂度 :每层 n,共有 log n 层,所以 O(n log n)
#include<iostream>
using namespace std;
const int N = 1000010;
int n;
int q[N],tmp[N];
/*******************归并排序*******************/
void merge_sort(int q[],int l,int r)
{
if(l>=r) return ;
int mid = l + r >> 1; //>>1就是除2
/*****通过递归的方式把左右两边都排好顺序****/
merge_sort(q,l,mid),merge_sort(q,mid+1,r); /*****将排好顺序的两端在进行排序,排序时先将其放到临时tmp[N]中,最后将这块 q[] 排好顺序(变成下一次排顺序的一侧)****/
int k = 0,i = l, j = mid + 1;
/*******将大的放在前面*******/
while(i<= mid && j <=r )
if(q[i]<=q[j]) tmp[k++] = q[i++] ;
else tmp[k++] = q[j++] ;
/*****将某一端剩下的放好****/
while(i<=mid ) tmp[k++] = q[i++] ;
while(j<= r ) tmp[k++] = q[j++] ;
/***** 物归原主 ****/
for(i = l,j = 0;i<=r; i++,j++)
q[i] = tmp[j] ;
}
int main()
{
scanf("%d",&n);
for(int i = 0;i< n ;i++)
scanf("%d",&q[i]);
merge_sort (q,0,n-1);
for(int i = 0;i< n ;i++ )
printf("%d",q[i]);
}
// r + l + 1 就是按照[l,mid-1] / [mid,r] 分
void merge_sort(int l,int r)
{
if(l>=r) return ;
int mid = (r+l+1) >>1;
int i = l,j = mid ;
merge_sort( l, mid-1 ) , merge_sort( mid , r );
int k = 0;
while(i <= mid-1 && j<= r )
{
if( a[i] < a[j] ) temp[k++] = a[i++];
else temp[k++] = a[j++];
}
while(i<=mid-1) temp[k++] = a[i++];
while(j<=r) temp[k++] = a[j++];
for(int i=0,j=l; j<=r ; j++,i++)
a[j] = temp[i];
}
逆序对的数量
核心在于:
//最坏的情况下,n*n/2 = 5*10^9 会爆int(21亿),用long long
//分成三部分:左半边内部逆序数数量,右半边内部逆序数数量,左半边在右半边的逆序数
//***重点*** 左右两边排序对 左边在右边的逆序数没有影响,但是会好计算,所以在归并排序中计算逆序数
#include<iostream>
using namespace std;
typedef long long LL;
const int N = 100010;
int n,tmp[N],q[N];
LL merge_sort(int q[],int l,int r) //注意返回的时候是LL
{
int mid = l+r >> 1;
if(l>=r) return 0 ; // 划分的区间中只有一个数时,返回逆序数为0
/* 分治 */
LL res = merge_sort(q,l,mid) + merge_sort(q,mid+1,r ) ;
/* 归并,计算出第三部分的同时把这个区间排序 */
int k =0 ,i = l,j = mid +1 ;
while(i<=mid && j<=r)
if(q[i]<=q[j]) tmp[k++] = q[i++];
else
{
tmp[k++] = q[j++];
res += (mid-i+1); // 小的大于另一边的那个数,那么比这个大的那m-i+1个数更比这个数大
}
while(i<=mid) tmp[k++] = q[i++];
while(j<=r) tmp[k++] = q[j++];
/* 物归原主 */
for(int i=l,j=0;i<=r;i++,j++)
q[i] = tmp[j];
return res ;
}
int main()
{
int n;
cin >> n;
for(int i=0;i<n;i++)
cin >> q[i];
cout << merge_sort(q,0,n-1) << endl ;
return 0;
}
2-二分
-
有关细节(while 中的 l 能不能等于 r 等问题)
https://blog.csdn.net/u011917745/article/details/115616604
真牛逼!!!
(1)整数二分
有单调性一定可以二分,没有单调性也有可能二分
- 先写一个check()函数
bool check(int x) {/* ... */} // 检查x是否满足某种性质
- 在考虑判断完后怎样更新
l = mid ,补上加1
如果不加1,当l = r-1 时,mid = (l+r) / 2 = l ,如果check 成功 结果还是之 前的那个区间 [ l,r ] , 死循环 ;
但是加了1后,当l = r-1 时,mid = (l+r+1) / 2 = r,就会变为[ r,r ]就不会死循环了
- (l+r+1) / 2 的使用条件
- 区间划分成 l = mid , r = mid - 1
- 区间划分成 r = mid -1 , l = mid 时
模板二:
// 区间[l, r]被划分成[l, mid - 1]和[mid, r]时使用:
//查找右边界
int bsearch_2(int l, int r)
{
while (l < r)
{
int mid = l + r + 1 >> 1;
if (check(mid)) l = mid; //当 l(左) = mid时,mid就要+1 **左加** 因为当l=mid=r-1时,mid=(l+r)/2=l,死循环
else r = mid - 1;
}
return l;
}
模板一:
// 区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用:
//查找左边界
int bsearch_1(int l, int r)
{
while (l < r)
{
int mid = l + r >> 1; // r = mid ,不需要补上+1
if (check(mid)) r = mid; // check()判断mid是否满足性质
else l = mid + 1;
}
return l;
}
//当指定的 x 值不存在时,模板2是取到最后一个小于 x 的值
//而模板1是取到第一个 大于 x 的值
具体而言:
- 当 if(q[ mid ] >= x) r = mid ,是寻找等于 x 的最左端的值,当指定的 x 值不存在时,取到是寻找大于 x 的最小的值(也算是最左侧)
- 相当于 int* it = lower_bound(arr, arr + n, x); 查找有序容器中第一个不小于x的元素,返回迭代器
- 当 if(q[ mid ] <= x) l = mid ,是寻找等于 x 的最右端的值,当指定的 x 值不存在时,取到是寻找小于 x 的最大的值(也算是最右侧)
- 当 if(q[ mid ] > x) r = mid , 是寻找大于 x 的最小的值
- 当 if(q[ mid ] < x) l = mid , 是寻找小于 x 的最大的值
789…数的范围
#include<iostream>
using namespace std;
const int N=100010;
int n,m;
int q[N];
int main()
{
scanf("%d%d",&n,&m);
for(int i=0;i<n;i++) scanf("%d",&q[i]);
while(m--)
{
int x;
scanf("%d",&x);
int l=0,r = n-1;
/*第一步:随便写出mid = l +r >> 1;
*第二步:根据需要左端点还是右端点 写check(mid) 和 true 后的操作和else 的操作
*第三步:根据 true 后的操作若为r=mid,则之前的 mid=r+l>>1,若为l=mid,则mid=l+r+1>>1
*/
while(l<r) //最后的时候 l=r 所以输出谁都可以
{
int mid = l+r >> 1;
if (q[mid]>=x) r=mid; //右边都满足同一个性质(q[] >= x),mid 也有可能是 x
else l=mid+1 ;
}
if(q[l]!= x) cout << "-1 -1" <<endl; //其实若没有等于x的值,则会输出第一个大于x的值
else
{
cout << l << " ";
int l=0,r=n-1;
while(l<r) //右端点
{
int mid = l+r+1 >> 1;
if (q[mid] <= x) l=mid; //左边都满足同一个性质(q[] <= x) **l(左)加**
else r=mid-1 ;
}
cout << l << endl;
}
}
return 0;
}
CSP29-3-2 垦田计划
// 二分
/* 只要不是比较单纯找数字位置的二分 ,思路就是:
1.按照题目,check() 为 true的情况是趋向你目的 的情况
2.按照 r = mid 搭配 (l+r)/2 ; l = mid 搭配 (l+r+1)/2 的原则
*/
// 在资源数为m时,最少开垦天数为多少
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 100010;
typedef struct Node{
int t;
int c;
}node;
int n,m,k;
node tr[N];
bool cmp(node a,node b)
{
return a.t < b.t ;
}
bool check(int u) // 所有资源数能否撑到减到实际耗时为u天
{
int sum = 0;
for(int i=n;tr[i].t>u ; i--)
{
sum += (tr[i].t - u)*tr[i].c ;
}
if(sum <= m) // 趋向你想到达的情况(最少开垦天数可以更小,即sum <= m ),须带等号
return true;
else
return false;
}
int main()
{
cin >> n >> m >> k;
for(int i=1;i<=n;i++)
{
int t,c;
scanf("%d%d",&tr[i].t,&tr[i].c);
}
sort(tr+1,tr+n+1,cmp);
int l = k,r = tr[n].t;
while(l<r)
{
int mid = (l+r) / 2; // r = mid 配合的 不带加1
if(check(mid)) r=mid; // true趋向你想到达的情况(最少开垦天数可以更小,即sum <= m )
else l = mid+1;
}
cout << l << endl;
return 0;
}
1227 .分巧克力
// 二分
/*因为 每块大小 与 分的块数 有递减的函数关系,所以找到大于等于人数的最小块数就好了,
然后对应的块大小就是答案
*/
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 100010;
int n,k;
int h[N],w[N];
int Max ;
bool check(int u)
{
int sum = 0;
for(int i = 1;i<=n;i++)
{
sum += (h[i]/u)*(w[i]/u) ; //神奇的公式,求指定长度的块数,只用于正方形
}
if(sum >= k) // 趋向你想到达的情况(切的可以更大),须带等号
return true;
else
return false;
}
int main()
{
cin >> n >> k ;
for(int i = 1 ;i<= n ;i++)
{
scanf("%d%d",&h[i],&w[i]);
Max = max(Max,h[i]);
Max = max(Max,w[i]);
}
int l = 1,r = Max;
while(l<r)
{
int mid = (r+l+1) /2 ;
if(check(mid)) l= mid; // true趋向你想到达的情况(切的可以更大),须带等号
else r = mid-1;
}
cout << l;
return 0;
}
1460 .我在哪?
// 二分
#include<iostream>
#include<algorithm>
#include<cstring>
#include<unordered_set>
using namespace std;
const int N = 100010;
int n ;
string s;
bool check(int u)
{
unordered_set<string> hash;
for(int i = 0; i + u -1 <s.size() ; i++)
{
string s1 = s.substr(i,u) ;
if(hash.count(s1)) return false;
hash.insert(s1);
}
return true;
}
int main()
{
cin >> n;
cin >> s;
int l = 1,r = n; // 答案k最小为 1,最大为 n
while(l<r)
{
int mid = (l+r)>>1;
if(check(mid)) r = mid; // true趋向你想到达的情况(更小)
else l = mid+1;
}
cout << l ;
return 0;
}
(2)实数二分
浮点数二分
开平方
#include<iostream>
#include<algorithm>
using namespace std;
int main()
{
double x;
cin>>x;
double l = 0,r = max(x,1); //max(x,1) 因为 根号下0.01 == 0.1 不在这个范围内
while(r-l>1e-6)
{
double mid = (l+r)/2;
if(mid * mid >= x)
r = mid;
else
l = mid;
}
printf("%lf\n",l);
return 0;
}
开三次方
#include<iostream>
using namespace std;
int main()
{
double l=-10000,r = 10000;
double x;
cin >> x;
while(r-l > 1e-8)
{
double mid = (r+l)/2;
if(mid*mid*mid > x) r = mid;
else l = mid;
}
printf("%.6lf",r);
return 0;
}
3-高精度
(C++专用)
大整数的存储,用数组(往后的位数越大)
//无论A,B是正负,均可以变为 |A| - |B| ,或者是 |A| - |B| 的情况
/**************高精度加法****************/
#include<iostream>
#include<vector>
using namespace std;
const int N = 1e6+10;
/********** 加法 ***********/
vector<int> add(vector<int>&A,vector<int>&B)
{
vector<int> C;
int t =0; //进位
for(int i =0;i<A.size() || i<B.size() ; i++)
{
if(i < A.size()) t+=A[i]; //如果A[i]这一位上有的话,加上A[i]
if(i < B.size()) t+=B[i]; //如果B[i]这一位上有的话,加上B[i]
C.push_back( t % 10 ); //在这一位上相加的值
t /= 10; /**秒啊**/ 如果是两值相加大于10,即要进位,则/10后最后为1 , 如果相加小于10,则/10 后为 0
}
if(t) C.push_back(); //最后一位
return C;
}
/********** 减法 **********/
vector<int> add(vector<int>&A,vector<int>&B)
{
vector<int> C;
}
int main()
{
string a,b;
vector<int> A,B;
cin >> a>>b ; // a = "123456"
for(int i = a.size() - 1; i>= 0; i--)
A.push_back(a[i] - '0'); //A = [6,5,4,3,2,1]
for(int i = b.size() - 1; i>= 0; i--)
B.push_back(a[i] - '0');
vector<int> C = add(A,B);
for(int i = C.size() - 1;i>=0 ; i--)
printf("%d",C[i]);
return 0;
}
高精度加法/减法
#include<iostream>
#include<vector>
using namespace std;
const int N = 1e6 + 10;
//C = A +B
vector<int> add(vector<int> &A,vector<int> &B) //引用不会拷贝整个数组
{
vector<int> C ;
int t=0; // A[i] + B[i] + 进位 ,第一次的进位位0
for(int i = 0;i < A.size() || i < B.size() ; i++ ) //只要有一个就继续,C的第i位
{
if(i < a.size()) t + = A[i];
if(i < b.size()) t + = B[i];
C.push_back(t%10);
t /= 10; //下一位的进位
}
if(t) C.push_back(1); //若最后的一个进位为1就进上
return C;
}
int main()
{
string a,b; //123456
vector<int> A,B;
cin >> a >> b;
for(int i=a.size()-1 ; i>=0 ;i--) A.push_back (a[i] - '0'); //a = [6,5,4,3,2,1]
for(int i=b.size()-1 ; i>=0 ;i--) B.push_back (b[i] - '0');
auto C = add(A ,B) ;
for(int i = C.size() -1 ;i>=0 ;i--) printf("%d",C[i]);
return 0;
}
//高精度减法
#include<iostream>
#include<vector>
using namespace std;
const int N = 1e5+10;
bool cmp(vector<int> &A,vector<int> &B)
{
if(A.size() != B.size() )
return A.size() > B.size() ;
for(int i= A.size()-1 ;i>=0 ;i--)
if(A[i] != B[i])
return A[i]>B[i];
return true;
}
vector<int> sub(vector<int> &A,vector<int> &B)
{
vector<int> C;
int t = 0;
for(int i=0; i<A.size() ;i++)
{
t = A[i] - t;
if( i < B.size() )
t -= B[i] ;
C.push_back((t+10)%10); //一举两得,当A[i]>B[i]时(t+10)%10没有关系,当A[i]<B[i]时刚好需要加10
if(t< 0)
t = 1;
else
t = 0;
}
while (C.size() > 1 && C.back() == 0) C.pop_back(); //遇到前导0就弹出
return C;
}
int main()
{
vector<int> A,B;
string a,b;
cin >> a >> b;
for(int i=a.size()-1 ;i>=0 ;i--) A.push_back(a[i]-'0');
for(int i=b.size()-1 ;i>=0 ;i--) B.push_back(b[i]-'0');
if(cmp(A,B))
{
auto C = sub(A,B);
for(int i=C.size()-1 ;i>=0;i--) cout << C[i];
}
else
{
auto C = sub(B,A);
printf("-");
for(int i=C.size()-1 ;i>=0;i--) cout << C[i];
}
cout <<endl;
return 0;
}
//******** 不能这样做 **********//
vector<int> sub(vector<int> &A,vector<int> &B)
{
vector<int> C;
int i,t = 0;
for(i=0 ; i< B.size() ; i++) //必须是 i< A.size()
{
if(A[i] - t - B[i] < 0)
{
C.push_back(A[i]+10-t-B[i]);
t = 1;
}
else
{
C.push_back(A[i]-t-B[i]);
t = 0;
}
}
while(i<A.size()) //不是简单的把剩下的填上,若有100000997 - 999 前面的1000000~ 都要 -t操作
{
C.push_back(A[i]);
i++;
}
//while (C.size() > 1 && C.back() == 0) C.pop_back(); //遇到前导0就弹出
return C;
}
高精度乘法/除法
//高精度乘法
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
vector<int> mul(vector<int> &A,int &b)
{
vector<int> C;
int t = 0; //进位的临时变量
for(int i=0; i < A.size() ; i++)
{
t = b * A[i] +t;
C.push_back(t%10);
t /=10;
}
while(t) //访问到了最后一位的进位情况
{
C.push_back(t%10) ;
t/=10 ;
}
while (C.size()>1 && C.back()==0) C.pop_back(); // 遇到前导0就弹出(本身数为0就不能弹出)
return C;
}
int main()
{
string a;
int b;
vector<int> A;
cin >> a >> b;
for(int i=a.size()-1 ; i>=0 ;i--) //按数字的倒叙放进去
A.push_back(a[i]-'0');
auto C = mul(A,b);
for(int i=C.size()-1 ;i>=0 ;i--) //按数字的倒叙输出
cout << C[i];
return 0;
}
除法
//高精度除法
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
vector<int> div(vector<int> &A,int &b,int &r)
{
vector<int> C;
r = 0;
reverse( A.begin() , A.end() ); //反转一下,或者用i倒序输出
for(int i=0;i<A.size() ; i++)
{
r = r*10 + A[i] ;
C.push_back( r/b ) ;
r =r%b ;
}
reverse( C.begin() , C.end() );
while (C.size() > 1 && C.back() == 0) C.pop_back(); //遇到前导0就弹出
return C;
}
int main()
{
string a;
int b;
vector<int> A;
cin >> a >> b;
for(int i=a.size()-1 ; i>=0 ;i--)
A.push_back(a[i]-'0');
int r ; //余数
auto C = div(A,b,r);
for(int i=C.size()-1 ;i>=0 ;i--)
cout << C[i];
cout << endl;
cout << r;
return 0;
}
4-前缀和与差分
前缀和
前缀和:sum[ i ] = sum[ i-1 ] + a[ i ]
差分
(不用纠结与怎么构造,注意怎么更新就好)
一、构造思想:
-
a1,a2,a3…
-
构造 b1,b2,b3… 使得 ai = b1 + b2 + … + bi
-
假想的 b 数组就是 a 数组的差分
-
a 就是 b 的前缀和
1.对于一维而言
b1 = a1;
b2 = a2 - a1;
b3 = a3 - a2;
.
.
.
.
2.对于多维
二、应用:
- 多用于对多个(这里设n个) [l,r] 区间(设每个长m)内的所有数都加 一个数 ,由 O(n*m) -> O(2n + n)
三、初始化
int a[N];
int b[N];
for(int i = 0; i <n ;i++)
b[i] = 0;
//初始化
for(int i = 0; i <n ;i++)
{
一个个插入:
a[i] -> a[ i,i ]这个区间 +a[i] -> b[i]+a[i] , b[i+1]-a[i] ;
}
一维差分
二维差分
789.差分矩阵
#include<iostream>
using namespace std;
const int N = 1010;
int n,m,q;
int a[N][N],b[N][N],b1[N][N];
void insert(int x1,int y1,int x2,int y2,int c)
{
b[x1][y1] += c;
b[x2+1][y2+1] += c;
b[x1][y2+1] -= c;
b[x2+1][y1] -= c;
}
int main()
{
cin >> n >>m >>q;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
cin >> a[i][j];
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
insert(i,j,i,j,a[i][j]);
for(int i=1;i<=q;i++)
{
int x1,y1,x2,y2,c;
cin >>x1>>y1>>x2>>y2>>c;
insert(x1,y1,x2,y2,c);
}
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
a[i][j] = a[i-1][j] + a[i][j-1] -a[i-1][j-1] + b[i][j];
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
cout << a[i][j] <<" ";
cout << endl;
}
return 0;
}
5-位运算
lowbit
返回x的最后一位1
6-离散化
- 一种保留顺序的哈希方式
// 用二分找合适位置实现离散化 + unqiue去重 , 前缀和
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
typedef pair<int, int> PII;
const int N = 300010;
int n, m;
int a[N], s[N];
vector<int> alls;
vector<PII> add, query;
int find(int x)
{
int l = 0, r = alls.size() - 1;
while (l < r)
{
int mid = l + r >> 1;
if (alls[mid] >= x) r = mid;
else l = mid + 1;
}
return r + 1;
}
int main()
{
cin >> n >> m;
for (int i = 0; i < n; i ++ )
{
int x, c;
cin >> x >> c;
add.push_back({x, c});
alls.push_back(x);
}
for (int i = 0; i < m; i ++ )
{
int l, r;
cin >> l >> r;
query.push_back({l, r});
alls.push_back(l);
alls.push_back(r);
}
// 去重
sort(alls.begin(), alls.end());
alls.erase(unique(alls.begin(), alls.end()), alls.end());
// 处理插入
for (auto item : add)
{
int x = find(item.first);
a[x] += item.second;
}
// 预处理前缀和
for (int i = 1; i <= alls.size(); i ++ ) s[i] = s[i - 1] + a[i];
// 处理询问
for (auto item : query)
{
int l = find(item.first), r = find(item.second);
cout << s[r] - s[l - 1] << endl;
}
return 0;
}
7-区间合并
/*
1.按区间左端点排序
2.更新维护的区间
- 在区间内 end不用动
- 有一部分交集但还有多出的 end往后走
- 没有交集
用双指针 头start 尾end
*/
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
const int N = 100010 ;
typedef pair<int,int> PII; //相当于一个简易的结构体(里面只有两个数据)
int n;
vector<PII> segs; //合并后的那些小区间,其实segs是一个二维数组,n*[int l,int r]
void merge(vector<PII> &segs)
{
vector<PII> res; //放排好序的segs
sort(segs.begin(),segs.end()); //先将segs排序(pair一般会是左端排序)
int st = -2e9 ,ed = -2e9 ; //st,ed 都为-2e9
for(auto seg : segs)
{ //跟python的遍历很像
if(ed < seg.first) //有断点,上一个区间结束
{
if(st != -2e9) //防止[-2e9,-2e9] 压入 res 中
res.push_back({st,ed}); //把当前这段打包放进 res,再开启新的一段
st = seg.first,ed = seg.second ;//出现新的独立区间就要更新st,ed
}
else //维护的区间有交集,管他的取end max就完了
ed = max(ed,seg.second) ;
}
//搞定最后一段
if( st!= -2e9) //如果为空,不能摸鱼放进去
res.push_back({st,ed}); //维护的最后一段不会因为下一个独立的segs进入res,因为后面没有segs了
segs = res;
}
int main()
{
cin >> n;
for(int i =0;i< n; i++)
{
int l,r;
cin >> l >> r;
segs.push_back({l,r}); //输入还未进行区间合并操作的数组
}
merge(segs); //进行区间和并
cout<<segs.size();
return 0 ;
}
作者:ctrl
链接:https://www.acwing.com/activity/content/code/content/2587354/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
8-双指针
第一类
- 维护两个区间,例如归并排序,把两个有序序列合并
第二类
- 维护一个区间,例如快排
模板写法
for (int i = 0, j = 0; i < n; i ++ )
{
while (j < i && check(i, j)) j ++ ;
// 具体问题的逻辑
}
常见问题分类:
(1) 对于一个序列,用两个指针维护一段区间
(2) 对于两个序列,维护某种次序,比如归并排序中合并两个有序序列的操作
例子
799.最长连续不重复子序列
800.数组元素的目标和
// "//"后为无解时的写法
#include<iostream>
using namespace std;
const int N=1e5+10;
int a[N],b[N];
int main()
{
int n,m,x;
cin >> n >> m >> x;
for(int i=0;i<n;i++)
cin >> a[i];
for(int i=0;i<m;i++)
cin >> b[i];
for(int i=0,j=m-1 ; i <n;i++)
{
//while(j>=0 && a[i]+b[j] > x)
while(a[i]+b[j]>x)
j--;
//if(j<0) return -1; //数据保证有唯一解,不加也行
//if(j>=0 && a[i] + b[j]==x)
if(a[i]+b[j]==x)
{
cout << i<< " " <<j <<endl;
return 0;
}
}
}
2816. 判断子序列
/*
1 2
1
1 0
这种情况是因为a[N]默认都是0,碰巧
*/
#include<iostream>
#include<cstring>
using namespace std;
const int N = 100010;
int a[N],b[N];
int main()
{
int n,m,i,j;
cin >> n >> m;
memset(a,0x3f,sizeof a);
for(int i=0;i<n;i++)
cin >> a[i];
for(int i=0;i<m;i++)
cin >> b[i];
for(i=0,j=0; j<m;j++)
{
if(a[i]==b[j])
i++;
}
if(i==n)
cout << "Yes";
else
cout << "No";
return 0;
}
二、数据结构
1-链表与邻接表
用数组模拟列表
1.单链表
邻接表表示
int head ,e[N],ne[N],idx;
void init()
{
head = -1,idx = 0;
}
void add_head(int x) /***头插法向头结点插入 x *****/
{
e[idx] = x; /****先把x存入当前的e[idx]中***/
ne[idx]= head; /****把想要的头插的点的下位置设为 head ***/
head = idx ; /****/
idx ++; /******/
}
void add_k(int k,int x) /******在第k个节点出添加一个 x 结点 *****/
{
e[idx] = x;
ne[idx] = ne[k] ;
ne[k] = idx;
idx ++;
}
void remove(int k)
{
ne[k] = ne[ne[k]];
}
int main()
{
init();
}
邻接表存储图
int h[N];
int e[2*N],ne[2*N] ;
int idx ;
void add(int a,int b) //往父节点a中加入子节点b 头插
{
e[idx] = b;
ne[idx]=h[a] ; // 先托付
h[a] = idx; // 再接受
idx ++ ;
}
// 有向边的暴搜
void dfs_has_direction(int u)
{
for(int i=h[u] ;i!=-1;i = ne[i])
{
int j = e[i]; // e[i] 中存放的是根的子节点
dfs(j); //直接深搜到树的最下面,最后的那个子节点再调用时发现h[u]是-1(因为它没有子节点)
}
}
// 无向边的暴搜 , 记录父节点防止回溯
int main()
{
memset(h,-1,sizeof h);
}
邻接表存储树
dfs搜索
int h[N];
int e[2*N],ne[2*N] ;
int idx ;
void add(int a,int b) //往父节点a中加入子节点b 头插
{
e[idx] = b;
ne[idx]=h[a] ; // 先托付
h[a] = idx; // 再接受
idx ++ ;
}
// 无向边的暴搜
void dfs_no_direction(int u)
{
for(int i=h[u] ;i!=-1;i = ne[i])
{
int j = e[i]; // e[i] 中存放的是根的子节点
dfs(j); //直接深搜到树的最下面,最后的那个子节点再调用时发现h[u]是-1(因为它没有子节点)
}
}
//
int main()
{
memset(h,-1,sizeof h);
}
2.双链表
包含两个指针
l[N] //指向左节点
r[N] //指向右节点
优化某些问题
#include<iostream>
using namespace std;
const int N = 100010;
int m;
int e[N] , l[N] ,r[N] ,idx;
/************ 初始化**************/
void init()
{
r[0] = 1,l[1] = 0; //0号点的右边是1号点,1号点的左边是0号点
idx = 2; //0和1被占用,idx从2开始
}
/***********在k的右边插入一个点**********/
void add(int k ,int x)
{
/******* k 右边的那个链表是通过 r[k] 得到的,所以r[k]必须最后变**/
e[idx] = x;
r[idx] = r[k]; //已经把k点的下一个位置保存下来了
l[idx] = k;
l[r[k]]= idx; //如果先进行 r[k] = idx ,大不了用 l[ r[idx] ] = idx;
r[k] = idx;
idx ++;
}
/***********在k的左边插入一个点**********/
/* 道理一样,只不过是l[k]必须最后变 */
/* 也可以用 add(l[k],x) */
void add_left(int k,int x)
{
e[idx] = x;
l[idx] = l[k];
r[idx] = k;
r[ l[k] ] = idx;
l[k] = idx;
}
/*******删除第k个点******/
void remove(int k)
{
r[l[k]] = r[k];
l[r[k]] = l[k];
}
2-栈与队列
栈
:先进后出
#include<iostream>
using namespace std;
const int N=100010;
//****************** 栈*******************//
int stk[N] ; //
int tt = 0; //栈顶下标
//插入
stk[++tt] = x;
//弹出
tt--;
if(tt > 0) not empty
else empty
//栈顶
stk[tt];
队列
:先进先出
//***************** 队列 ****************//
int q[N] , hh = 0 , tt = -1;
//插入
q[++tt] = x;
//弹出
hh++;
//判断队列是否为空
if(hh<=tt) not empty
else empty
//取出对头,用数组的好处就是可以取出队尾元素
q[hh];
q[tt];
3302.表达式求值(栈)
用栈来实现递归树的效果
#include<iostream>
#include<cstring>
#include<algorithm>
#include<unordered_map>
#include<stack>
using namespace std;
stack<int> num;
stack<char> op;
unordered_map<char,int> cmp =
{
{'+',1},{'-',1},{'*',2},{'/',2},{'(',-1},{')',-1}
};
void eval()
{
int b = num.top(); num.pop();
int a = num.top(); num.pop();
char opr = op.top(); op.pop();
int x;
if(opr=='*') x =a*b;
else if(opr == '/') x = a/b;
else if(opr == '+') x = a+b;
else if(opr == '-') x = a-b;
num.push(x);
}
int main()
{
string s;
cin >> s;
for(int i=0;i<s.size() ;i++)
{
char c = s[i];
if(isdigit(c)) //检查所传的字符是否是十进制数字字符
{
int x=0;
while( isdigit(c) ) //还原为数
{
x = x*10 + c-'0';
c = s[++i];
}
num.push(x);
i--; //for中还有一个i++,这个用来抵消
}
else if(c=='(') op.push(c);
else if(c==')')
{
while(op.top()!='(')
{
eval();
}
op.pop(); // 弹出的是这个 “ ( ”
}
else
{
while (op.size() && cmp[op.top()] >= cmp[c]) eval();
//先判断栈是否为空,再进行取值;
//cmp[op.top()] >= cmp[c] 是为了防止类似3*(3*2+6)
op.push(c);
// if(op.size() && cmp[c]<=cmp[op.top()] ) //不能使用if,while能防止 0-15+1 = 0-(15+1) 情况的出现
// eval();
// op.push(c);
}
}
while(op.size()) eval(); //比如 最后有个 +1 ,没这步操作没法计算
cout << num.top() <<endl;
return 0;
}
830.单调栈
做一个单调的栈
/********** 此题的核心在于维持一个单调栈 *****************/
/* 每次输入x后,对前面比x大的数进行剔除(1),从而保证最后形成的栈是递增的,再插入(2) */
#include<iostream>
using namespace std;
const int N=100010;
int n;
int stk[N],tt = 0;
int main()
{
cin>>n;
for(int i=0;i<n;i++)
{
int x;
cin>>x; //还未赋值 stk[tt+1] = x;
/***** (1) *****/
while(tt && stk[tt] >=x) //tt &&作用,为了防止stk[tt = -1] 数组越界访问
tt--; //找到第一个小于x 的位置
/****** 输出第一个小于的数 *****/
if(tt)
cout<<stk[tt]<<" " ; //只要 tt 不是 0 ,(stk[0] 中不存值,当出现一个x小于所有数时,上面的while( tt &&)让它到了tt = 0)
else
cout<< -1 <<" "; // tt = 0 说明x是最小
/***** (2) ****/
stk[++tt] = x; //第一个单调栈的值是存放在 stk[1] 中
}
}
154.滑动窗口
/*
单调队列优化(类似单调栈),队列里的值不超过三个,
把暴力的 O(n*k) 优化为 O(n)
*/
#include<iostream>
using namespace std;
const int N= 10000100;
int n,k;
int a[N] , q[N] ; //a[i] 放原数组,q[N]存的是下标
int main()
{
scanf("%d%d",&n,&k);
int hh =0,tt = -1; //对头hh 队尾tt
for(int i =0;i<n; i++) scanf("%d",&a[i]);
/********** 最小值 **********/
for(int i =0;i<n; i++)
{
/**************顾头****************/
while(hh <= tt && i-k +1 > q[hh] ) // 判断队头是否已经滑出窗口
// 其实是当 i-q[hh]+1 > k 的时候,说明要往后移动一位,while也可以,不过没必要,因为每次i增大1,hh最多往前走一个
hh ++;
/**************顾尾***************/
while(hh<= tt && a[ q[tt] ] >= a[i]) //hh<=tt的作用是,出现一个很小的值时,别让tt往前跑过了头
// 看当下的a[i]是否小于a[ q[tt] ],如果小于,相当于那个 q[tt] 没作用了
tt--;
q[ ++ tt ] = i; // 经过上面while,到了a[i]最后一个大于的值的位置,把i 放到q[]中
// q[0] = 0
if( i>=k-1)
printf("%d ",a[q[hh]]);//第两个也就是 0,1凑不够三个所以不用输出,一个缺点就是最后 q[N] 不会完整正确保留下每个位置的区间最小值
//因为队列在不断的覆盖,这种题就是要每个都及时输出,****q[i] 不是第a[i]和前二值所成区间的最小值下标****
}
puts("");
/********* 最大值 **********/
hh=0,tt=-1;
for(int i =0;i<n; i++)
{
while(i-q[hh]+1>k)
hh++;
while(hh<=tt && a[ q[tt] ] <= a[i])
tt--;
q[++tt] = i;
if(i>=k-1)
printf("%d ",a[ q[hh] ]);
}
puts("");
return 0;
}
3-KMP
-
概念如下:KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。
———————————————— -
strstr(str1,str2) 函数用于判断字符串str2是否是str1的子串。如果是,则该函数返回 str1字符串从
str2第一次出现的位置开始到 str1结尾的字符串;否则,返回NULL。 -
#include<iostream> #include<string.h> using namespace std; int main() { char s1[] = "asdfghj123456"; char s2[] = "h"; cout << strstr(s1,s2); return 0; }
#include<iostream>
using namespace std;
const int N = 1e5+10;
const int M = 1e6+10;
int n,m;
char s[M],p[N];
int ne[N];
int main()
{
cin >> n >> p + 1 >> m >> s+1; //s和p都从1开始存储
//计算char s[1~j]的最长后缀(有相应的前缀) ne[j]
//原理就是通过模板串自己与自己进行匹配操作得出来的(代码和匹配操作几乎一样)
//所以先理解匹配操作,移植到计算ne[]就可以
for(int i=2,j=0 ; i <= n; i++ ) //详解见下,这里就是p[n] 与 p[n] 找匹配
{//ne[1]=0直接跳过,从i=2开始
while(j && p[i]!=p[j+1]) j = ne[j];
if( p[i]==p[j+1] ) j++;
ne[i] = j;
}
for(int i=1,j=0 ; i <= m; i++ )
{
//j表示降低要求(j=ne[j])后,还可以与s[]匹配的位置
//当j为0时,说明目前 p[]与s[i.....] 一个匹配的都没有,连提要求的资格都没有了,只能等有匹配的再说了
while(j && s[i]!=p[j+1]) j = ne[j];
//跳出上面while循环有两种情况
/* 一、s[i] == p[j+1] */
if( s[i]==p[j+1] ) j++; //则让p[]到下一个判断
if( j == n )
{
printf("%d ",i-n);
j = ne[j];
}
/* 二、 j==0,那就等有s[]、p[]相等再说呗 */
//此处 一、二一样都需要等待 i++
}
return 0;
}
4-Trie数
高效的存储和查找字符串集合的数据结构
- 传统的trie树是一种按字节进行检索的多叉树,具有比hash表更快的查找速度,主要应用于变长字符串的索引。
把所有结尾的结点标记出来
835.Trie字符串统计
/*
idx 是节点号(例如a,ab,abc是三种节点),idx唯一所以p才不会重复、 cnt[p]才能表示以当前字母结尾的单词数量
son[][26] 26表示26个字母
*/
#include<iostream>
using namespace std;
const int N = 1e5+10; //注意是所有所有所有字符串的总个数不超过100000
int son[N][26]; //第一维表示这个节点的位置 , 第二维表示是哪个字母 ,得数表示子节点的位置
int cnt[N]; //以此idx结尾的字符串的个数
int idx ; //当前用到哪个下标,下标是0的点,既是根节点,又是空节点
/* **********插入************ */
void insert(char str[])
{
int p =0; //根节点从0开始
for(int i=0;str[i];i++)
{
u = str[i] - 'a'; //把字母映射为0~25,放到son[][]的第二位标上
if(!son[p][u]) //如果还没有到这里的节点,一片净土,那么它将会成为一个新的节点
son[p][u] = ++idx ; /* son[p][u] 不为0 即保存了加入字符的操作 */
p = son[p][u]; /* 有两种作用:
1.当上面if语句不成立的时候,单词还有剩余的部分,则p=son[p][u]助力它进入下一个节点()
2.当上面if语句成立的时候,将产生一个新节点(一直到str[]遍历完成前,一直在产生新节点)
,那么p=son[p][u]将完成更新新节点号的任务 */
}
cnt[p] ++ ;
}
/* *询问一个字符串在集合中出现了多少次* */
/* ***顺着找到cnt[p],然后输出就好了** */
int query(char str[])
{
int p = 0;
for(int i = 0;str[i];i++)
{
u = str[i] - 'a';
if(!son[p][u])
return 0; //中途想开新的节点,说明s[][]l里面没有当前的str[]
p = son[p][u];
}
return cnt[p]; //输出
}
int main()
{
int n;
char op[2];
cin>>n;
while(n--)
{
char str[N];
scanf("%s%s", op, str);
if(op[0]=='I')
insert(str);
else
cout<< query(str)<<endl;
}
return 0;
}
143.最大异或对
c = a ^ b ,给定N个数,从中任取两个数的最大的 c
//暴力的第一重优化不了,但是第二重可以用牺牲空间的方式优化(trie树)
//从而使时间复杂度由 O( n*(n-1) ) 变为O( n*31)
#include<iostream>
using namespace std;
const int N = 1e5+10;
int n;
int a[N];
int son[N*31][2],idx; //节点至多为 N*31
// 在trie树中 二维数组son存的是节点的下标
// 第一维就是下标的值 第二维代表着儿子 在本题中 只有0或1 两个儿子
//idx 为节点值,每次遇到新节点 ++idx 显然好于直接开N*31个节点
void insert(int x)
{
int p=0; //每次都从零号节点出发
for(int i=31;i>=0;i--)
{
int u = x >> i & 1; //取出二进制数的第i位
if(!son[p][u] ) //如果不存在,创建这个新节点
son[p][u] = ++idx; //++idx 为新节点的下标
p = son[p][u]; //在p节点状态下,下一个为u的节点
}
}
int query(int x) //查询
{
int p =0,res =0;
for(int i=31 ;i>=0 ;i--)
{
int u = x >> i & 1;
if(son[p][!u]) //如果另一个方向存在的话,皆大欢喜,走到另一个方向上去
{
p = son[p][!u];
res = res*2 + !u; //在个位加上最后一位(此处为 !u)
}
else //如果不存在只能将就
{
p = son[p][u];
res = res*2 + u ; //在个位加上最后一位(此处为 u)
}
}
return res ;
}
int main()
{
cin >> n;
for(int i =0; i<n ; i++) cin >> a[i];
int res = 0;
for( int i=0 ; i<n; i++ )
{
insert(a[i]);
int t = query(a[i]) ; //trie树的存在,相当于省去了while(0~i的访问)
res = max(res , a[i]^t );
}
printf("%d\n",res);
return 0;
}
5-并查集
可以快速的处理这样的问题:
1.询问两个元素是否在通过一个集合中
(如果用暴力的方法,用数组存储 x 在哪个集合 )
belong[x] = a, belong[y] = b;
if(belong[x] == belong[y] )
//在一个集合中 还是O(1)
2.将两个集合合并,就费劲了
第一个集合有1000个元素,第二个有2000个
要么是把第一个集合1000的编号都改为b
要么是把第二个集合2000的编号都改为a
起码需要1000次计算,很耗时,因此需要用并查集
836. 合并集合
/*
* 1.刚开始每个集合只有一个数,让自己就是祖宗节点 p[i] = i,这个是判断到达祖宗节点的唯一方法
*/
#include<iostream>
using namespace std;
const int N= 1000010;
int p[N];
int n,m;
int find(int x) //返回祖宗节点,包含路径压缩
{
if(p[x] != x) //还没有到最终的祖宗节点
p[x] = find( p[x] ); //既然子节点没有找到祖宗节点,就让父节点找祖宗节点
return p[x]; //其实是一个递归,最终找到祖宗节点后就将祖宗节点一直返回
// 这其实也起到了路径压缩的作用,因为一路退回来,这条链上的所有点的p[x]的值(父节点)都等于祖宗节点
}
//或者清晰一点的话就是这样写
int find(int x) //寻找祖宗节点
{
int m ; // m 是祖宗节点
if( p[x] != x ) //如果找不到,递归的方式,找父节点的祖宗节点
{
m = find( p[x] ); // 找父节点的祖宗节点
p[x] = m ; // 找到祖宗节点后赋给子节点的父节点
}
else
m = x ;
return m;
}
/* 如果不进行状态压缩,会超时
int find(int x)
{
while( p[x] !=x ) x = p[x];
return p[x];
}*/
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) //刚开始每个集合只有一个数,让自己就是根节点 p[i] = i
p[i] = i;
while(m--)
{
char op[2]; //用scanf读字符时会读入空格啥的,但是读字符串的时候不会读入空格啥的
int a,b;
scanf("%s%d%d",op,&a,&b);
if(op[0]=='M')
p[find(a)] = find(b); //让b的祖宗节点称为a祖宗节点的父节点,相当于合并集合了
else
if(find(a)==find(b)) //如果祖宗一样,就说明是在同一个集合内
puts("Yes");
else
puts("No");
}
return 0;
}
837. 连通块中点的数量
/*
* 1.刚开始每个集合只有一个数,让自己就是祖宗节点 p[i] = i,这个是判断到达祖宗节点的唯一方法
*/
#include<iostream>
using namespace std;
const int N= 1000010;
int p[N];
int Size[N] ;
int n,m;
int find(int x)
{
if(p[x] != x)
p[x] = find( p[x] );
return p[x];
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) //刚开始每个集合只有一个数,让自己就是根节点 p[i] = i
{
p[i] = i;
Size[i] = 1;
}
while(m--)
{
char op[2]; //用scanf读字符时会读入空格啥的,但是读字符串的时候不会读入空格啥的
int a,b;
scanf("%s",op);
if(op[0]=='C') //a与b连一条边,其实就是把a与b分别所在的集合合并
{
scanf("%d%d",&a,&b);
if(find(a)==find(b)) continue; //如果是在同一个集合,不能 + size 否则会出现加倍情况
Size[find(b)] += Size[find(a)]; //集合中保证只有祖宗节点维护 size 就行
p[find(a)] = find(b); //放在后面
}
else if(op[1]=='1')
{
scanf("%d%d",&a,&b);
if(find(a)==find(b)) //如果祖宗一样,就说明是在同一个集合内
puts("Yes");
else
puts("No");
}
else
{
scanf("%d",&a);
printf("%d\n",Size[find(a)]);
}
}
return 0;
}
240. 食物链
去建立一棵树,树上的节点满足所有的真话,不满足所有的假话
//注意:这道题对真话的判断就是如果无法根据话本身或前面的话判定为假,就是真话
//去建立一棵树,树上的节点满足所有的真话,不满足所有的假话,叫此树为《正确树》
//每次判断是假话就是:这句话在这棵《正确树》中不对
#include<iostream>
using namespace std;
const int N = 50010;
int n,k;
int p[N];
int d[N]; //开始时是到父节点的距离(除了根节点其余都为1),更新后为到根节点的距离
int find(int x)
{
if(p[x]!=x)
{
int t = find(p[x]); //先不去更新p[x],为了求 d[p[x]]
d[x] = d[x] + d[p[x]];
p[x] = t;
}
return p[x];
}
int main()
{
cin >> n >> k;
for(int i=1;i<=n;i++)
{
p[i] = i;
d[i] = 0;
}
int res = 0; //假话数量-
while(k--)
{
int t,x,y;
cin >> t >> x >> y;
if(x>n || y >n) res ++;
else if(t==1) //x,y同类
{
int px = find(x),py = find(y); //先找到x,y的根节点
if(px==py && (d[x] - d[y])%3 !=0 ) //在同一棵树上且不是同类
res ++;
else if(px!=py) //不在一个集合的时候
{ //去建立一棵树,树上的节点满足所有的真话,不满足所有的假话
p[px] = py; //先把他们放到一个集合中
d[px] = d[y] -d[x];
}
}
else if(t==2)
{
int px = find(x),py = find(y);
if(px==py && (d[x] - d[y]- 1 )%3 !=0) //在同一棵树上且不是x吃y
res ++;
else if(px!=py) //不在一个集合的时候
{
//去建立一棵树,树上的节点满足所有的真话,不满足所有的假话
p[px] = py; //先把他们放到一个集合中
d[px] = d[y] +1 -d[x];
}
}
}
cout << res <<endl;
return 0;
}
- t==1 && px != py (此话是说x和y是同类,但是他们不在一个根节点下)
d[ px ] = d[y] - d[x]
- t==2 && px!=py (此话是说x吃y,但是目前还没有归于那颗 正确树 上)
带权并查集(处理相对关系)
- 一般的并查集主要记录节点之间的链接关系,而没有其他的具体的信息,仅仅代表某个节点与其父节点之间存在联系,它多用来判断图的连通性
- 而有的时候在这些边中添加一些额外的信息可以更好的处理需要解决的问题,在每条边中记录额外的信息的并查集就是带权并查集。
- 带权并查集的路径压缩 :记录到父结点的距离 -> 记录到 祖宗结点的距离
int find(int x) // value[i] 记录的是i到祖宗结点的距离
{
if (x != parent[x])
{
int t = parent[x]; //先记录下原本父节点的编号,因为在路径压缩后父节点就变为根节点了
parent[x] = find(parent[x]);
value[x] += value[t]; // 将当前节点的权值加上原本父节点的权值
}
return parent[x];
}
6-堆
堆的含义表明,完全二叉树中所有非终端结点的值均不大于(或不小于)其左、右孩子结点的值。由此,若序列{k1,k2,…,kn}是堆,则堆顶元素(或完全二叉树的根)必为序列中n个元素的最小值(或最大值)。
- (完全二叉树:除了最后一层节点之外,所有的节点都是满的,且最后一层节点是从左到右排布的)
1.插入一个数
2.求集合当中的最小值
3.删除最小值
4.删除任意一个元素
5.修改任意一个元素
小根堆:
存储【用一个一维数组存储】
x ( 某个根节点下标) 左儿子为 2x (下标) , 右儿子为 2x + 1
/*
* 先往根节点放那个大的数,然后下沉
*/
down(x)
{
}
/*
* 先往最下面的叶子节点加一个数,上升(与父节点比)
*/
up(x)
{
}
1.插入一个数
heap[ ++size ] = x;
up(size);
2.求集合当中的最小值
heap[1];
3.删除最小值
heap[1] = heap[size]; //用最后一个元素覆盖堆顶元素
size-- ; //最后的元素上去了,所以下面那个删去就好了
down(1);
4.删除任意一个元素
heap[k] = heap[size];
size--;
down(k);up(k); //只会执行一个
//和上面一个道理
5.修改任意一个元素
heap[k] = x;
down(k);up(k);
/*
* 堆排序
*/
#include<iostream>
using namespace std;
const int N=100010;
int n,m;
int h[N],Size; //注意size 是会和iostream库变量名字重合的
void down(int u)
{
int t = u ;
if(u*2 <=Size && h[2*u] < h[t]) t = u*2; //左儿子与父节点比大小
if(u*2+1 <=Size && h[2*u+1] <h[t]) t = u*2+1; //右儿子与父节点比大小
if(u!=t)
{
swap(h[u],h[t]); // t 保存了小的那个的位置
down(t); //递归,将一直递归到最后一层结束
}
}
void up(int u )
{
while(u/2 && h[ u/2 ]>h[u] ) //迭代,只要有父节点 u/2 且父节点的值大于这个值
{
swap(h[ u/2 ] ,h[u]);
u/= 2 ; //换完之后再找父节点
}
}
int main()
{
scanf("%d%d",&n,&m);
for(int i =1;i<=n;i++)
{
scanf("%d",&h[i]);
}
Size = n;
for(int i= n/2;i;i--) //最后一层下面没有层了,所以不需要down O(n)=== (1*n/2 + 2*n/4 + 3*n/8 .......)
down(i);
while(m--) //输出前m个小的数
{
printf("%d ",h[1]); //每次从堆顶拿出最小的值
h[1] = h[Size]; //然后把堆顶删掉
Size--;
down(1);
}
return 0;
}
6.额外附加条件
当删除、修改等操作按照第几个插入堆的顺序来找时,需要维护k(第k个插入) i (第k个插入的值在数组中的下标) ,就相对与来说麻烦些,不过就是那三个 = ,使得值交换时万无一失
/*
* idx 记录第几次插入,每个idx对应唯一一个数组值 ; size目前堆总共包含的值的个数
* 二叉树不同于Trie树,其父节点很容易找到,因为子节点与父节点位置有两倍关系
*
*/
#include<iostream>
#include<string.h>
#include<algorithm>
using namespace std;
const int N= 100010;
int h[N]; /* 堆 */
int ph[N]; /* 存储第k个插入的数在堆中的下标 */
int hp[N]; /* 存储堆中指定下标的值是第k个插入的数,与ph[N]互逆 */
int k; /* 存储当前堆中用到了哪个下标,用于插入与删除等操作 */
int Size;
int idx;
/* *******交换(因为题目中要求从第几个插入位置找值了,所以要维护k与i的关系)******* */
void heap_swap(int a,int b) // a,b是在数组中的下标
{
swap(ph[hp[a]],ph[hp[b]]); /*
1.找下标为a的h[a] 是第几个插入的元素 = hp[a]
2.因为节点的移动,所以第k= hp[a] 个插入的点在数组中的位置(ph[k])也随之移动
*/
swap(hp[a], hp[b]); //交换 idx,现在数组下标为a的hp[a] 变化了
swap(h[a],h[b]); //交换值
}
void down(int u) //数组下标第u个
{
int t = u;
if(u*2<= Size&& h[u*2] <h[t]) t = u*2;
if(u*2+1 <= Size && h[u*2+1] <h[t]) t = u*2+1;
if(u != t)
{
heap_swap(t,u);
down(t);
}
}
void up(int u)
{
while(u/2 && h[u/2] > h[u])
{
heap_swap(u/2,u);
u/=2;
}
}
int main()
{
int n;
scanf("%d",&n);
while(n--)
{
char op[10];
int k,x;
scanf("%s",op);
/* 插入一个数 */
if(!strcmp(op,"I"))
{
scanf("%d",&x);
idx++,Size++; //记录第idx个插入的数
ph[idx] = Size,hp[Size] = idx; //刚插入的时候就是Size值对应第几个插入
h[Size] = x;
up(Size); //尾插,再up一遍就好
}
else if(!strcmp(op,"PM")) printf("%d\n",h[1]); //输出最小值
else if(!strcmp(op,"DM"))
{
heap_swap(1,Size);
Size--; /* ***** 小心 **** */
down(1); /* 别把这两行弄反了 */
}
else if(!strcmp(op,"D"))
{
scanf("%d",&k);
k = ph[k]; // k由第k个插入的元素,变为其在数组中的位置,从而适应下面的heap_swap函数
heap_swap(k,Size); //删除第k个的值,就相当于与最后一个Size交换
Size--;
down(k),up(k);
}
else
{
int x;
scanf("%d%d",&k,&x);
k = ph[k] ;
h[k] = x;
down(k),up(k);
}
}
}
7.堆的STL
make_heap(first, last, comp); //建立一个空堆;向堆中插入一个新元素;
top_heap(first, last, comp); //获取当前堆顶元素的值;
sort_heap(first, last, comp); //对当前堆进行排序;
7-哈希表
定义
散列表(Hash table,也叫哈希表),是根据键(Key)而直接访问在内存存储位置的数据结构。也就是说,它通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。这个映射函数称做散列函数,存放记录的数组称做散列表。
1-两种防止冲突的方法
- 哈希表 (处理冲突)
- 拉链法:
- 拉链法:
/* 拉链法: 哈希表的每个h[k]相当于是第K个头节点,因为下面总是t
*/
//开一个槽,每一个槽上开一个链
int h[N]; //h[k] 存着的是,下面挂着的单链表的的头节点(就是单链表首节点在e[]中的位置)
int e[N]; //保存拉链上挂着的链表的值
int ne[N]; //保存链表的下一个的位置
int idx; //当前用到了哪个位置
void insert(int x)
{
int k = (x % N + N) % N //在C++中,正数的模是正数,负数的模是负数(-7 % -3 = -1),加 N 再模 N 的作用就是将其变为正的,只保证正的就行
//k 是哈希值
e[idx] = x,ne[idx] = h[k],h[k] = idx,idx++; //在哈希值为k处的单链表进行头插
}
bool find(int x)
{
int k = (x%N + N) % N; //找到x的哈希值 k
for(int i =h[k]; i!=-1 ;i = ne[i]) //在哈希值为k的单链表中找那个数
if( e[i]==x )
return true;
return false;
}
int main()
{
int n ;
scanf("%d",&n);
memset(h, -1 , sizeof(h)); //将初值都赋 -1
while(n--)
{
char op[2];
int x;
scanf("%s%d",op,&x);
if(op[0] == 'I')
insert(x);
else
{
if(find(x))
puts("Yes");
else
puts("No");
}
}
return 0;
}
/* 开放寻址法: (只开了个一维数组(要为题目范围的2~3倍),不用去开链表) */
/* 冲突解决方案:
从前往后,如果前面坑位有人就往下面的坑位找,直到找到第一个空的坑位放进去
原理:
3放在h[3],6放在位置h[4],9放在h[5],现在4只能放在h[6],其实4在最最好的情况下不过也是放在h[4]中,所以找4的时候就要从第四个位置开始找
*/
const int N= 200003,null = 0x3f3f3f3f; //N为2~3倍的模的值,,null 的作用就是 说是空的
int h[N] ;
int find(int x) /* 核心 */
{
int k = ( x%N + N) % N ; //先把x映射到K上
while(h[k] !=null && h[k] !=x )
//寻找x可以存放的位置:
// 1.当x之前已经在散列表中存下,则停在这个位置
// 2.当没有时,放到一个还没被使用过的地方
{
k++;
if(k == N) //看完最后一个,再反过来看第一个,其实前面第一个与后面的道理相同,可能会在取模位置的后面
k=0;
}
return k; //返回
}
int main()
{
int n ;
scanf("%d",&n);
memset(h, 0x3f, sizeof(h)); //初值都为 0x3f (可以近似为无穷大的)
while(n--)
{
char op[2];
int x;
scanf("%s%d",op,&x);
int k = find(x); //当插入x时,数组中没有x必然会找到一个新位置 ; 当寻找x时刚好使用find();
if(op[0] == 'I')
h[k] = x;
else //查询是水到渠成的时,根据返回x在散列表中的位置,若不是新位置(h[k] = NULL),就代表在散列表中存在
{
if(h[find(x)] == null)
puts("No");
else
puts("Yes");
}
}
return 0;
}
2-哈希字符串
- 长度100000的字符串 都不在话下,还要什么KMP
(假定不会出现冲突)
1 ----- 把字符串哈希为一个数字-------
1.p进制(p = 131 或者 p=13331)
2.Q
2 --------用一个函数求出所有部分的哈希值--------
-
比较不好想的地方是 : 求一个字串的哈希值的公式
h[r] - h[l-1]*p[r-l+1]
其实从列举的公式就可以看出 h[ l-1 ] 乘 p[ r-l+1 ] 是为了正确的删掉前 l 位 ,因为随着 字串长度增加,会不断的乘p
hash[1]=s1 hash[2]=s1∗p+s2 hash[3]=s1∗p2+s2∗p+s3 hash[4]=s1∗p3+s2∗p2+s3∗p+s4 hash[5]=s1∗p4+s2∗p3+s3∗p2+s4∗p+s5
// 快速判断字符串是否相等,甚至比KMP算法还牛逼
时间复杂度由O(n)变为O(1)
/*
给定一个长度为 n 的字符串,再给定 m 个询问,每个询问包含四个整数 l1,r1,l2,r2,请你判断 [l1,r1] 和 [l2,r2] 这两个区间所包含的字符串子串是否完全相同。
字符串中只包含大小写英文字母和数字。
*/
#include<iostream>
using namespace std;
typedef unsigned long long ULL;
const int N=100010 ,P = 131; //131进制
int n,m;
char str[N];
ULL h[N],p[N];
ULL get(int l,int r)
{
return h[r] - h[l-1]*p[r-l+1];
}
int main()
{
scanf("%d%d%s",&n,&m,str +1); // str数组从1开始存
p[0] = 1; // 注意初始化 p[0] = 1
for(int i =1;i<=n;i++)
{
p[i] = p[i-1]*P; // 每次乘p并记录
h[i] = h[i-1]*P + str[i]; // 前缀h[i]
}
while(m--)
{
int l1,r1,l2,r2;
scanf("%d%d%d%d",&l1,&r1,&l2,&r2);
if(get(l1,r1)==get(l2,r2))
puts("Yes");
else
puts("No");
}
return 0;
}
8-STL
vector
变长数组,倍增思想(每次数组长度不够的时候就变为2倍)
#include<vector>
int main()
{
//赋初值
int s[] = {1,2,3,4,5};
vector<int> a(s,s+5);
//遍历方式:
一、
for(int i=0; i<a.size(); i++)
cout<< a[i] <<' ';
二、
for(auto i=a.begin() ;i!=a.end(); i++ ) //auto 自动识别,其实i是一个迭代器
cout<< *i <<endl;
三、
for(auto x:a )
cout<< x ;
四、
for (vector<int>::iterator iter = a.begin(); iter != a.end(); iter++)
cout << (*iter).x << endl;
}
-
- a.pop_back() // 删除向量中最后一个元素 - a.size() // 大小 - a.empty() //是否为空 - a.clear() // 清空向量中所有元素 - a.push_back(num) // 添加元素num - a.begin() //第一个元素 - a.end() //最后一个元素的下一个位置 - a.insert(pos,first,last) // pos 要插入的位置,first ~ end 去插入的元素集合 // 例tree[p[i]].insert(tree[p[i]].end(),tree[i].begin(),tree[i].end()); - a.reverse(A.begin() , A.end()) // 翻转 - sort(a.begin(), a.end()); // 从小到大排序 - sort(vec.rbegin(), vec.rend()); //从大到小排序 - //去重 -结合sort和unique函数
unique()函数将相邻且重复的元素放到vector的尾部 然后返回指向第一个重复元素的迭代器再用erase函数擦除从这个元素到最后元素的所有的元素。
所以可以先进行排序,这样重复元素就会堆一起了,调用unique()函数,再调用erase函数删除重复
a.erase(unique(a.begin(), a.end()), a.end());
黑科技:
支持比较运算
### pair
```c
int main()
{
pair<int,string>p;
p = make_pair(10,"lks"); //pair的初始化
p = {20,"abc"}; //初始化(有一个对象,既有姓名也有排名),要是有三个属性,用pair< int,pair<int,int> >p;
p.first
p.second
支持比较运算,以first为第一关键字,second为第二关键字(字典序),
string
a,size();
a.empty();
a.clear();
int main()
{
string a = "lks" ;
a += "def" ;
a += 'c';
cout<< a.substr(x,y) <<endl; //输出第 x+1 ~ y+1 个字符
//或者用printf:
printf("%s\n", a.c_str() );
// printf没法直接输出 string 类型,所以用这个来
}
字符串,substr( ) , c_str( )
queue
-
queue 遵循 FIFO(first in first out) 原则,即先进的元素先出。
-
常见的单端队列就是只能在队尾(rear) 入队,只能在队首(front)出队的队列
queue入队,如例:q.push(x); 将x 接到队列的末端。
queue出队,如例:q.pop(); 弹出队列的第一个元素,注意,并不会返回被弹出元素的值。
访问queue队首元素,如例:q.front(),即最早被压入队列的元素。
访问queue队尾元素,如例:q.back(),即最后被压入队列的元素。
判断queue队列空,如例:q.empty(),当队列空时,返回true。
访问队列中的元素个数,如例:q.size()
没有clear()函数
queue<int> q;
q = queue<int>();
priority_queue
优先队列具有队列的所有特性,包括队列的基本操作,只是在这基础上添加了内部的一个排序,它本质是一个堆实现的。
定义:priority_queue<Type, Container, Functional>
Type 就是数据类型,Container 就是容器类型(Container必须是用数组实现的容器,比如vector,deque等等,但不能用 list。STL里面默认用的是vector),Functional 就是比较的方式。
当需要用自定义的数据类型时才需要传入这三个参数,使用基本数据类型时,只需要传入数据类型,
默认是大顶堆。
一般是:
#include<queue>
//升序队列,小顶堆
priority_queue <int,vector<int>,greater<int> > q;
//降序队列,大顶堆
priority_queue <int> q; // 默认就是大根堆
priority_queue <int,vector<int>,less<int> >q;
//greater和less是std实现的两个仿函数(就是使一个类的使用看上去像一个函数。其实现就是类中实现一个operator(),这个类就有了类似函数的行为,就是一个仿函数类了)
自定义排序
- 无论是普通 int 还是 结构体排序都可以用一种模板
struct cmp1{
bool operator()(int x,int y) // 改数据结构即可
{
return x > y; //小的优先级高 ,从小到大排
}
};
- 为同一个结构体设置两种不同的 自定义排序方式
struct Person{
string name;
int age;
};
struct sortByAge {
bool operator()(const Person& p1, const Person& p2) const {
//也可以直接 bool operator()(Person p1, Person p2)
return p1.age < p2.age;
}
};
struct sortByName {
bool operator()(const Person& p1, const Person& p2) const {
return p1.name < p2.name;
}
};
priority_queue<Person, vector<Person>, sortByAge> pq1; // 按照年龄从小到大排序
priority_queue<Person, vector<Person>, sortByName> pq2; // 按照名字字典序从小到大排序
#include<iostream>
#include<vector>
#include<queue>
using namespace std;
int tmp[100];
struct cmp1{
bool operator()(int x,int y)
{
return x>y;//小的优先级高 ,从小到大排
}
};
struct cmp2{
bool operator()(const int x,const int y)
{
return tmp[x]>tmp[y];
}
};
struct cmp3_node{
bool operator()(node a,node b)
{
return a.x > b.x ;
}
};
struct node{
int x,y;
/*friend bool operator<(node a,node b) // 只能用小于号
{
return a.x>b.x;//按x从小到大排 , 大于号 则 从小到大排 小于号则从大到小排
}*/
};
priority_queue<int>q1;
priority_queue<int,vector<int>,cmp1>q2;
priority_queue<int,vector<int>,cmp2>q3;
//priority_queue<node>q4;
priority_queue<node,vector<node>,cmp3_node > q4;
int main()
{
int i,j,k,m,n;
int x,y;
node a;
while(cin>>n)
{
for(int i=0;i<n;i++)
{
cin>>a.y>>a.x;
q4.push(a);
}
cout<<endl;
while(!q4.empty())
{
cout<<q4.top().y<<" "<<q4.top().x<<" "<<endl;
q4.pop();
}
cout<<endl;
int t;
for(i=0;i<n;i++)
{
cin>>t;
q2.push(t);
}
while(!q2.empty())
{
cout<<q2.top()<<endl;
q2.pop();
}
cout<<endl;
}
return 0;
}
举例
/*岛屿问题: 海平面下降,露出的岛屿,只有高于某个数的数会露出来 */
#include<iostream>
#include<algorithm>
#include<cstring>
#include<vector>
#include<queue>
using namespace std;
typedef pair<int,int> PII;
//优先队列按照pair的第二关键字从小到大排序
struct cmp{
bool operator()(PII a, PII b){
return a.second > b.second;
}
};
int main()
{
PII p0(1,20),p1(6,21),p3(2,6);
priority_queue<PII,vector<PII>,cmp > heap;
heap.push(p0);
heap.push(p1);
heap.push(p3);
while(heap.size())
{
cout << heap.top().first <<endl ;
heap.pop();
}
return 0;
}
stack
栈:
仅在表尾进行插入或删除操作的线性表。 栈是一种数据结构,它按照后进先出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据。
stack<int> a;
//插入元素x
int x;
cin >> x;
a.push(x);
//取出栈顶元素
int tmp = a.top();
//弹出栈顶元素
a.pop() ;
//栈的长度
a.size() ;
set
- TL中的set是一种关联容器,它用于存储一组非重复的元素,并且按照一定的顺序进行排序。与普通数组或向量不同,set中的元素是唯一的,它们被自动排序
- 查找、插入和删除操作的时间复杂度均为 O(log N)
- 不能使用下标访问元素 ( 在set中插入元素时,set会自动调整元素的位置 )
set<typename> a;
a.begin();
a.end(); // for(auto it = a.begin() ; it != a.end() ; it++ )
a.size(); // for(int i = 0 ; i< a.size() ; i++ )
a.empty();
a.clear();
a.insert(x); // 将 x 插入set容器中,并实现自动增序 和 去重
a.find(value); // 在set中查找 value 并返回其迭代器
a.erase(it); //it为所需要删除元素的迭代器 先别用
a.erase(value); //删除a中等于 value 的那个元素
lower_bound(x); //返回大于等于x 的最小的数的迭代器
upper_bound(x); //返回大于x的最小的数的迭代器
在数组a[5]={0,2,3,4,5}中,对于需要查找的x=2
lower_bound(a,a+n,x)-a 返回值为1,表示数组中第一个大于等于x的下标值
upper_bound(a,a+n,x)-a 返回值为2,表示数组中第一个大于x的下标值
multiset
https://blog.csdn.net/sodacoco/article/details/84798621
lower_bound(x)
-
lower_bound 函数的前提是区间必须是已经有序的。
-
返回大于等于x 的最小的数的迭代器
upper_bound(x)
-
upper_bound 函数的前提也是区间必须是已经有序的
-
返回大于x的最小的数的迭代器
unordered_set 哈希表
unordered_set
和 unordered_map
都是 C++ STL 中的哈希表容器,二者最大的区别在于:
unordered_set
存储键值(即“键=值”,且键唯一),而unordered_map
存储键值对(即“键=值”)。在unordered_set
中,键也是值,但是在unordered_map
中键和值是不同的内容。unordered_set
只存储键,没有相关联的值,因此其元素类型只有键类型;而unordered_map
中存储了键和与之关联的值,因此其元素类型包括键和值两个类型。
此外,它们的基本操作和使用方法基本相同。对于 unordered_set
和 unordered_map
中的各种操作函数,都有类似的函数名称和参数列表,并且时间复杂度也相同。
unordered_set<string> hash;
string s;
hash.count(s); // 哈希表中有了s就返回1
hash.insert(s); // 插入
map
- 映射
- 改进了数组只能 数字下标 -> 类型的限制 , 可以将 任何基本类型 -> 任何基本类型
###定义:###
map< 键key的类型 ,键值value类型 > mp;
// 还可以是复杂的STL容器 映射 到一个字符:map< set<int> , string > mp;
###访问:###
---通过键访问:
map<char,int> mp;
mp['s'] ; // 通过键访问 ,须保证键是唯一的
---通过迭代器访问:
map<char, int>::iterator it;
for(it = mp.begin() ; it != mp.end() ;it++ )
{
printf("%c %d\n" , it->first , it->second ); // 键通过 first 键值通过 second
}
for(auto it : mp){}
- map会自动实现以键从大到小的顺序自动排序
mp.insert(make_pair(2,"b")); // 插入 key value
value = mp.find(key); // find(key) == mp[key]
mp.erase(key); // 删除 键key 映射的键值对
mp.erase(first , second);
multimap
- 一个键对应多个值
unordered_map 哈希表(带映射)
基本操作
- 没有自动排序的map
哈希表
哈希表是根据关键码值(key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度,这个映射函数叫做散列函数。
- 对于 unordered_map< int ,int > a; 可以使用数组来替代
unordered_map<int,int> a;
int b;
a.count( b ) //使用count,返回的是被查找元素的个数。如果有,返回1;否则,返回0。
a[b] //结果为 key=b 的键值,若没有存这个,则返回 0
插入方法一:
a["key"] = value; // 插入相同键的键值对时,后一组的键值对会覆盖前一组键值对。
插入方法二:
a.insert({key, value}); //使用方法二插入相同键的键值对时,后一组的键值对不会插入map容器,即不会覆盖前一组键值对
// 遍历
for(auto it=a.begin() ; it!= a.end(); ++it )
{
cout << it->first << it->second ;
}
// 删除
a.erase(key);
a.clear();
应用
1 . 字符串哈希
unique 去重
- 它能够快速地将相邻的重复元素删除,只保留唯一元素。
- 该算法要求被操作的容器已经按照指定的顺序进行了排序,并返回去重后的最后一个元素的迭代器位置。
int tmp[9] = {0,1,1,1,2,3,6,6,9};
int* a = unique(tmp, tmp + 9) ; //返回值为最后一个数下标的下一个位置
9-图
参考:https://blog.csdn.net/raelum/article/details/129108365
有向图
邻接矩阵
邻接表(每个节点上存了一个单链表)
- 一种特殊的有向图
单链表存可以走到哪个位置
无向图
- 两边 ( a , b ) 和 ( b , a ) 都 add 一下
// dfs 框架
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 100010 ,M = 2*N;
int h[N],e[M],ne[M],idx; //h[a] 表示a的子节点的链
void add(int a,int b) //连一条线
{
e[idx] = b,ne[idx] = h[a],h[a] = idx++;
}
void dfs(int u)
{
st[u]=true; // 标记一下,记录为已经被搜索过了,下面进行搜索过程
for(int i=h[u];i!=-1;i=ne[i])
{
int j=e[i];
if(!st[j])
{
dfs(j);
}
}
}
int main()
{
int n;
cin >> n ;
memset(h,-1 ,sizeof h);
while(n--)
{
int a,b;
cin >> a >> b;
add(a,b);
add(b,a);
}
}
三、搜索与图论
1-DFS(深度优先搜索)
stack (栈) ----- 空间 O(h) -------- 不具最短性
模板
int dfs(int u)
{
st[u] = true; // st[u] 表示点u已经被遍历过
for (int i = h[u]; i != -1; i = ne[i])
{
int j = e[i];
if (!st[j]) dfs(j);
}
}
全排列
1.手写全排列
https://blog.csdn.net/e3399/article/details/7543861
- 最容易出错的地方是: path[u] = i;
/******** 全排列 ********/
using namespace std;
const int N=10;
int path[N] ; /*记录状态 三个数全排列的时候,某种排列123 path[1]=1,path[2]=2,path[3]=3
当排列的长度为 n 时,是一种方案,输出。*/
int state[N]; /*用 state 数组表示数字是否用过。
当 state[i] 为 1 时:i 已经被用过,state[i] 为 0 时,i 没有被用过。*/
int n;
void dfs(int u) // u为遍历的层数,path[]中元素的总个数
{
if(u > n) //当path[]中的数已经到达n时,DFS到底,需要输出
{ // 为什么不到 n 就结束,因为这不是记录全排列的个数(在u==n判断就好),是要添加 path[] 的,而添加path[] 的操作在下面
// 所以就到 n + 1 层来判就好
for(int i=1;i<=n;i++)
printf("%d ",path[i]);
puts("");
return;
}
for(int i=1;i<=n ;i++) //for 的自动+ ,使得排列按字典不重复排列成为可能
{
if(state[i]==0) //在i之前未使用的前提下
{
path[u] = i;
state[i] = 1; //表示使用过了
dfs(u+1); //本位进行完成然后进行下一位
state[i] =0 ; //把这个i解除约束,同时,此位将会被for循环自动赋上一个++i,这个解除约束的i就能出现在下面的位置中了
}
/* *********
有两种情况导致回溯,
一:当层数到了n时,输出 + 回溯 + 恢复使用过的数(例如在全排列123时,到3的时候 dfs(3+1),就会先输出123再return,然后把用过3的痕迹抹掉)
二:当for循环完成时,这个栈结束,回溯到上一个栈(例如在全排列123时,到3的时候 dfs(3+1),就会先输出123再return,然后把用过3的痕迹抹掉,再for循环i=4,此栈内程序结束,返回到上一个栈。然后在这个栈中,首先执行state[2] = 0操作,把用2的痕迹抹去,然后for循环中 i由2 变为3,产生13,然后下一位只能取2,然后输出132,然后抹除用2的痕迹,for i=3不符合if,然后此for结束,再抹除用3的痕迹,然后此层的for继续,开启2..时代)
*/
}
}
int main()
{
scanf("%d",&n);
dfs(1);
return 0;
}
2.C++全排列函数
/*!!!!!!!!!!!!!在使用前需要对欲排列数组按升序排序,否则只能找出该序列之后的全排列数。*/
#include <iostream>
#include <algorithm>
using namespace std;
int main()
{
int num[4]= {1,2,3,4};
do
{
cout<<num[0]<<" "<<num[1]<<" "<<num[2]<<endl;
}
while(next_permutation(num,num+4));
return 0;
}
3.应用:一个含有数据的数组的全排列
没有重复数据的全排列
- 直接用 C++全排列函数
next_permutation(a,a+n); // 返回一种全排列
有重复数据的全排列
- 直接用 C++全排列函数
next_permutation(a,a+n); // 返回一种全排列
找子集
组合数
一、无重复元素求组合数
1. 按照位置枚举(不同位置上放哪个数)
- 如果 从 1~n 中选m个数 变为 从 a[1] ~ a[n] 选 m 个数,硬用这种方法也可以 ,就相当于把组合数用在下标上呗
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 30;
int n, m;
int a[N];
void dfs(int u)
{
if (u > m) // 因为从 a[1] 开始存的数
{
for (int i = 1; i <= m; i++) cout << a[i] << " ";
cout << endl;
return;
}
//if( a[u-1]==n) return ; // 没有选够 m 个就已经没得选了 return ,这步没有也可以,下面会自动跳过
for (int i = a[u - 1] + 1; i <= n; i++) // 选比上一个数字大的第一个数
{
a[u] = i;
dfs(u + 1);
}
}
int main()
{
cin >> n >> m;
a[0] = 0; // 边界设置成无穷小,本题0即可
dfs(1);
return 0;
}
2. 按照数字枚举(每个数字选不选)
- 从 a[1] ~ a[n] 选 m 个数,此方法好想
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 30;
int n, m;
int a[N];
int temp[N];
void dfs(int u, int s) // u代表u这个数(或者temp[u]),s代表当前选了几个数
{
if(s+n-u < m) return; // 剪枝,就是说都选上还不够 n < m 的情况,直接别费劲了
if (s == m)
{
for (int i = 0; i < m; i++) cout << a[i] << " ";
cout << endl;
return;
}
if (s > m) return; //
if (u > n) return;
// 选u
a[s] = u; // 或者 a[s] = temp[u]
dfs(u + 1, s + 1);
// 不选u
dfs(u + 1, s);
}
int main()
{
cin >> n >> m;
dfs(1, 0);
return 0;
}
// 从 a[1] 到 a[n] 选 m 个数
// M 个数互不相同
#include <iostream>
#include <algorithm>
#include <cstring>
#include <vector>
using namespace std;
typedef long long LL;
const int N = 100010;
int a[N];
int n,m ;
int ans[N]; // 路径
void dfs(int u,int s) // 从第 u 个点开始选,已经选了 s 个点了
{
if(s == m)
{
for(int i= 1;i<=m;i++)
cout << ans[i] << " " ;
cout << endl;
return;
}
if( n-u+1+s < m )
return ;
for(int i = u ; i <=n ; i++ )
{
ans[s+1] = a[i];
dfs(i+1,s+1);
}
}
int main() {
cin >> n >> m ;
for(int i = 1;i<=n ;i++)
cin >> a[i];
dfs(1,0);
return 0;
}
3. 位运算
// 效率较低 时间复杂度为2^n, 因为无论从n中选多少个数,始终要从 0遍历到 2^n -1 ,先判断这个数中有几个 1 然后再
#include <bits/stdc++.h>
using namespace std;
int n,m;
void dfs(int x,int now,int date)
{
if (x>n || date+(n-x+1)<m )
return ;
if (date==m)
{
for (int i=1;i<=n;i++)
if (now>>(i-1) & 1)
cout<<i<<" ";
puts("");
return ;
}
dfs(x+1,now+(1<<x),date+1);
dfs(x+1,now,date);
}
int main()
{
cin>>n>>m;
dfs(0,0,0);
}
4. next_permutation 产生{0…1} 选择数
二、有重复元素求组合数
- 当遇到相同的数时,枚举选几个
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 30;
int n, m;
int a[N], path[N];
void dfs(int u, int s) // u代表选到了a[u],s代表当前选了几个数
{
if (s == m)
{
for (int i = 0; i < m; i++) cout << path[i] << " ";
cout << endl;
return;
}
if (s > m) return;
if (u > n) return;
int k = u;
while (k <= n && a[k] == a[u]) k++;
int cnt = k - u;
for (int i = cnt; i >= 0; i--) // 由于输出时要按照字典序输出,所以我们按照从多到少循环,
{ // 小的数越多,字典序就越小😁
for (int j = u; j < u + i; j++)
path[s + j - u] = a[u];
dfs(k, s + i);
}
}
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> a[i];
sort(a + 1, a + 1 + n);
dfs(1, 0);
return 0;
}
作者:optimjie
链接:https://www.acwing.com/blog/content/2131/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
放盘子
爆搜 -> 记忆化数组
/*这道题目相当于是把n个苹果放m个盘子里的一道题,不考虑盘子的顺序 .*/
#include<cstdio>
#include<algorithm>
#include<iostream>
using namespace std;
int f(int x,int y){
if(x == 0) return 1; //没有苹果,全部盘子为0
if(y == 0) return 0; //没有盘子,没法放
if(y > x){ //盘子数大于苹果数,至多只能x个盘子上都放一个
return f(x,x);
}
return f(x - y, y) + f(x, y - 1);//盘子数小于等于苹果数 -> 分类讨论: 有盘子为空,没有盘子为空
//有盘子为空的时候即至少有一个盘子为空,f(x,y-1);没有盘子为空即最少每个盘子都有一个,f(x-y,y)
}
int main(){
int t,n,m; // n个苹果分到m个盘子里去,运行盘子为空
cin >> t;
while(t --){
cin >> n >> m;
cout << f(n,m) << endl;
}
return 0;
}
实际上我们可以发现,在递归的过程中就是要用到之前的数据,继而这道题可以转换为记忆化搜索将结果保存来做,即dp做法,但是这个dp是从递归去思考出来的- -而不是像灿总那样直接思考dp做法.
#include<iostream>
#include<cstdio>
using namespace std;
int a[25][25],m,n;
int main()
{
int t,m,n;
for(m=0; m<=10; m++)
{
for(n=0; n<=10; n++)
{
if(m<n)
a[m][n]=a[m][m];
else if(m==0)
a[m][n]=1;
else if(n==0)
a[m][n]=0;
else
a[m][n]=a[m-n][n]+a[m][n-1];
}
}
scanf("%d",&t);
for(int i=1; i<=t; i++)
{
scanf("%d%d",&m,&n);
printf("%d\n",a[m][n]);
}
return 0;
}
作者:Safe_Sound
链接:https://www.acwing.com/solution/content/8444/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
n 皇后问题
https://www.acwing.com/solution/content/2820/
/**** n 皇后问题 (用DFS解决) ****/
/* 按行枚举 */
//就是在全排列的基础上进行了剪枝
#include<iostream>
using namespace std;
const int N=20; //有了对角线之后长度乘二
char g[N][N];
bool col[N]; //列的状态,每一列中只有一个皇后
bool dg[N]; //正对角线(2~2*n)
bool udg[N]; //反对角线
int n;
void dfs(int u)
{
if(u > n)
{
for(int i=1;i<=n;i++)
{
for(int j =1;j<=n;j++)
cout<<g[i][j];
puts("");
}
puts("");
return;
}
for(int i=1;i<=n ;i++)
{
if(!col[i] && !dg[i+u] && !udg[n+u-i]) //这个对角线的截距b是自己定的(u为纵坐标,i为横坐标),只要是对角线上的点的b都相同就行
{
g[u][i] = 'Q'; //path[u] = i 为了适应题型的修改
col[i] = dg[u+i] = udg[n+u-i] = true; //state[i] =1 修改得 ,用了就记录
dfs(u+1);
col[i] = dg[u+i] = udg[n+u-i] = false; //state[i] =0 修改得; 恢复
g[u][i] = '.'; // 恢复 ,因为全排列时用的 path[]下一次可以覆盖所以不用管,但是这个字符输出需要管
}
}
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
g[i][j] = '.';
dfs(1);
return 0;
}
/**** n 皇后问题 (用DFS解决方法二) ****/
/* 每个位置都来枚举 */
#include<iostream>
using namespace std;
const int N=20;
int n ;
char g[N][N];
bool row[N],col[N],dg[N],udg[N];
void dfs(int x,int y,int s) //x为行,y为列,s为已经放的皇后的个数
{
if( y==n ) y=0,x++; //出界了就转到下一行第一个
if( x==n ) //枚举完最后一行要停止了
{
if( s==n ) //如果此时摆的皇后的个数是n,说明找到了一组解
{
for(int i=0; i<n ; i++)
puts(g[i]);
puts("");
}
return;
}
/***************其实跟全排列一个道理,只是说,全排列是枚举行,每个行数上都要遍历1~n ,所以要用一个for来,但是,这个是按照每个位置来,每个位置都只有两种,放还是不放,所以简化了for ,一个dfs()直接过,接下来是一个dfs()....g[x][y]='Q'... 但是在放的时候是有条件的(!row[x] && !col[y] && !udg[x-y+n] && !dg[x+y]), ****************/
//不放皇后
dfs(x,y+1,s);
//放皇后
if(!row[x] && !col[y] && !udg[x-y+n] && !dg[x+y])
{
g[x][y] = 'Q';
row[x] = col[y] = dg[x+y] = udg[x-y+n] = true ; //记录放上皇后了
dfs(x,y+1,s+1);
row[x] = col[y] = dg[x+y] = udg[x-y+n] = false ;
g[x][y] = '.';
}
}
国际象棋(八皇后拓展版)
- 注意二维的DFS遍历方式 , 双重for循环写出来的是:认为每个马都不一样,相当于全排列,
- 而 y > m 时 x ++ , y = 1 这种方式,就像使用二进制表示状态一样 ,只看位置有没有马 2^(n*m) ,值得注意的是,这种方式加上剪枝 ,是比二进制表示状态的方法效率高的
// 此方法 优于 二进制表示状态,因为可以剪枝
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 110 ,MOD = 1e9+7;
int n,m,k;
int st[N][N] ; // 棋盘,
int dx[] = { -2,-1,1,2,-2,-1,1,2 };
int dy[] = { 1,2,2,1,-1,-2,-2,-1 };
int res ;
void dfs(int x,int y,int sum)
{
if(sum == k) // 又不需要记录数据,判断sum==k 就好,不用sum > k
{
res++;
return;
}
if(y>m)
{
x++,y = 1;
if(x>n) return ;
}
dfs(x,y+1,sum); // 不放马
// 放马
if(!st[x][y]) // 可以放马
{
for(int i = 0;i<8;i++)
{
int x1 = x + dx[i],y1 = y + dy[i];
if( x1 > n || x1 <=0 || y1 > m || y1<=0 )
continue;
st[x1][y1] ++;
}
dfs(x,y+1,sum+1) ;
for(int i = 0;i<8;i++) // 恢复现场
{
int x1 = x + dx[i],y1 = y + dy[i];
if( x1 > n || x1 <=0 || y1 > m || y1<=0 )
continue;
st[x1][y1] --;
}
}
}
int main()
{
cin >> n >> m >> k;
dfs(1,1,0);
cout << res ;
return 0;
}
2-BFS(宽度优先搜索)
找到最近值
queue(队列) ------- O(2的n次方) ---------- 最短路
模板
queue<int> q;
st[1] = true; // 表示1号点已经被遍历过
q.push(1);
while (q.size())
{
int t = q.front();
q.pop();
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
if (!st[j]) //当第一次出现时
{
st[j] = true; // 表示点j已经被遍历过
q.push(j); //将其压进去
}
}
}
迷宫问题
/*
迷宫问题
如下图所示
*/
#include<iostream>
#include<cstring>
using namespace std;
typedef pair<int ,int> PII; //记录每个点的坐标(x,y)
const int N=110;
int n,m;
int g[N][N]; // 0 为可走的路,1为墙
int d[N][N]; //放从开始到此点经历的距离
PII from[N][N]; //记录从哪里来的,最终可以追溯到起源点
PII q[N*N]; //定义N*N个格子,每个格子都有坐标
int bfs()
{
//手写一个队列
//队列的作用是若出现某个步骤有多个可以满足的点,挨个访问这几个点,然后将下一步的点放到队尾,以此循环起来
int hh=0,tt=0;
q[0] = {0,0};
memset(d,-1,sizeof d); //先将距离都初始为 -1
d[0][0] = 0 ; //为零表示已经走过了,是第一步
int dx[4] = {-1,0,1,0},dy[4] = {0,1,0,-1}; //相当与写了一个向量,上下左右
while( hh<=tt ) //访问到最后可走的点都走完了才结束
{
auto t = q[ hh++ ]; //获取对头元素并将对头元素出队(hh++)
for(int i=0;i<4;i++)
{
int x = t.first + dx[i] ,y = t.second + dy[i]; //上下左右随便走了一个
if( x>=0 && x<n && y>=0 && y<m && g[x][y] == 0 && d[x][y] ==-1) //在边界内,并且g[][]=0 为空地,没有走过的时候d[x][y] == -1
/* 目的是找到第一次搜到的点,最短距离嘛 */
{
d[x][y] = d[t.first][t.second] +1; //将原来的点的d[][] +1 后赋给 前进一步后的点(x,y),毕竟人家是多走一步得来的嘛
from[x][y] = t; //记录上次经过的点
q[ ++tt] = {x,y}; //把这个点加入队尾
}
}
}
/***********往前捯治,记录路径************/
int x = n-1,y = m-1;
while( x || y) // 当(x,y)不为(0,0)时,就一直往前捯
{
cout << x+1 << " " << y+1 <<endl;
auto t = from[x][y];
x = t.first, y = t.second ;
}
cout << "1 " << "1" <<endl;
return d[n-1][m-1]; //最后把右下角的距离输出
}
int main()
{
cin>>n>>m;
for(int i=0;i<n;i++)
for(int j=0;j<m;j++)
cin >> g[i][j];
cout<< bfs() <<endl;
return 0;
}
八数码问题(BFS + 哈希)
/*
*/
#include<iostream>
#include<cstring>
#include<algorithm>
#include<unordered_map> //
#include<queue>
using namespace std;
const int N=100010;
int d[N]; //步s
int bfs(string start)
{
string end ="12345678x"; //定义终点
queue<string> q; //字符串队列装不同情况
unordered_map<string,int> d ; //哈希表
q.push(start); //将start先从队尾放进去
d[start] = 0 ; //先进行第一步
int dx[4] = {-1,0,1,0},dy[4] = {0,1,0,-1}
while(q.size()) //经典的宽搜代码
{
auto t = q.front() ;
q.pop() ;
int distance = d[t] ;
if(t == end) //先判断是否完成了,就输出distance
return distance ;
//状态转移
int k = t.find("x"); //首先去找x的位置,然后把上下左右分别与之互换
int x=k/3,y = k%3; //把 k 的x,y坐标找出来
for(int i=0; i<4 ; i++)
{
int a = x + dx[i],b = y + dy[i];
if(a>=0 && a<3 && b>=0 && b<3 ) //如果a,b都没有出界
{
swap(t[k],t[a*3 + b]); //把状态更新
if(!d.count(t)) //更新完成后立马加入到队列中(之前没有搜到的情况下),count(t)记录t出现的次数
{
d[t] = distance + 1; //此时t已经到了新的节点上去了
q.push(t); //入队
}
swap(t[k],t[a*3 + b]); //还原回来哦
}
}
}
/*push(); //向队尾插入一个元素
front(); //返回队头元素
back(); //返回队尾元素
pop(); //弹出队尾元素 */
return -1;
}
int main()
{
string start; //用字符串来表示那个3*3矩阵
for(int i=0;i<9;i++)
{
char c;
cin >> c;
start += c;
}
cout << bfs() << endl;
return 0 ;
}
3-树与图的深度优先遍历
// dfs 框架
void dfs(int u){
st[u]=true; // 标记一下,记录为已经被搜索过了,下面进行搜索过程
for(int i=h[u];i!=-1;i=ne[i]){
int j=e[i];
if(!st[j]) {
dfs(j);
}
}
}
846.树的重心 (无向树)
// 利用了树的 D F S 遍历是递归的方式 , 可以先知 树的size
// dfs 保证了是从上往下遍历的,不会回溯到上面,从而可以求出正确的 size
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 100010 ,M = 2*N;
int n;
int h[N],e[M],ne[M],idx;
bool st[M]; //**状态数组,防止子节点搜索父节点**
int ans= N ; //记录最大的子连通块的点数,这个N要足够大哦
void add(int a,int b) //两点之间连一条线
{
e[idx] = b,ne[idx] = h[a],h[a] = idx++;
}
int dfs(int u)
{
st[u] = true; //标记访问过u节点
int sum = 1; //因为本身就有一个,所以初始为1
int size = 0; //size删掉元素后各个子连通块的最大值
for(int i=h[u] ; i!= -1 ;i = ne[i] ) // i!=-1 是因为最开始 h[] = -1,往下走的原因
{
int j = e[i]; //把一个根节点的树枝节点赋给 j
if(!st[j]) //判断这个树枝节点之前有没有搜到过,即不会发生子节点搜索父节点才行哦
{
int s = dfs(j);
size = max(size,s);
sum += s; //以j为根的连通块的点数累积到sum中
}
}
//for循环完成后也就是说把以 u 为根的树(无论是总的大树,还是分支的小树)的点数求好了
//最后返回的是sum,在返回之前,先把这个情况下的点数(这个分法分完所有连通块的最大点数)与之前的比较,是最大就更新ans
size = max(size,n-sum); //此分发下,比较size(根下的2个连通块的点数)与根上的点数(n-sum)比出最大
ans = min(size,ans); //全局变量ans存最小的最大值
return sum ;
}
int main()
{
memset(h,-1,sizeof h); //初始化h数组 -1表示尾节点
cin>>n;
for(int i = 0;i<n-1;i++)
{
int a,b;
cin >> a >> b;
add(a,b),add(b,a);
}
dfs(1); //随便从一个开始都行
cout<<ans<<endl;
return 0;
}
// 二刷错误
/* 如下细节要注意:
1.因为没有指定根节点, 两个边都要存(无向),dfs 的时候,只需变为 true 即可,画图易理解
2. i != -1 错写为 ne[i]!=-1
3. min 写为 max 没读清题目
*/
#include<iostream>
#include<cstring>
using namespace std;
const int N = 100010;
int n;
int h[N];
int ne[2*N],e[2*N]; // 无向图有 2*N 条边
int idx;
int ans = N;
bool st[N];
int add(int a,int b)
{
e[idx] = b; // b 是 a 的一棵子树
ne[idx]= h[a];
h[a] = idx++;
}
int dfs(int u) // 返回以u为根节点的树的总个数
{
int sum = 1 ; // 先把自己这个结点算上
int res = 0 ; // 每种情况下连通块的最大个数
st[u] = true; // 表明以此点为根节点的树已经得到了结果 ,细节是无向图可以往上走,为true往上走不动
for(int i = h[u] ; ***********ne[i]!=-1*********** ; i = ne[i]) // 要写为 i!=-1
{
int j = e[i] ; // 每个 e[] 存一个孩子的地址
if(!st[j])
{
int s = dfs(j);
res = max(res,s) ;
sum += s ;
}
}
res = max(res,n-sum);
ans = min(res,ans);
return sum;
}
int main()
{
cin >> n;
memset(h,-1,sizeof h);
for(int i = 1;i<n;i++)
{
int a,b;
cin >> a >> b;
add(a,b),add(b,a);
}
dfs(1);
cout << ans << endl;
return 0;
}
4-树与图的广度优先遍历
模板
//BFS模板
queue<int> q;
st[1] = true; // 表示1号点已经被遍历过
q.push(1);
while (q.size())
{
int t = q.front();
q.pop();
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
if (!st[j])
{
st[j] = true; // 表示点j已经被遍历过
q.push(j);
}
}
}
847.图中点的层次
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环。
所有边的长度都是 1,点的编号为 1∼n。
请你求出 1 号点到 n号点的最短距离,如果从 1 号点无法走到 n号点,输出 −1。
- 出现自环时 广度优先遍历必须用一个计数值来保证只会访问一次 ,防止无限循环
/*
注意是有向图
只写一个add(a, b),无向图的时候才些两个add(a, b),add(b,a);
*/
#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 100010,M= 2*N;
int n,m;
int idx;
int h[N],e[M],ne[M],q[M];
int d[N]; //存放路径的值,点是第几步被第一次访问到的
void add(int a,int b)
{
e[idx] = b,ne[idx] = h[a],h[a] = idx++;
}
int bfs()
{
int hh=0,tt=0;
memset(d,-1,sizeof d); //先将所有点的路径值初始为 -1
q[0] = 1; //把第一个节点给放到队列里,即从树的根节点开始宽度遍历
d[1] = 0; //第一个点(此树的根)是第一步访问到的,所以置为0
while(hh<=tt)
{
int t = q[ hh++ ]; //将队头首先存到 t 中,然后队头自动移到下一个位置,省的在最后再写一个 hh++
for(int i=h[t] ; i!= -1 ;i = ne[i])
{
int j = e[i]; //用 j 来记录接下来某个子树的idx
if( d[j] == -1)
/*有两个作用:
1.对于每个节点,只有第一次访问时才把在路径中的步骤赋上,
是到达这个点的最短路径(后来的路径肯定是多走了几步呀)
2.无向图时,每两个点互相有边,防止在子树时又访问到根上倒念,当然现在是有向图没有这个用处
*/
{
d[j] = d[t] + 1; //把路径中的步骤给这个新点赋上
q[++tt] = j; //只放入一遍,防止出现自环/重边
}
}
}
return d[n];
}
int main()
{
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
for (int i = 0; i < m; i ++ )
{
int a, b;
scanf("%d%d", &a, &b);
add(a, b);
}
cout << bfs() << endl;
return 0;
}
#include<iostream>
#include<cstring>
#include<vector>
#include<queue>
using namespace std;
const int N = 100010;
int n,m;
int h[N];
int ne[2*N],e[2*N]; // 无向图有 2*N 条边
int idx;
bool st[N];
int d[N] ;
int p[N] ;
int add(int a,int b)
{
e[idx] = b; // b 是 a 的一棵子树
ne[idx]= h[a];
h[a] = idx++;
}
int bfs()
{
queue<int> q;
q.push(1);
d[1] = 0;
while(q.size())
{
auto t = q.front();
q.pop();
if(!st[t])
{
st[t] = true;
for(int i = h[t] ;i!=-1 ; i=ne[i])
{
int j = e[i];
if(d[j]==-1) // 只记录第一次经过
{
q.push(j);
d[j] = d[t] + 1; p[j] = t;
}
}
}
}
return d[n];
}
int main()
{
cin >> n >> m ;
memset(h,-1,sizeof h);
memset(d,-1,sizeof d);
for(int i = 0;i<m ;i++)
{
int a,b;
cin >> a >> b;
add(a,b);
}
cout << bfs();
return 0;
}
5-拓扑排序
针对有向图
有向无环图称为拓扑图
所有的边都是从前指向后的,所以环就不行
- 入度:为 0就是没有指向我的边(没有边在我前面),入度为零的点在当前的最前面的位置
- 出度:相当于子树的个数
拓扑排序的宽搜
//步骤
queue <- 所有入度为零的点
while queue不空
{
t <- 队头赋上
for(枚举t 的所有出边 t -> j)
删掉 t -> j (删掉每个j的一个入度),d[j]--;
if(d[j] == 0) //当入度为0时,即可加入队列
queue <- j ;
}
- 注意别忘记 memset (h,-1,sizeof h)
- 入度是 b 增
- q[ N ] 用 hh<=tt 队列来维护,最上面的砖拿开后(附带将下面 的子砖 rudo[ ] – )
- 但是q [ ] 存放的是一种拓扑序列
/*
拓扑序列搜索:
其实就像是一堆砖头,从上面往下拿,只有某个砖头上面没有砖头的时候(入度为零的时候)
才能把这个砖头拿走
**注意**:
队列的实现,当不提前赋初值时tt = -1
*/
#include<iostream>
#include<cstring>
#include<iostream>
using namespace std;
const int N= 100010;
int n,m;
int idx;
int q[N],ne[N],e[N],h[N];
int d[N]; //点的入度
void add(int a,int b)
{
e[idx] = b,ne[idx] = h[a],h[a] = idx++;
}
/******************/
bool topsort()
{
int hh=0, tt=-1; //定义队头队尾
for(int i=1;i<=n;i++)
if(!d[i])
q[ ++tt ] = i ; //把所有入度为0 的点入队
while(hh<=tt)
{
int t = q[ hh ++ ]; //每次取出队头元素并将往后走一个
//拓展一下队头元素
for(int i=h[t];i!= -1;i = ne[i])
{
int j = e[i] ;
d[j]--; //
if(d[j] == 0) //如果上面没有砖头了就入队
q[++tt] = j;
}
}
return tt == n-1 ; //所有的都进入队列了,就是有向无环图,否则说明是一个
}
/******************/
int main()
{
cin >> n>>m ;
memset(h, -1, sizeof h);
for(int i =0;i< m ;i++)
{
int a,b;
cin>>a>>b;
add(a,b);
d[b] ++; //入度加1
}
if(topsort())
{
for(int i= 0;i<n;i++)
cout<<q[i]<<" ";
puts("");
}
else puts("-1");
return 0;
}
#include<iostream>
#include<algorithm>
#include<cstring>
#include<queue>
using namespace std;
const int N = 100010;
int n,m;
int idx,h[N],e[2*N],ne[2*N],rudo[N];
int tmp[N],cnt;
void add(int x,int y)
{
e[idx] = y;
ne[idx]= h[x];
h[x] = idx++;
rudo[y]++; //y的入度+1
}
int main()
{
cin >> n>> m;
queue<int> q;
memset(h,-1,sizeof h);
while(m--)
{
int x,y;
cin >> x >>y;
add(x,y);
}
for(int i=1;i<=n;i++)
if(rudo[i]==0)
q.push(i);
for(int i = 1;i<=n;i++)
{
//cout << rudo[i]<<endl;
}
while(!q.empty())
{
int t = q.front();
q.pop();
tmp[++cnt] = t;
for(int i=h[t]; i!= -1 ;i=ne[i])
{
int s = e[i];
rudo[s]--;
if(rudo[s]==0)
q.push(s);
}
}
if(cnt!=n)
cout << -1 <<endl;
else
for(int i=1;i<=n;i++)
cout << tmp[i] << " ";
return 0;
}
拓扑排序的深搜
6-最短路
- 不区分有向无向
单源最短路
1. Dijkstra算法
条件:用于计算图中从一个指定点到其余所有点的最短路径。图是有向图,存在重边和自环,所有边的权重为非负数(负数的话不能保证贪心:每一步是最优解)
-
先把 dis[ 1 ] = 0 , dis[ i ] = +无穷
-
n次迭代,每次找到距离的最小值加入V中,然后用这个最小值去更新一下V-S中其他点到指定点的距离
-
稠密图用邻接矩阵存,稀疏图用邻接表
求源点到其余各点的最短距离步骤如下:
- 用一个 dist 数组保存源点到其余各个节点的距离,dist[i] 表示源点到节点 i 的距离。初始时,dist 数组的各个元素为无穷大。
用一个状态数组 state 记录是否找到了源点到该节点的最短距离,state[i] 如果为真,则表示找到了源点到节点 i 的最短距离,state[i] 如果为假,则表示源点到节点 i 的最短距离还没有找到。初始时,state 各个元素为假。
- 源点到源点的距离为 0。即dist[1] = 0。
- 遍历 dist 数组,找到一个节点,这个节点是:没有确定最短路径的节点中距离源点最近的点。假设该节点编号为 i。此时就找到了源点到该节点的最短距离,state[i] 置为 1。
- 遍历 i 所有可以到达的节点 j,如果 dist[j] 大于 dist[i] 加上 i -> j 的距离,即 dist[j] > dist[i] + w[i][j](w[i][j] 为 i -> j 的距离) ,则更新 dist[j] = dist[i] + w[i][j]。
- 重复 3 4 步骤,直到所有节点的状态都被置为 1。
- 此时 dist 数组中,就保存了源点到其余各个节点的最短距离。
伪代码:
int dist[n],state[n];
dist[1] = 0, state[1] = 1;
for(i:1 ~ n)
{
t <- 没有确定最短路径的节点中距离源点最近的点;
state[t] = 1;
更新 dist;
}
/*
即进行n次迭代去确定每个点到起点的最小值,
最后输出的终点的即为我们要找的最短路的距离。
- dist[i] 装的是点i到的起点的最短距离(刚开始的时初始为正无穷)
- g[a][b] 装的是任意两条边之间的最短距离
自环:指向自己的边
重边:两个点之间有许多边(只保留一个距离最短边就行)
-
*/
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=510;
int n,m;
int g[N][N]; //邻接矩阵
int dist[N]; //当前的最短距离是多少
bool st[N]; //当前的最短路是不是已经确定了
//True表示已经确定最短路 属于s集合
int dijkstra()
{
memset(dist,0x3f,sizeof dist); //第一步:把dist初始化为正无穷
dist[1] = 0;
for(int i=0; i<n ; i++) //第二步:n次迭代,每次找到点i到的起点的最短距离
{
/***********************这一段秒啊**************************/
/** 保证了每次都会访问到st[] = false中的dist最小的那个******/
int t = -1; //一开始t的赋值是-1,如果t没有被更新,那么要强制更新一下t
for(int j=1;j<=n;j++) //遍历所有的点,找到当前dist最小的点
{
if(!st[j] && (t==-1 || dist[t] > dist[j])) //t== -1 就是为了先随便放一个,以后要是有更小的就再更新呗。不然dist [t] = 上次最小正值,科可能导致 dist[t] < dist[j],然而之前的t已经用过,再搞就死循环在这
t = j; //当dist不是最短的,那么把t更新成j
}
/****************************************************/
st[t] = true;
/****** 然后计算从t通往各点的距离,并与之前距离取最小***/
/*****其实有用的也就是t出度的那些点,
*****但是我们直接for 1~n省事,反正其他都是正无穷无所谓**/
for(int j=1;j<=n;j++)
dist[j] = min(dist[j],dist[t]+g[t][j]);
}
if(dist[n]==0x3f3f3f3f)
return -1; //如果第n个点路径为无穷大即不存在最低路径
return dist[n];
}
int main()
{
cin >> n >> m ;
memset(g,0x3f,sizeof g); //初始化
while(m--)
{
int a,b,c;
cin>>a>>b>>c;
g[a][b] = min(g[a][b],c); //g[a][b]存a到b的最小的权重(路径的最短距离)
}
int t =dijkstra();
cout << t;
return 0;
}
方法二:
#include<iostream>
#include<cstring>
using namespace std;
const int N = 510;
int g[N][N]; //linjiejuzhen
int dist[N];
bool st[N];
int n,m;
int dijkstra()
{
dist[1] = 0;
for(int i=0;i<n;i++)
{
int t = -1 ,Min = 0x3f3f3f3f + 1000; //一定要加上一个数 ,保证即使 第一个为false的dist[j] 等于0x3f3f3f3f 也能赋上
for(int j=1;j<=n;j++)
{
if(st[j]==false && dist[j]<Min)
t = j,Min = dist[j];
}
st[t] = true;
for(int j=1;j<=n;j++)
{
if(st[j]==false && g[t][j] != 0x3f3f3f3f && g[t][j] + dist[t] < dist[j])
dist[j] = g[t][j] + dist[t];
}
}
if(dist[n]==0x3f3f3f3f)
return -1;
return dist[n];
}
int main()
{
cin >> n>>m;
memset(g,0x3f,sizeof g);
memset(dist,0x3f,sizeof dist);
for(int i = 0;i<m;i++)
{
int x,y,z;
cin >> x >>y>>z;
g[x][y] = min(z,g[x][y]);
}
cout << dijkstra();
return 0;
}
2. 堆优化的Dijkstra算法
为了解决稀疏图将邻接矩阵改为邻接表
/*
不断循环,直到堆空。每一次循环中执行的操作为:
弹出堆顶(与朴素版diijkstra找到S外距离最短的点相同,并标记该点的最短路径已经确定)。
用该点更新临界点的距离,若更新成功就加入到堆中。
因为有权重,所以其实堆里存的是 pair<下一个的节点,权重(路程)>
*/
#include<iostream>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
typedef pair<int,int> PII; //{距离,编号}
const int N=150010; //要是O(n^2)就爆了
int n,m,idx;
int h[N],e[N],ne[N]; //邻接表
int w[N]; //邻接表中存权重
int dist[N]; //当前的最短距离是多少
bool st[N]; //当前的最短路是不是已经确定了
//True表示已经确定最短路 属于s集合
void add(int a,int b,int c)
{
e[idx] = b,w[idx] = c;
ne[idx] = h[a];
h[a] =idx++;
}
int dijkstra()
{
memset(dist,0x3f,sizeof dist); //第一步:把dist初始化为正无穷
dist[1] = 0;
priority_queue<PII,vector<PII>,greater<PII>> heap; //开一个小根堆,注意用于排序的是
heap.push({0,1}); //1号点放进来,距离是0,编号是1
while(heap.size()) //不为空
{
auto t = heap.top();
heap.pop(); //把队头弹出(用完了)
int distance = t.first,dian= t.second;
if(st[dian]) continue;
st[dian] = true; // 从队头出来的一定是dist最小的,选择才此处将st[] 置为 true,即进入s集合
/**************************************************/
for(int i =h[dian];i!= -1 ; i = ne[i] )
{
int j = e[i] ;
if(dist[j] > dist[dian] + w[i]) //目的是找到某个节点dist的最小值,一旦更小了之后,优先队列会自动排序,不用管,然后之前那个入列的dist不是最小的那个节点,就会在访问到时continue掉了
{
dist[j] = dist[dian] + w[i];
heap.push({dist[j], j}); //我们是用优先队列简化了每次找下一个最小dist[]的for循环,要保证每次dist更新后,说不定就是下一个 dist[] 最小呢 ,所以加到对列中
}
}
/*************************************************/
}
if(dist[n]==0x3f3f3f3f)
return -1; //如果第n个点路径为无穷大即不存在最低路径
return dist[n];
}
int main()
{
cin >> n >> m ;
memset(h,-1,sizeof h ); //初始化
while(m--)
{
int a,b,c;
cin>>a>>b>>c;
add(a,b,c);
}
int t =dijkstra();
cout << t;
return 0;
}
3. Bellman-Ford算法
应用条件:
- 有负权边可用,可判断存在负环 , 负环时间复杂度高,尽量不用,用spfa
- 可以计算 经过最多k条边的最短回路
主要思想:
- 对所有的边进行n-1轮松弛操作,因为在一个含有n个顶点的图中,任意两点之间的最短路径最多包含n-1边
换句话说:
-
第1轮在对所有的边进行松弛后,得到的是源点最多经过一条边到达其他顶点的最短距离;
-
第2轮在对所有的边进行松弛后,得到的是源点最多经过两条边到达其他顶点的最短距离;
-
第3轮在对所有的边进行松弛后,得到的是源点最多经过三条边到达其他顶点的最短距离…
(有限制:经过最多k的最短回路)
存在负环时可能存在最短路径 ,也可能不存在
- 当负环在到n 的路径上时,不存在最短路径
- 当负环不在到n 的路径上时,存在最短路径
时间复杂度:
- 两重循环 O(n*n)
实现代码:
随便存边:用一个结构体
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 510, M = 10010;
struct Edge
{
int a, b, c;
}edges[M];
int n, m, k;
int dist[N];
int last[N]; //原状态
void bellman_ford()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
for (int i = 0; i < k; i ++ )
{
memcpy(last, dist, sizeof dist); //先把原状态存下来,防止下面遍历时前一步铺垫后一步树藤摸瓜,多进了一些层数,而这样就破坏了顺序(因为每次只允许多加一条边)
for (int j = 0; j < m; j ++ )
{
auto e = edges[j];
dist[e.b] = min(dist[e.b], last[e.a] + e.c); //用最初的dist即last,就没法s
}
}
}
int main()
{
scanf("%d%d%d", &n, &m, &k);
for (int i = 0; i < m; i ++ )
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
edges[i] = {a, b, c};
}
bellman_ford();
if (dist[n] > 0x3f3f3f3f / 2) puts("impossible");
else printf("%d\n", dist[n]);
return 0;
}
4. spfa算法
应用条件:
- spfa算法规定 可以有负权,但是数据保证不存在负权回路
- 能指定走几步吗?不能,只有Bellman-Ford 算法可以
主要思想:
- 先说一个结论:只有一个点在上一轮被松弛成功时,这一轮从这个点连出的点才有可能被成功松弛。
为什么?显而易见
好吧其实我当初也花了不少时间理解这玩意
松弛的本质其实是通过一个点中转来缩短距离(如果你看了前置很容易理解)。所以,如果起点到一个点的距离因为某种原因变小了,从起点到这个距离变小的点连出的点的距离也有可能变小(因为可以通过变小的点中转)。(通读三遍再往下看)
所以,可以在下一轮只用这一轮松弛成功的点进行松弛,这就是SPFA的基本思想。
-
我们知道了在下一轮只用这一轮松弛成功的点进行松弛,就可以把这一轮松弛成功的点放进队列里,下一轮只用从队列里取出的点进行松弛。
为什么是队列而不是其他的玄学数据结构?因为队列具有“先进先出,后进后出”的特点,可以保证这一轮松弛的点不会在这一轮结束之前取出。
时间复杂度:
- 一般O(m) , 最坏 O(n*m)
spfa求最短路
/*比Bellem-ford 的改进是,Bellem-ford 每次都是无差别的遍历所有的边,
但是spfa只遍历有用的边(有可能使下一个点dist变小的边)!!!其实就是去遍历本层的子边,而不是像bellem-ford一样遍历所有的边
实现原理是:
只有当父节点的dist变化时,才访问后面的点,即遍历这一点的所有子边
(注意,遍历子边是无差别的遍历,但是这一层中第一次访问到这个点时才进队列,
这一层的其他子边再访问到这个点时就不重复加入对列,只更新dist[]就好了,实现方法是第一次后st[i] = true 上锁即可,当更新完i后解锁st[i] = false)
【说“层”的原因:这种方法类似图的宽搜,其实就是Bellem-ford层遍历的剪枝】
【spfa不能实现像Bellem-ford的功能(最多走k条边dist[1~n]的最小值),我认为原因是使用了数据结构 queue 的固有缺陷,
虽然更方便的实现了遍历,但带来的问题是难以找到每层的边界】
*/
#include<iostream>
#include<algorithm>
#include<queue>
#include<cstring>
using namespace std;
const int N = 100010;
int m,n;
int e[N],ne[N],idx,w[N],h[N];
int dist[N];
bool st[N];
int add(int a,int b,int c)
{
e[idx] = b , w[idx] = c;
ne[idx] = h[a];
h[a] = idx++;
}
int spfa()
{
queue<int> q;
q.push(1);
dist[1] = 0;
while(!q.empty())
{
int a = q.front();
q.pop();
st[a] = false ; //在遍历子边前、后解锁都无所谓,只要在下一次while前就行,因为下一次while的点可能指向这个点
for(int i=h[a] ; i!=-1 ;i=ne[i])
{
int j = e[i];
if(dist[j] > dist[a] + w[i] ) //这个点还没有放进来 && 路径更新最小值
{
dist[j] = dist[a] + w[i] ;
//cout << dist[j] <<endl;
if(!st[j])
{
q.push(j);
st[j] = true;
}
}
}
}
if(dist[n]>=0x3f3f3f3f/2)
return 0x3f3f3f3f;
else
return dist[n];
}
int main()
{
cin >> n >> m;
memset(h,-1,sizeof h);
memset(dist,0x3f,sizeof dist);
while(m--)
{
int a,b,c;
cin >> a >> b >>c;
add(a,b,c);
}
int t = spfa();
if(t==0x3f3f3f3f)
puts("impossible");
else
cout << t << endl;
return 0;
}
spfa判断负环
/*
建一个虚拟源点 ,然后把 { 1,2,3 .... } 都加进去,并将st[i] 都为true 就顺理成章了
*/
多源汇最短路(任一点到任一点的最短路)
Floyd 算法
![image-20230927230442593](https://i-blog.csdnimg.cn/blog_migrate/0e271afb49ddc4c24a1990977d0d3231.png)
-
基于动态规划 dp : d[ k , i , j ] 第i 个点只经过1~k 这些中间点 到 j 的最短距离
-
递推式 : d[ k , i , j ] = d[ k-1 , i , k ] + d[ k-1 , k , j ]
-
第一维 k 依赖于上一次的 k-1 层 ,不是因为省一维空间来省掉 ,而是为了实现 d [ i,j ] 的动态更新必须省掉
一 、首先: // 每一个 d[ k , i , j ] 是从 d[ k-1 ,.....] 推出的,所以 k 从 0 往上循环 for(int k = 1 ; k<=n ;k++ ) for(int i = 1;i<=n ;i++ ) for(int j = 1;j<=n;j++) d[i][j] = min(d[i][j] , d[i][k] + d[k][j]); //记得把 对角线d[i][i] 初始化为零 for(int i = 1;i<=n;i++) d[i][i] = 0;
-
(y总真言,简单易懂)
-
f[i, j, k]表示从i走到j的路径上除i和j点外只经过1到k的点的所有路径的最短距离。那么f[i, j, k] = min(f[i, j, k - 1), f[i, k, k - 1] + f[k, j, k - 1]。
因此在计算第k层的f[i, j]的时候必须先将第k - 1层的所有状态计算出来,所以需要把k放在最外层。 -
读入邻接矩阵,将次通过动态规划装换成从i到j的最短距离矩阵
-
在下面代码中,判断从a到b是否是无穷大距离时,需要进行if(t > INF/2)判断,而并非是if(t == INF)判断,原因是INF是一个确定的值,并非真正的无穷大,会随着其他数值而受到影响,t大于某个与INF相同数量级的数即可
7-最小生成树
应用:
给定一个无向图,在图中选择若干条边把图的所有节点连起来。要求边长之和最小。在图论中,叫做求最小生成树。
- 无向图
Prim
无向图,边权可以为负数
朴素版 Prim
O(n^2)
稠密图
/* 其实就是两个重要的点:
1.和迪杰斯特拉算法一样,每次都找《到连通块集合》距离最小的节点(st[]为true了)进入连通块(迪杰斯特拉的不同是每次将《距离源点》最小的节点进入集合st[]为true了)
2.累积访问到的点(刚刚成为集合、连通图内的点)对所有点的dist,也就是说,
-对于 Dijkstra算法,任何一个点经过刚访问到的点然后到源点的最短距离dist,相当于去更新到源点的最短路
-对于 prim算法,q更新任一点到此点的最短路,也就是说更新了任何一点到连通块的最短距离(只不过是到达连通块的某一节点)
*/
/*注意: < 把res += dist[t]; 放在用t点更新dist[]前,防止自环产生(出现一个负值g[t][],把dist[t] 变小了,) >
关键点两种写法:
一:
res += dist[t];
for(int i = 1; i <= n; i++)
{
if(dist[i] > g[t][i] && !st[i]) //此处带了!st[i]为了能记录前驱,不用记录前驱的话不带也行
{
dist[i] = g[t][i];
pre[i] = t; //这种带有!st[t] 保护,能记录前驱
}
}
二:
for(int i = 1; i <= n; i++)
{
if(dist[i] > g[t][i] && !st[i]) //此处必须带!st[i]
{
dist[i] = g[t][i];
pre[i] = t; //这种带有!st[t] 保护,能记录前驱
}
}
res += dist[t];
*/
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N =510, INF = 0x3f3f3f3f;
int n,m;
int g[N][N];
int dist[N]; //是到连通块的最小距离,区别于到原点的距离
bool st[N]; //这个点是不是再连通块里面
int pre[N]; //保存上一个节点是啥
int prim()
{
memset(dist,0x3f,sizeof dist);
int res =0; //最小生成树里所有边的长度之和
dist[1] = 0; //从 1 号节点开始生成
for(int i =0; i<n ; i++) //每个节点都要访问一遍,进行n次
{
int t = -1; //小技巧,保证让第一次读入
for(int j =1; j<=n; j++)
if(!st[i] && (t==-1 || dist[t] > dist[j]) )
t = j; //找到未列入连通块 且 与上次访问的节点最近的点
/*
其实与迪杰斯特拉算法很像,可能用到了贪心的原理
*/
st[t] = 1; // 将该点接入连通块里面
/*先最小生成树的权值之和 ,再进行遍历更新 ,防止出现有个 g[t][t] < 0 导致更新后dist[t] 变小 ,所以要么提前加,要么后面更新的时候跳过自身 */
res += dist[t]; //累计最小生成树的权值之和
/* 2 */
for(int i = 1; i <= n; i++) //用新值去更新接下来的 dist[] ,并找到下一个进入连通图的点
{
if(dist[i] > g[t][i] && !st[i]) //从 t 到节点 i 的距离小于原来距离,则更新,!st[i]这样可以避免自环;原来dist[t] = 4,g[t][] = -10 ,就会使 dist[t] 变小,如果把 res += dist[t] 放到前面就没问题了
{
dist[i] = g[t][i]; //更新距离
pre[i] = t; //从 t 到 i 的距离更短,i 的前驱变为 t.
}
}
}
}
int main()
{
scanf("%d%d",&n,&m);
memset(g,0x3f,sizeof g);
while(m--)
{
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
g[a][b] = g[b][a] = min( g[a][b],c ); //无向连通图,让a->b与b->a 一样,且找到a,b边中边长最小的值
}
int t = prim();
if(t==INF) puts("impossible");
else printf("%d\n",t);
return 0;
}
void Prim_2()
{
for(int k = 0;k < n ;k++) // 把 n 个点都放进去
{
int t = 0 , min = INF; //小技巧,保证首先把第一个放进去
for(int i = 1 ;i <= n ;i++)
{
if(!st[i] && d[i]<= min) // <= 为了把最后那个点(无论是否有边连着它)也放进来
{
t = i;
min = d[i];
}
}
st[t] = true;
res += d[t];
for(int i = 1 ; i<=n ;i++)
{
if(!st[i] && d[i]>f[t][i])
{
d[i] = f[t][i] ;
}
}
}
if(res>INF/2)
cout << "impossible" <<endl;
else
cout << res ;
return 0;
}
堆优化版
O(m log n)
稀疏图
一般可以用 Kruskal 算法 替代
Kruskal算法
给定一个 n 个点 m 条边的无向图,图中可能存在重边和自环,边权可能为负数。
稀疏图中使用,因为用到了排序,所以使得简单化了
- 1.将所有的边按权重从小到大排序 O(mlogm)
- 2.枚举每条边a,b的权重c
if a,b 不连通
将这条边加入到连通块中
克鲁斯卡尔算法查找最小生成树的方法是:
-
将连通网中所有的边按照权值大小做升序排序,
-
从权值最小的边开始选择,只要此边不和已选择的边一起构成环路,就可以选择它组成最小生成树。
-
对于 N 个顶点的连通网,挑选出 N-1 条符合条件的边,这些边组成的生成树就是最小生成树。
难点在于“如何判断一个新边是否会和已选择的边构成环路”
859. Kruskal算法求最小生成树
-
从小到大从从所有边中添加 ,能形成环的不要添加 , 共添加 n -1 条边
- 而且最小生成树不一定是唯一的
-
//用并查集的思路
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 200010;
int p[N];
int n,m;
struct Edge
{
int a,b,w;
/*
重载运算符
bool operator< (const Edge &W)const
{
return w<W.w;
}
*/
}edges[N];
bool cmp(Edge a,Edge b)
{
return a.w < b.w; //从小到大排序
}
int find(int x)
{
if(p[x]!=x)
p[x] = find(p[x]);
return p[x];
}
int main()
{
cin >> n >> m ;
for(int i=0;i<m;i++)
scanf("%d%d%d",&edges[i].a,&edges[i].b,&edges[i].w);
sort(edges,edges + m,cmp);
for(int i=1;i<=n;i++) p[i] = i; //初始化
int res = 0,cnt =0; //res为最小生成树边权和,cnt为集合中边的个数
for(int i=0 ; i<m ; i++)
{
int a = edges[i].a,b = edges[i].b,w = edges[i].w;
a = find(a),b = find(b); //a,b更新为祖宗节点
if( a!=b )
{
p[a] = b;
cnt++;
res += w;
}
}
if(cnt<n-1) cout << "impossible" <<endl;
else
cout << res <<endl;
return 0;
}
/* 2022.10.17 */
/* 不形成环的实现方式:两点不在同一个连通块中 */
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N = 200010;
const int INF = 0x3f3f3f3f;
int n,m;
int p[N];
struct Egde
{
int a,b,w;
}edge[N];
bool cmp(Egde a,Egde b)
{
return a.w<b.w;
}
int find(int x)
{
if(p[x]!=x)
p[x] = find(p[x]);
return p[x];
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) p[i] = i;
for(int i=0;i<m;i++)
{
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
edge[i] = {a,b,c};
}
sort(edge,edge+m,cmp);
int res = 0 ,cnt = 0;
for(int i = 0; i<m ; i++)
{
int a=edge[i].a , b = edge[i].b , w = edge[i].w;
if( find(a)!=find(b) ) //如果不连通
{
p[find(b)] = find(a);
cnt++;
res += w;
}
}
if(cnt<n-1) //不像prim可以用res = INF 来判定所有边全部连通, 看最后的连通块的边数是否为n-1
printf("impossible");
else
printf("%d",res);
return 0;
}
8-二分图
- 将所有点分成两个集合,使得所有边只出现在集合之间,就是二分图
- 换句话说:二分图当且仅当图中不含奇数环
- 二分图:一定不含有奇数环,可能包含长度为偶数的环, 不一定是连通图
染色法
-
1和2交替染色,刚好使形成左边一个 右边一个
因为不含有奇数环,所以染色的过程一定不会有矛盾,就相当于把一个点染了一个颜色后,接下来满足不存在奇数环下,再去染色,还是之前那个颜色,不会混染
实现方式:
860.染色法判定二分图
//无向图存两条有向边
//若已经染色且颜色和相邻顶点的颜色相同则说明不是二分图,若颜色不同则继续判断。
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N =100010;
int h[N],idx;
int e[N*2],ne[N*2];
int color[N];
int n,m;
bool dfs(int u,int c)
{
color[u]=c;
for(int i=h[u];i!=-1;i=ne[i])
{
int j = e[i];
if(!color[j]) //如果没有染色
{
if(!dfs(j,3-c))
return false;
}
else //如果之前已经染色了,看冲突吗
{
if(color[j]==c)
return false;
}
}
return true;
}
int main()
{
cin >> n >> m;
memset(h,-1,sizeof h);
while(m--)
{
int u,v;
cin >> u >>v;
e[idx] = v,ne[idx] = h[u] ,h[u] = idx, idx++;
e[idx] = u,ne[idx] = h[v] ,h[v] = idx, idx++;
}
bool flag = true;
for(int i= 1;i<=n;i++) //因为图不一定是连通的,非连通图也可以是二分图
{
if(!color[i]) //如果没有染色
{
if(!dfs(i,1)) //染色后如果不成功,直接结束
{
flag = false;
break;
}
}
}
if(flag == false)
cout << "No" <<endl;
else
cout << "Yes"<<endl;
return 0;
}
257.关押罪犯
匈牙利算法
- 匹配:类似于一夫一妻制
//无向图,但是只需要左边指向右边的
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 510,M = 100010;
int ne[M],idx,e[M],h[N];
int match[N]; //match[j] j号女生匹配的男朋友
bool st[N]; //对所有妹子的状态
void add(int a ,int b)
{
e[idx] = b,ne[idx] = h[a],h[a] = idx++;
}
bool find(int x) //判断这个男生能否找到合适的妹子
{
for(int i=h[x] ; i!=-1 ;i = ne[i]) //枚举这个男生看上的妹子的集合
{
int j = e[i]; //用j表示
if(!st[j]) //如果男孩对这个女孩有意思
{
st[j] = true;
if(match[j] == 0 || find(match[j])) //如果这个女孩刚好单身或者 这个女生已经有男朋友了(而且这个男朋友有备胎)
{
match[j] = x;
return true;
}
}
}
return false; //实在找不到女朋友
}
int main()
{
int n1,n2,m;
cin >> n1 >>n2 >>m ;
memset(h,-1,sizeof h);
while(m--)
{
int a,b;
cin >> a >>b ;
add(a,b);
}
int res =0; //匹配数
for(int i=1;i<=n1;i++) //遍历所有的男生
{
memset(st,false ,sizeof st); //初始化,一个妹子也没考虑
if(find(i)) res ++; //男生成功的找到一个妹子
}
cout << res <<endl;
return 0;
}
小结
-
prim算法 和 dijkstra 思路差不多
- prim 算法 每次选择一个与当前集合(已选过点的集合)距离最近的顶点 ,作为下一个加入的点
- Dijktra 算法 每次选择 距离源点距离最近的顶点 ,作为下一个加入的点,加入后更新它可以到达的点的距离(与之前值取min)
-
prim 代码实现的核心在于
四、数学知识
1-质数
质数的判定
--------------试除法 O(sqrt(n))
bool flag = true;
for(int j=2; j<=i/j ; j++) //优化后的,只遍历到 j*j <=i
{
if( i % j == 0) //不是质数
{
flag = false;
break;
}
}
if(flag)
{
cout<< i << endl;
}
分解质因数 O(N *sqrt(N))
---------------试除法 由O(n) -> O( sqrt(n) )
把所有的因子都变为质因子
-
从小到大尝试n的因数
-
n中最多只包含一个大于 sqrt(n) 的质因子
void divide(int n)
{
for(int i=2; i<=n ;i++ )
if(n%i == 0) //能成立的i一定是质数,例如:在i=2时,经过下面不断除2,直到再也不能除2及其倍数,所以所有以2为因数的合数,就都不能成立进行下面的操作
//或者说,所以的合数都由比他们小的质数因子构成,在i遍历到那些质数因子时一顿除,早就使此合数不再为因数了 {
while(n%i == 0) //除了之后还能再除,只需输出多加1就好
{
n/=i;
s++;
}
printf("%d %d\n",i,s);
}
}
//************优化***************//
//根据n中最多只包含一个大于 sqrt(n) 的质因子
由O(n)->O(根号n)[其实准确地说是介于log n 与根号n之间]
for(int i=2; i<=n /i ;i++ ) //变为先只看小于根号n的,然后单独输出大于根号n的那一个
if(n%i == 0)
{
while(n%i == 0)
{
n/=i;
s++;
}
printf("%d %d\n",i,s);
}
if(n>1) //最后只要不被完全除成1,就是剩下的那个大于根号n的那个值
printf("%d %d",n,1);
筛质数
给定一个正整数 nn,请你求出 1∼n1∼n 中质数的个数。
原理:
先把数写到数表中,先把某个数的倍数删掉,筛掉之后剩下的就是质数,筛到 p-1 时如果 p 没被筛到,就说明是个质数(p不是 2~p-1 中间任何一个数的倍数,也就是说,不存在一个 2~p-1 的约数)
朴素筛法 O(nlogn)
int primes[N],cnt;
bool st[N];
void get_primes(int n )
{
for(int i =2;i<=n; i++) //从2到n枚举
{
if(!st[i]) //已经在 i 处了,如果当前这个数没被筛过(即st[i] == 0),就说明 2~ i-1 都没有它的因数,就说明它是质数
{
primes[ cnt++ ] = i; //把这个质数i存起来
}
for(int j=i + i; j<=n; j+=i) //把i的倍数筛掉
st[j] = true;
}
}
埃式筛法 O(nlog log n)
我们只需要把质数的倍数筛掉就行,不需要把合数的倍数在进行筛选,这样一来时间复杂度就更低了
求时间复杂度:
质数定理(1~n中有 n /(ln n) 个质数 )
int primes[N],cnt;
bool st[N];
void get_primes(int n )
{
for(int i =2;i<=n; i++) //从2到n枚举
{
if(!st[i]) //已经在 i 处了,如果当前这个数没被筛过(即st[i] == 0),就说明 2~ i-1 都没有它的因数,就说明它是质数
{
primes[ cnt++ ] = i; //把这个质数i存起来
for(int j=i;j<=n;j+=i) st[j]=true; //可以用质数就把所有的合数都筛掉;
}
}
}
- 埃氏筛法的缺陷 :对于一个合数,有可能被筛多次。例如 30 = 2 * 15 = 3 * 10 = 5*6……那么如何确保每个合数只被筛选一次呢?我们只要用它的最小质因子来筛选即可,这便是欧拉筛法。
线性筛法 O(n)
-
欧拉筛法的基本思想 :在埃氏筛法的基础上,让每个合数只被它的最小质因子筛选一次,以达到不重复的目的。
-
不是用 i 的倍数来消去合数,而是把 primes 里面纪录的素数,升序来当做要消去合数的最小素因子。
int primes[N],cnt;
bool st[N];
void get_primes(int n )
{
for(int i = 2; i<=n ; i++) //从2到n枚举
{
if(!st[i]) //已经在 i 处了,如果当前这个数没被筛过(即st[i] == 0),就说明 2~ i-1 都没有它的因数,就说明它是质数
primes[ cnt++ ] = i; //把这个质数i存起来
for(int j=0; primes[j]<=n/i ;j++) //下面的 primes[j]*i >= n 就没意义了
{
st[primes[j]*i] = true ; //不管i % primes[j] 等不等于0,都有primes[j] 是 i*primes[j] 的最小质因子
if(i % primes[j]==0) break ;
/*
当i是prime[j]的整数倍时,记 m = i / prime[j],那么 i * prime[j+1]
就可以变为 (m * prime[j+1]) * prime[j],这说明 i * prime[j+1] 是 prime[j] 的整数倍
不需要再进行标记(在之后会被 prime[j] * 某个数 标记)
对于 prime[j+2] 及之后的素数同理,直接跳出循环,这样就避免了重复标记。
*/
}
}
}
2-约数
试除法求约数
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
vector<int> get_divisors(int n)
{
vector<int> res;
for(int i= 1;i<= n/i ; i++) //别忘了 =
{
if( n%i == 0)
{
res.push_back(i) ;
if(i!= n/i) //防止出现 3*3 = 9 然后把3存两遍这种情况
{
res.push_back(n/i);
}
}
}
sort(res.begin(), res.end() );
return res;
}
int main()
{
int n;
cin >> n;
while(n--)
{
int x ;
cin >> x;
auto res = get_divisors(x);
for(auto t:res )
cout << t << ' ' ;
cout << endl;
}
return 0;
}
约数个数
//约数 -> 质因数的组合
#include<iostream>
#include<algorithm>
#include<unordered_map> //哈希表存,用数组其实也行,但是数组最大开1e7个,大数据存不下
using namespace std;
const int mod = 1e9+7;
int main()
{
int i,n;
long long res = 1;
cin >> n;
unordered_map<int,int> primes;
while(n--)
{
int x;
cin >> x;
//求质因数
for(int i=2;i<=x/i;i++)
{
while(x % i==0)
{
x /=i ;
primes[i] ++ ; //primes存了所有质因数的指数
}
}
if(x > 1) primes[x] ++ ;
}
for(auto t : primes ) res = res*(t.second + 1) % mod ;
cout << res ;
return 0;
}
约数之和
#include<iostream>
#include<algorithm>
#include<unordered_map>
using namespace std;
unordered_map<int ,int > primes;
const int mod = 1e9+7;
long long res = 1;
int main()
{
int n;
cin >> n;
while(n--)
{
int x ;
cin >> x;
for(int i=2;i<=x/i;i++)
{
while(x % i==0)
{
x /= i;
primes[i]++;
}
}
if(x >1) primes[x]++;
}
for(auto t : primes)
{
int p = t.first,a = t.second;
long long f= 1;
while(a--) f = (f*p+1) % mod;
res = res*f % mod; //约数之和(p1^0 + p1^1 + p1^2...)*(p2^0 + p2^1 + p2^2...)*()*()....()
}
cout << res ;
return 0;
}
最大公约数
一、基础知识
欧几里得算法的原理是 GCD递归定理
GCD递归定理:
对任意 非负整数 a 和 任意 整数 b,gcd(a,b) = gcd(b, a mod b)
为了证明这个定理,我们首先需要知道一下几个有关 gcd
的基本知识跟相关等式跟推论
1.1 基本知识:
- 公约数
**定义:**如果d|a
(d 整除 a)且d|b
,那么 d 是 a 与 b 的 公约数。
**性质:**如果d|a
且d|b
,那么d|(ax + by); x,y ∈ Z(任意整数)
- 最大公约数
定义:两个非零整数 a 和 b 的公约数里最大的就是 最大公约数。
1.2 相关等式跟推论:
- **等式 1:**如果
a|b 且 b|a
那么a = ±b
- **等式 2:**如果
d|a 且 d|b
那么d|(ax + by); x,y ∈ Z
- 等式 3:
a mod n = a - n⌊a/n⌋(向下整除); a∈Z,n∈N*(正整数)
- **推论 1:**对任意整数 a , b,如果
d|a 且 d|b
则d|gcd(a, b)
二、证明过程
如果我们想要获得结论gcd(a,b) = gcd(b, a mod b)
那么我们只需要证明gcd(a,b)|gcd(b, a mod b) 且 gcd(b,a mod b)|gcd(a,b)
就可以利用等式 1来证明他俩相等了。
2.1 证明 gcd(a,b)|gcd(b,a mod b)
设
d = gcd(a, b)
∴d|a 且 d|b
∵ 由 等式 3 可知:(a mod b) = a - qb
q = ⌊a/b⌋
∴a mod b
是 a 与 b 的线性组合
∴ 由 等式 2 可知 :d|(a mod b)
∵d|b 且 d|(a mod b)
∴ 由 推论 1 可知d|gcd(b, a mod b)
等价结论:gcd(a, b)|gcd(b, a mod b)
2.2 证明 gcd(b,a mod b)|gcd(a,b)
设
c = gcd(b, a mod b)
∴c|b 且 c|(a mod b)
∵a = qb + r
r = a mod b
q = ⌊a/b⌋
∴ a 是 b 和 (a mod b) 的线性组合
∴ 由 等式 2 可知:c|a
∵c|a 且 c|b
∴ 由 推论 1 可知:c|gcd(a, b)
等价结论:gcd(b, a mod b)|gcd(a, b)
s
2.3 证明 gcd(a,b) = gcd(b, a mod b)
由 上述两个结论 可知:
gcd(a, b)|gcd(b, a mod b)
gcd(b, a mod b)|gcd(a, b)
∴ 由 等式 1 可知:
gcd(a, b) = gcd(b, a mod b)
到这里 GCD递归定理 就证明结束了
欧几里得算法
辗转相除法:
a 和 b 的最大公约数 等于 b 和 (a mod b) 的最大公约数
证明:
int gcd(int x,int y){
if(y == 0) return x; // (y, x%y ) 当y = 0时,0是任何数的公约数,所以返回x
return gcd(y,x % y);
}
最小公倍数
a . b 的最大公倍数等于 a * b 除以 a 与 b 最大公因数 gcd ( a , b )
int gbs(int a , int b)
{
return a*b / gcd(a,b) ;
}
3-欧拉函数
欧拉函数
互质是公约数只有1的两个整数,叫做互质整数
证明:
用容斥原理
#include<iostream>
#include<algorithm>
using namespace std;
int main()
{
int n;
cin >> n;
int res ;
while(n--)
{
int x;
cin >> x;
res = x;
//分解质因子
for(int i=2;i<=x/i;i++) //到 i*i<=x 处
{
if(x % i==0)
{
res = res /i * (i-1); //先除i 再乘 (i-1),用来避免小数的产生,同时先除后乘可以避免溢出。
while(x % i==0) x /= i ;
}
}
if(x > 1) res = res/x * (x- 1);
cout << res <<endl;
}
return 0;
}
筛法求欧拉函数
- 质数的欧拉函数为 1
- 非质数的欧拉函数
- 当 ( i % pj == 0) φ (pj * i ) = pj * φ( i )
- 当 ( i % pj !=0) φ (pj * i ) = pj * φ( i ) * (1- 1/pj) = φ (i) * (pj -1)
//用线性筛法求质数的过程中求每个的欧拉函数(判断出质数就好办了,φ(i) = i-1 )
#include<iostream>
using namespace std;
typedef long long LL;
const int N = 1000010;
int primes[N],cnt;
bool st[N];
int phi[N]; //欧拉函数
LL res ;
int main()
{
int n ;
cin >> n;
phi[1] = 1;
for(int i=2; i<=n; i++)
{
if(!st[i])
{
primes[cnt++] = i;
phi[i] = i- 1;
}
for(int j=0;primes[j]<=n/i;j++)
{
st[primes[j]*i] = true;
if(i % primes[j]==0)
{
phi[i*primes[j]] = phi[i]*primes[j]; //当 ( i % pj == 0) φ (pj * i ) = pj * φ( i )
break;
}
phi[i*primes[j]] = phi[i]*(primes[j]-1);
//当 ( i % pj !=0) φ (pj * i ) = pj * φ( i ) * (1- 1/pj) = φ (i) * (pj -1)
}
}
for(int i=1;i<=n;i++)
{
res += phi[i];
}
cout << res <<endl;
return 0;
}
欧拉定理
- 注意,这两组值相同是指 mod n之后相等
费马定理
当 n 是质数的时候 ----> 费马定理
4-快速幂
-
编程竞赛有相当一部分题目的结果过于庞大,整数类型无法存储,往往只要求输出取模的结果。
-
例如(a+b)%p,若a+b的结果我们存储不了,再去取模,结果显然不对,我们为了防止溢出,可以先分别对a取模,b取模,再求和,输出的结果相同。
-
a mod b表示a除以b的余数。有下面的公式:
- (a + b) % p = (a%p + b%p) %p
- (a - b) % p = ((a%p - b%p) + p) %p
- (a * b) % p = (a%p)*(b%p) %p
-
注意对于除法取模,我们不能直接分别取模了,详见逆元。
//a^k mod p 的暴力做法:
int res = 1;
for(int i = 1; i<=k ; i++ )
res = res*a mod p;
//k 在 1e9次 下就会暴死
//而快速幂算法的时间复杂度为 O(log k) ,也就是 k=1e9 时,用30次左右就可算出来
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long LL;
int n;
LL res;
//a^b % p
int quick_mi(int a,int b,int p)
{
res =1;
while(b)
{
if(b & 1)
res = res * a % p; //最开始时a=a^(2^0),如果例:b=(11010101)2,不停的加
b = b >> 1; //把b的末位删掉
a = (LL)a * a % p; //每次平方使a^(2^0) -> a^(2^1) -> a^(2^2)......
}
return res % p;
}
int main()
{
scanf("%d",&n);
while(n--)
{
int a,b,p;
scanf("%d%d%d",&a,&b,&p);
cout << quick_mi(a,b,p) <<endl;
}
return 0;
}
快速幂求逆元
若整数 b,m 互质,并且对于任意的整数 a,如果满足 b|a,则存在一个整数 x,使得 a/b≡a*x(mod m),则称 x 为 b 的模 m 乘法逆元,记为 b^−1(mod m)。
b 存在乘法逆元的充要条件是 b 与模数 m 互质。当模数 m 为质数时,b^m−2 即为 b 的乘法逆元。
如果不满足 模数为质数 ,用扩展欧几里得算法 !!!!!!!!!
( x 叫做 b的模 m 的逆元)
由费马小定理可知,当n为质数时
b ^ (p - 1) ≡ 1 (mod p)
逆元 = b^(p-2)
- 若b 是 p 的倍数,b^(p-1) % p == 0 不等于1 ,所以一定无解
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long LL;
int n;
LL res;
int quick_mi(int a,int b,int p)
{
res =1;
while(b)
{
if(b & 1)
res = res * a % p; //最开始时a=a^(2^0),如果例:b=(11010101)2,不停的加
b = b >> 1; //把b的末位删掉
a = (LL)a%p * a%p % p; //每次平方使a^(2^0) -> a^(2^1) -> a^(2^2)......
}
return res % p;
}
int main()
{
scanf("%d",&n);
while(n--)
{
int a,b,p;
scanf("%d%d",&a,&p);
res = quick_mi(a,p-2,p);
if(a % p) cout << res <<endl;
else cout << "impossible" <<endl;
}
return 0;
}
5-扩展欧几里得算法
裴蜀定理
裴蜀定理(或贝祖定理)得名于法国数学家艾蒂安·裴蜀,说明了对任何整数a、b和它们的最大公约数d,关于未知数x和y的线性不定方程(称为裴蜀等式):若a,b是整数,且gcd(a,b)=d,那么对于任意的整数x,y,ax+by都一定是d的倍数,特别地,一定存在整数x,y(可正可负),使ax+by=d成立。
它的一个重要推论是:a,b互质的充分必要条件是存在整数x,y使ax+by=1.
扩展欧几里得算法
//就是找一个算法,来算对应a,b 的一对 x,y
x,y的推导公式
写法一:
/*写法一: 递归结束时有 x=y`,y=x`-[a/b]*y`*/
int d = exgcd(b,a%b,x,y);
int tmp = x;
x = y;
y = tmp - a/b*y;
写法二:
/*写法二:*/
int d = exgcd(b,a%b,y,x); //递归结束的时候有 b*y + a%b * x = gcd(a,b)-> x*a + ( y- a/b*x)*b = gcd(a,b)
y = y- a/b*x;
//在欧几里得算法 GCD递归定理 的过程中顺便计算出x,y;
#include<iostream>
#include<algorithm>
using namespace std;
int n;
int exgcd(int a,int b,int &x,int &y) //加上引用才能传回来
{
if(b==0)
{
x = 1,y =0; // 1*a + 0*0 = gcd(a,0) 0和任何x的最大公约数都等于x
return a;
}
/*写法一: 递归结束时有 递归结束时有 x=y`,y=x`-[a/b]*y
int d = exgcd(b,a%b,x,y);
int tmp = x;
x = y;
y = tmp - a/b*y;
*/
/*写法二:*/
int d = exgcd(b,a%b,y,x); //递归结束的时候有 b*y + a%b * x = gcd(a,b)-> x*a + ( y- a/b*x)*b = gcd(a,b)
y = y- a/b*x;
return d;
}
int main()
{
cin >> n;
while(n--)
{
int a,b,x,y;
cin >> a >>b ;
exgcd(a,b,x,y);
cout << x <<" "<<y<<endl;
}
return 0;
}
线性同余方程
#include<iostream>
#include<algorithm>
using namespace std;
int exgcd(int a,int b,int &x,int &y)
{
if(b==0)
{
x =1 ,y=0;
return a;
}
int d = exgcd(b,a%b,y,x);
y = y - a/b * x;
return d;
}
int main()
{
int n;
cin >> n;
while(n--)
{
int a,b,m,x,y;
cin >> a >> b>> m;
int d = exgcd(a,m,x,y);
if(b % d)
{
cout << "impossible" << endl;
}
else
cout << (long long)x*b/d % m << endl;
//ax % m=b⟺ a⋅(x % m) % m=b ax % m=b⟺ a·(x % m) % m=b
//所以对 x % m仍是个答案
//因为输出要在int范围,所以%m
}
return 0;
}
6-中国剩余定理
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long LL;
LL exgcd(LL a,LL b,LL &x,LL &y)
{
if(b==0)
{
x = 1,y =0;
return a;
}
LL d = exgcd(b,a%b,y,x);
y = y-a/b*x;
return d;
}
int main()
{
int n;
cin >> n;
//第一个的时候,一个构不成欧几里得的算式
LL a1,m1;
cin >> a1 >>m1;
bool has_answer = true;
for(int i=0;i<n-1;i++)
{
LL a2,m2;
cin >> a2 >> m2 ;
LL k1,k2;
LL d = exgcd(a1,a2,k1,k2);
if((m2-m1)%d)
{
has_answer = false;
break;
}
k1 = k1*(m2-m1)/d;
LL t = a2/d;
k1 = (k1 % t + t)%t;
m1 = a1*k1 +m1;
a1 = a1*a2/d; //先更新m1再更新a1
}
if(has_answer)
cout << (m1 % a1 + a1) % a1; //既然求最小的x,那么就是正的m1
else
cout << -1 <<endl;
return 0;
}
7-高斯消元
高斯消元解线性方程组
化为最简阶梯型矩阵
初等行变换(不改变下线性方程组的解的结构):
- 某一行乘一个非零的数
- 交换某两行
- 把某行的若干倍加到另一行上去
步骤:
#include<iostream>
#include<algorithm>
#include<cmath>
using namespace std;
const int N =110;
const double eps = 1e-6;
double a[N][N];
int n;
int r; //行
int c; //列
void out()
{
for(int i=0;i<n;i++)
{
for(int j=0;j<=n;j++)
{
cout << a[i][j] <<" ";
}
puts("");
}
}
int gauss()
{
for(c=0,r=0;c<n;c++) //遍历每一列
{
/*第一步:先找这一列中最大值的行*/
int t = r; //先存下当前行
for(int i=r;i<n;i++)
if(fabs(a[i][c]) > fabs(a[t][c]))
t = i;
if(fabs(a[t][c])<eps)
continue;
/*第二步:把这一行与第一行交换*/
for(int i=0;i<=n;i++)
swap(a[r][i],a[t][i]);
/*第三步:把这一行变成第一个非零数为1*/
for(int i=n;i>=c;i--)
{
a[r][i] = a[r][i]/a[r][c];
}
/*第四步:用这一行化简下面的所有行*/
for(int i=r+1;i<n;i++)
if (fabs(a[i][c]) > eps)
for(int j=n;j>=c;j--)
a[i][j] = a[i][j] - a[i][c]*a[r][j];
r++;
}
if(r<n)
{
for(int i=r;i<n;i++)
if(fabs(a[i][n])>eps)
return 2;
return 1;
}
/*如果有唯一解,挨个算出来*/
for(int i=n-1;i>=0;i--) //从最后一行往上
for(int j=n-1;j>i;j--)
a[i][n] = a[i][n] - a[i][j] * a[j][n];
return 0;
}
int main()
{
cin >> n;
for(int i=0;i<n;i++)
for(int j=0;j<=n;j++)
cin >> a[i][j];
int t = gauss();
if(t==0)
{
for(int i=0;i<n;i++)
if(fabs(a[i][n])<eps)
cout << "0.00"<<endl;
else
printf("%.2lf\n",a[i][n]);
}
else if(t==1) cout << "Infinite group solutions"<<endl;
else cout << "No solution" << endl;
return 0;
}
高斯消元解异或线性方程组
- 1.消成上三角矩阵
- 枚举列
- 找非零行
- 将非零行换到最上面去
- 用非零行消零
- 2.判断
- 完美 - 唯一解
- 有矛盾 - 无解
- 无矛盾 - 无穷解
8-求组合数
递推法求组合数:O(n^2)
1.当n,m都很小的时候可以利用杨辉三角直接求。
C(n,m)=C(n-1,m)+C(n-1,m-1);
逆元 + 快速幂
// 逆元 + 快速幂
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long LL;
const int N = 1e5+10;
LL fact[N];
int qmi(int a,int b,int p)
{
int res = 1;
while(b)
{
if(b & 1) res = (LL)res *a % p; //(LL)要加
b >>= 1;
a = (LL)a * a % p;
}
return res ;
}
int C(LL a,LL b,int p)
{
if(a<b) return 0;
LL res = 1;
res = (LL)fact[a]*qmi(fact[a-b],p-2,p)*qmi(fact[b],p-2,p) % p;
return res;
}
LL lucas(LL a,LL b,int p)
{
return b==0 ? 1 : lucas(a/p,b/p,p) % p *C(a % p, b% p ,p) % p;
}
int main()
{
int n ;
cin >> n;
while(n--)
{
LL a,b;
int p;
cin >> a >> b>> p;
fact[0] = 1;
for(int i=1; i<N ;i++)
fact[i] = (LL)fact[i-1]*i % p;
cout << lucas(a,b,p)<<endl;
}
return 0;
}
应用
卡特兰数
满足条件的01序列
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long LL;
const int p = 1e9+7;
int qmi(int a,int b,int p)
{
int res = 1;
while(b)
{
if(b & 1) res = (LL)res*a % p;
b >>= 1;
a = (LL)a*a % p;
}
return res;
}
int main()
{
int n ;
int res = 1;
cin >> n;
int a = 2*n,b = n;
for(int i =a;i>a-b;i--) res = (LL)res*i % p;
for(int i =1;i<=b;i++) res = (LL)res*qmi(i,p-2,p) % p;
res = (LL)res* qmi(n+1,p-2,p) % p;
cout << res ;
return 0;
}
火车进站问题
9-容斥原理
韦恩图
10-博弈论
五、动态规划
- 递归的思想,迭代的实现
- 1.化零为整
- 2.化整为零
0. 背包问题
- 其实是一种组合模型 ( 选择模型 )
- 在某个限制下,选若干物品,使结果最好
1. 01背包问题
1.1 01背包基础 (占不大于背包容量)
- 在一开始暴力枚举每件物品放或者不放入背包时,其实忽略了一个特性:
- 第 i 件物品放或不放而产生的最大值是完全可以由前面 i-1 件物品的最大值来决定的,而暴力做法无视了这一点。
- 其实可以用动态规划维护 f[ i ] [ v ] (从1~i 中选,体积不大于 v 的最大价值)
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N =1010;
int f[N];
int v[N],w[N];
int n,m;
int main()
{
cin >> n>> m;
for(int i=1;i<=n;i++)
cin >> v[i] >> w[i];
for(int i=1;i<=n;i++)
for(int j=m;j>=1;j--)
{
// 当前背包容量装不进第i个物品,则价值等于前i-1个物品
if(j < v[i])
f[i][j] = f[i - 1][j]; //一维情况下,自动保存了呗
// 能装,需进行决策是否选择第i个物品
else
f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]);
}
int res = 0;
cout << f[m];
return 0;
}
//空间优化,成1维
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N =1010;
int f[N];
int v[N],w[N];
int n,m;
int main()
{
cin >> n>> m;
for(int i=1;i<=n;i++)
cin >> v[i] >> w[i];
for(int i=1;i<=n;i++)
for(int j=m;j>=1;j--)
{
if(j>=v[i]) //当 j >= v[i],才有减去一些空间 让第i个进来的可能
{
f[j] = max(f[j] , f[j-v[i] ] + w[i] );
}
}
int res = 0;
cout << f[m];
return 0;
}
1.2 01背包满包 (刚好占满背包)
凑零钱
#include<bits/stdc++.h>
#define inf 0x3f3f3f3f
using namespace std;
typedef long long ll;
const int maxn=1e4+10;
int dp[111],ans[maxn],v[maxn],pre[maxn];
void print(int u){
if(!pre[u]){
printf("%d",ans[u]);return ;
}
print(pre[u]);
printf(" %d",ans[u]);
}
int main()
{
int n,m;
scanf("%d %d",&n,&m);
for(int i=1;i<=n;i++)
scanf("%d",&v[i]);
memset(dp,-inf,sizeof(dp));
memset(pre,-1,sizeof(pre));
dp[0]=0;
sort(v+1,v+n+1);
for(int i=1;i<=n;i++)
{
for(int j=m;j>=v[i];j--)
{
if(dp[j]<=dp[j-v[i]]+1) // +1 因为把所有的 w[i] 都设为1 了
{
dp[j]=dp[j-v[i]]+1;
ans[j]=v[i]; // 在 为了到 j 这个价钱 加入的钱数
pre[j]=j-v[i]; // 没加这个 v[i] 钱时,原来的钱数 ,由上面的递归可知,这个值为零时打印最小那个钱数
}
}
}
//cout<<dp[m]<<endl;
if(dp[m]>0)
{
print(m);
}
else
cout<<"No Solution"<<endl;
return 0;
}
1.3 背包容量特殊要求 (价值为K的倍数)
1047.糖果
/* s[i][j] 从 前 i 个糖果包中选,模K取余为 j = 0~100 的糖果总数
s[i][j] = max(s[i-1][j] , s[i-1][((j - w)%k + k) % k ] + w) ;
*/
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 110;
int s[N][N];
int a[N];
int n , k ;
int main()
{
cin >> n >> k;
for(int i = 1; i<=n ; i++)
cin >> a[i];
memset(s,-0x3f,sizeof s); // 只有一个合法方案 s[0][0] 其他的s[0][i]都不合理
s[0][0] = 0;
for(int i = 1;i<=n;i++)
{
int w = a[i];
for(int j = 0;j < k ; j++)
{
s[i][j] = max(s[i-1][j] , s[i-1][((j - w)%k + k) % k ] + w);
}
}
if(s[n][0] == -1)
cout << 0 << endl;
else
cout << s[n][0] << endl;
return 0;
}
2.完全背包问题
每一个物品的个数是无限个
首先推导状态转移方程:
比较 01背包与完全背包
- 在二维上差别好理解:
- 在一维上差别就是遍历的顺序 :
- 01 背包是从 m -> 1 枚举
- 而完全背包则是从 1 -> m 枚举(正向枚举),因为上面的 f[ i ] [ j - v ] +w 而不是以前01背包的 f[ i-1 ] [ j - v ] +w
//空间优化,成1维
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N =1010;
int f[N];
int v[N],w[N];
int n,m;
int main()
{
cin >> n>> m;
for(int i=1;i<=n;i++)
cin >> v[i] >> w[i];
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
if(j>=v[i]) //当 j >= v[i],才有减去一些空间 让第i个进来的可能
{
f[j] = max(f[j] , f[j-v[i] ] + w[i] );
}
}
int res = 0;
cout << f[m];
return 0;
}
3.多重背包问题
每一个物品的个数是有限个s[ i ] ,
// n*n*n 三次方的时间复杂度 100*100*100 = 1000,000
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 110;
int f[N];
int n,m;
int s[N],w[N],v[N];
int main()
{
cin >> n >> m;
for(int i=1;i<=n;i++)
{
cin >> v[i] >> w[i] >> s[i];
}
for(int i=1 ;i<=n;i++)
for(int j=m;j>=1;j--)
for(int k=1; k<=s[i] ;k++ ) // s个 && k*v[i]<= j
{
if(k*v[i]<= j)
f[j] = max(f[j] , f[ j-k*v[i] ] + k* w[i] );
}
cout << f[m] <<endl;
return 0;
}
// 数据量为1000的话,三次方肯定10^9不行,把s个背包都变成单个 1*v[i] ,2*v[i].3*v[i]......
// 多重背包最终优化为01背包,01背包可以优化为一维
// 拆的话,聪明的做法是用二进制方法
// 用1,2,4,8....... 代替往里面插入 n 个1(就是for(int k=1;k<=n;k++) 这种)
#include<iostream>
#include<algorithm>
const int N = 25000;
using namespace std;
int n,m;
int s[N],v[N],w[N];
int f[N]; //多重背包最终优化为01背包,01背包可以优化为一维
int main()
{
cin >> n >> m;
int cnt = 0 ; //表示被拆出来的物品的编号
for(int i=0;i<=n;i++)
{
int a,b,s; //读入每个数据的体积,价值,个数
cin >> a>>b>>s ;
//接下来要把每个物品打包成1,2,3......s个一份
int k = 1;
//例如:156 -1-2-4-8-16-32-64 = 29
while( k <= s) //64<=93继续,当128>29时就不能在分了,剩下一个29
{
cnt ++ ; //新生成的物品加一
v[cnt] = a * k;
w[cnt] = b * k;
s -= k; //先这一步,我们已经算好k了
k *= 2; // 每次1,2,4,8,16
}
if(s>0) // 把剩下的29 纳入新物品中
{
cnt ++;
v[cnt] = a * s;
w[cnt] = b * s;
}
}
n = cnt ; //原来的n个元素变成了cnt个元素
//然后就是01背包问题
for(int i=1;i<=n;i++)
for(int j=m;j>=1;j--)
if(j>=v[i])
f[j] = max(f[j],f[j-v[i]]+w[i]);
cout << f[m] <<endl;
}
4.分组背包问题
每组物品有若干个,同一组内的物品最多只能选一个
其实多重背包问题就是分组背包问题的一种特殊情况
然后完全背包又是多重背包问题的一种特殊情况
-
由于他的普遍性,导致只能三重循环的方式进行
-
分组背包问题是 枚举第 i 组物品选哪个
状态表示:只从前 i 组物品中选 ,且总体积不大于 j 的所有的选法
/*就是把 01 背包问题 新纳入一个物品(在这里变成了新纳入一组)
*由只判断 f[i-1][j] 与 f[i-1][j-v[i]] + w[i] 的大小,变为判断
*f[i-1][j] 分别与 f[i-1][j-v[i][k]] (0<k<=s[i]) 共s[i]个的大小
*/
//优化方式与01背包方式相同
#include<iostream>
#include<algorithm>
using namespace std;
const int N =110;
int n,m;
int f[N],s[N];
int v[N][N],w[N][N];
int main()
{
cin >> n >>m;
for(int i = 1;i<=n ;i++)
{
cin >> s[i] ;
for(int j=0;j<s[i];j++)
cin >> v[i][j] >>w[i][j];
}
for(int i=1;i<=n;i++) //从前往后遍历每一组
for(int j=m;j>=0;j--) //从大到小枚举
for(int k=0;k<s[i];k++)
if(j>=v[i][k])
f[j] = max(f[j],f[j-v[i][k]] + w[i][k]);
cout << f[m]<<endl;
return 0;
}
5.线性DP
所谓线性dp,就是指我们的递归方程有一个明显的线性关系的,有可能是一维线性的,也可能是二维线性的.
(1).数字三角形
- 注意初始化的时候要多初始化一个
也可以从下往上走,到最后就不用 for(i=1;i<=n;i++) res = max(res,f [ n] [ i ] )
(2).最长上升子序列
给定一个长度为 N的数列,求数值严格单调递增的子序列的长度最长是多少
以倒数第二个数为划分依据, if ( a[i] > a[j] )
集合:所有以第i个数结尾的上升子序列
状态转移:
#include<iostream>
#include<algorithm>
using namespace std;
const int N= 1010;
int n;
int a[N];
int f[N];
int main()
{
cin >> n;
for(int i =1;i<=n;i++)
cin >> a[i] ;
for(int i=1;i<=n;i++)
{
f[i] = 1;
for(int j=1;j<=i;j++)
if(a[j] < a[i]) //以倒数第二个数为划分依据
f[i] = max(f[i],f[j]+1);
}
int res ;
for(int i=1;i<=n;i++)
res = max(res,f[i]);
cout << res;
return 0;
}
- 用数组记录,二分查找,N*logN
(3).最长公共子序列
状态表示:
所有在第一个子序列的前 i 个字母中出现,且在第二个序列的前 j 个字母中出现的子序列 ----> f[ i , j ]
f[ i -1,j -1 ] 被f[ i -1 ,j ] 取代了
(4).最短编辑距离
状态表示与状态计算:
无优化: 时间复杂度 O(n²) 空间复杂度 O(n²)
图解动态规划求最短编辑距离过程
在写代码之前,为了让读者对动态规划有一个直观的感受,笔者以表格的形式,列出动态规划是如何一步步地工作的。
下面以字符串 a[ ] = “axyzc” 和 b[ ] = “xyzab” 为例
- 用 a[ ] = abcde , b[ ] = abde 来说明DP真的会只删掉一个 c
参考资料:
(188条消息) 经典动态规划问题:最短编辑距离算法的原理及实现_程序员的自我反思的博客-CSDN博客_最短编辑距离
6.区间DP
状态表示:
状态计算:
模板:
区间 DP 常用模版
所有的区间dp问题枚举时,第一维通常是枚举区间长度,并且一般 len = 1 时用来初始化,枚举从 len = 2 开始;第二维枚举起点 i (右端点 j 自动获得,j = i + len - 1)
模板代码如下:
for (int len = 1; len <= n; len++) { // 区间长度
for (int i = 1; i + len - 1 <= n; i++) { // 枚举起点
int j = i + len - 1; // 区间终点
if (len == 1) {
dp[i][j] = 初始值
continue;
}
for (int k = i; k < j; k++) { // 枚举分割点,构造状态转移方程
dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j] + w[i][j]);
}
}
}
282.石子合并
/*DP问题的思维与最小生成树等相似,都是从最小的单元开始,
一个个满足需要,最后总体就满足需要了;
区间DP用len和左端点i构成拆分区间集合的方式,二维,加上对于每个集合,还要用k做分割,
找到这个区间的消耗最小值 ,值得注意的是 ,每个分割的小区间集合的消耗min都在前面算出来了f[i][j]
*/
#include<iostream>
using namespace std;
const int N = 310;
int n,m;
int f[N][N]; //f[i][j] 即区间 i~j 的最小
int s[N];
int main()
{
cin >> n ;
for(int i = 1;i<=n;i++) cin >> s[i], s[i]+= s[i-1]; //前缀和
for(int len= 2;len <= n ;len++)
for(int i=1 ;i+len -1 <=n ;i++) //i+len-1 是右端点的位置!
{
int j = i + len -1 ; //右端点的位置
f[i][j] = 1e8; //先把f[i][j]设置成一个比较大的数,不然出来后都是0
for(int k=i;k<j;k++)
f[i][j] = min(f[i][j],f[i][k] + f[k+1][j] + s[j]-s[i-1]);
}
cout << f[1][n] <<endl;
return 0;
}
1222.密码脱落 (回文子串)
- 先找最长回文子串
- n - 最长回文子串长度
// 回文串就是比较的边界上两个端点是否相同。
#include <cstdio>
#include <string.h>
const int N = 1010;
char s[N];
int f[N][N];
int main()
{
scanf("%s", s);
int n = strlen(s);
for (int len = 1; len <= n; len ++ )
for (int l = 0; l + len - 1 < n; l ++ )
{
int r = l + len - 1;
if (len == 1) f[l][r] = 1;
else
{
if (s[l] == s[r]) f[l][r] = f[l + 1][r - 1] + 2;
if (f[l][r - 1] > f[l][r]) f[l][r] = f[l][r - 1];
if (f[l + 1][r] > f[l][r]) f[l][r] = f[l + 1][r];
}
}
printf("%d\n", n - f[0][n - 1]);
return 0;
}
作者:yxc
链接:https://www.acwing.com/activity/content/code/content/204856/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
1070.括号配对
另一种划分方式
#include <iostream>
#include <cstring>
using namespace std;
const int N = 1010;
int f[N][N];//f[l][r]:表示将s[l~r]变成镜像串的最小操作数
/*
状态转移方程:
①s[l] == s[r],此时可以直接从f[l + 1][r - 1]转移而来,并且操作数不用变
②s[l] != s[r],此时可以选择从f[l + 1][r] 或 f[l][r - 1]转移而来,并且操作数+1
*/
string s;
int main()
{
cin >> s;
int n = s.size();
memset(f , 0x3f , sizeof f);
for(int len = 0 ; len <= n ; len++)
for(int l = 0 ; l + len - 1 < n ; l++)
{
int r = l + len - 1;
if(len <= 1) f[l][r] = 0;//长度为1或0就是镜像串
else
{
if(s[l] == s[r]) f[l][r] = min(f[l][r] , f[l + 1][r - 1]);
else f[l][r] = min(f[l][r] , min(f[l + 1][r] , f[l][r - 1]) + 1);
}
}
cout << f[0][n - 1] << endl;
}
作者:Anoxia_3
链接:https://www.acwing.com/solution/content/37925/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
7.计数类DP
整数划分
思路:把1,2,3, … n分别看做n个物体的体积,这n个物体均无使用次数限制,问恰好能装满总体积为n的背包的总方案数(完全背包问题变形)
方法一:
-
状态表示:f[ i ] [ j ] 表示前 i 个整数(1,2,3…)恰好拼成 j 的方案数
-
状态计算:
-
属性:数量(方案数)
朴素算法
#include<iostream>
#include<algorithm>
using namespace std;
const int N=1010,M = 1e9+7;
int n;
int f[N][N];
int main()
{
cin >> n;
//初始化
for(int i=0;i<=n;i++) f[i][0] = 1 ;
for(int i=1;i<=n;i++) f[0][i] = 0 ; //全局变量本来就是0,可以省略
for(int i=1;i<=n; i++)
for(int j=0; j<=n; j++)
{
/*使用朴素算法时必须先赋一遍f[i-1][j] ,
*防止f[i][j](i > j 时)没有值
*而用优化空间做法则不会出现这种情况
*(注意优化空间做法顺序:倒xu)
*/
f[i][j] = f[i-1][j] % M;
if(j>=i)
f[i][j] = (f[i-1][j] + f[i][j-i]) % M;
}
cout << f[n][n] <<endl;
return 0;
}
空间优化
#include<iostream>
#include<algorithm>
using namespace std;
const int N=1010,M = 1e9+7;
int n;
int f[N];
int main()
{
cin >> n;
f[0] = 1;
for(int i=1; i<=n; i++) //从 1 开始选
for(int j=i;j <=n ; j++ )
f[j] = (f[j] + f[j-i] ) % M ;
cout << f[n] << endl;
return 0;
}
方法二:
状态表示:
-
所有总和是 i ,并且恰好表示成 j 个数的和的方案
-
属性:数量
状态计算:
-
f [ i ] [ j ] = f [ i-1 ] [ j-1 ] + f [ i-j ] [ j ]
-
答案需要枚举
// 最小值是1时,因为都有一个 1 ,所有的方案都删掉这个最小值 1 ,所以等价为总和为 i-1 ,个数为 j-1 ,即 f [ i-1 ] [ j-1 ]
// 最小值严格大于 1 ,所有方案中的每个数都减去1 ,相当于总和减去 j,等价为总和为 i-j ,个数还是 i ,即 f [ i-j ] [ j ]
#include<iostream>
#include<algorithm>
using namespace std;
const int N =1010,mod = 1e9+7;
int n;
int f[N][N];
int main()
{
cin >> n;
/*初始化*/
f[0][0] = 1; //总和为0,恰好是0个数的和,方案数为1,其余的那些 f[0][i],f[i][0] (i>=1) 默认是0
for(int i =1; i<=n ; i++)
for(int j=1;j<=i;j++) //总和为 i 的数最多表示成 i个数的和
f[i][j] = (f[i-1][j-1] + f[i-j][j]) % mod;
int res = 0;
for(int i=1;i<=n;i++)
res = (res + f[n][i]) % mod ;
cout << res <<endl;
return 0;
}
8.数位统计DP
分情况讨论
- 计数问题
用 前缀和的思想 :
//例如abcdefg 求1在第四位 d 上出现的次数
#include<iostream>
#include<algorithm>
#include<cstring>
#include<vector>
using namespace std;
int n;
/*求 (1) */
int get(vector<int>num , int l,int r)
{
int res = 0;
for(int i = l; i >= r ; i--) //所求位的前几位
res = res*10 + num[i] ;
return res;
}
int power10(int x) //pow(10,x)
{
int res = 1;
while(x--)
res *= 10;
return res;
}
int count(int n,int x) //从 1~n 中统计一下 x 出现的次数
{
if(!n) return 0;
vector<int> num ; //倒序把每位数放在数组中
while(n)
{
num.push_back(n % 10);
n = n/10;
}
n = num.size();
int res = 0;
for(int i=n-1-!x; i>=0 ; i--) //从高往低位遍历,当x=0时,从第二位开始遍历,例如0999不合法
{
/*第一类只有i小于n-1时才存在*/
if(i<n-1)
{
res += get(num,n-1,i+1)*power10(i) ; // (000~abc-1)-> abc * {size(edf) == 1000 }
if(!x)
res -= power10(i) ; //当x==0时,abc必须不能等于000,上述的000~abc-1变为001~abc-1
}
/*第二类*/
if(num[i] == x) res+=get(num,i-1,0) +1 ; // edf + 1
else if(num[i] > x) res+= power10(i) ;
}
return res;
}
int main()
{
int a,b;
while(cin >> a >> b, a||b ) //a,b为 0 0 时停止
{
if(a>b) swap(a,b);
for(int i=0 ;i<10 ;i++)
cout << count(b,i) - count(a-1,i) <<' ';
cout << endl;
}
return 0;
}
# include <iostream>
# include <cmath>
using namespace std;
int dgt(int n) // 计算整数n有多少位
{
int res = 0;
while (n) ++ res, n /= 10;
return res;
}
int cnt(int n, int i) // 计算从1到n的整数中数字i出现多少次
{
int res = 0, d = dgt(n);
for (int j = 1; j <= d; j ++) // 从右到左第j位上数字i出现多少次,所有位上的次数加起来就是i出现的总次数
{
// l和r是第j位左边和右边的整数 (视频中的abc和efg); dj是第j位的数字
int p = pow(10, j - 1), l = n / p / 10, r = n % p, dj = n / p % 10;
// 计算第j位左边的整数小于l (视频中xxx = 000 ~ abc - 1)的情况
if (i) res += l * p;
if (!i && l) res += (l - 1) * p; // 如果i = 0, 左边高位不能全为0(视频中xxx = 001 ~ abc - 1),并且&&l表示这时i也不能在最高位出现。
// 计算第j位左边的整数等于l (视频中xxx = abc)的情况
if ( (dj > i) && (i || l) ) res += p; //(i || l)表示i=0时,i不能出现在最高位(即l不能为0),因为这种数是不存在的
if ( (dj == i) && (i || l) ) res += r + 1; //(i || l)表示i=0时,i不能出现在最高位(即l不能为0),因为这种数是不存在的
}
return res;
}
int main()
{
int a, b;
while (cin >> a >> b , a)
{
if (a > b) swap(a, b);
for (int i = 0; i <= 9; ++ i) cout << cnt(b, i) - cnt(a - 1, i) << ' ';
cout << endl;
}
return 0;
}
作者:Alicia编程果果
链接:https://www.acwing.com/solution/content/7128/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
9.状态压缩DP
蒙德里安的梦想
旅行商问题
国际象棋
DFS
10.树形DP
递归的特殊实现方式
没有上司的舞会
/* 用邻接表存图 */
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N = 6010;
int happy[N];
int f[N][2];
int n;
bool has_father[N]; //当为falis即没有父节点,则为根节点
int h[N],e[N],ne[N],idx; //存储邻接表
void add(int a,int b)
{
e[idx] = b;
ne[idx] = h[a]; //头插入 a 的子节点中
h[a] = idx;
idx++;
}
void dfs(int u)
{
f[u][0] = 0; //以u为根节点,最坏情况,一个都不选
f[u][1] = happy[u]; //以u为根节点,最坏情况,只选一个u,则为happy[u]
for(int i=h[u] ;i!=-1;i = ne[i])
{
int j = e[i];
dfs(j); //直接深搜到树的最下面,最后的那个子节点再调用时发现h[u]是-1(因为它没有子节点)
f[u][0] += max(f[j][0] , f[j][1] );
f[u][1] += f[j][0] ;
}
}
int main()
{
scanf("%d",&n);
for(int i=1; i<=n; i++)
scanf("%d",&happy[i]) ;
memset(h, -1 ,sizeof h ); // 如果访问到h[a]=-1 表示没有子节点
for(int i=0; i < n-1 ;i++) //父子关系(边的条数是节点的个数-1)
{
int a,b;
scanf("%d%d",&a,&b); //b是a的父节点
has_father[a] = true ;
add(b,a);
}
int root = 1;
while(has_father[root]) root++; //找到根节点
dfs(root);
printf("%d\n",max(f[root][0],f[root][1]) );
return 0;
}
1220.生命之树
11.记忆化搜索
递归的实现方式(可用 dfs 搜索)
滑雪
使用记忆化数组 记录每个点的最大滑动长度
遍历每个点作为起点
然后检测该点四周的点 如果可以滑动到其他的点
那么该点的最大滑动长度 就是其他可以滑到的点的滑动长度+1
dp[i][j] = max(dp[i][j-1], dp[i][j+1],dp[i-1][j],dp[i+1][j])
由于滑雪是必须滑到比当前低的点 所以不会存在一个点多次进入的问题
如果该点的 dp[][] 不为初始化值 那么就说明计算过 不必再次计算。
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 310;
int n, m;
int g[N][N]; //高度
int f[N][N]; //f[i][j] 以(i,j)为起点的最大滑动长度
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
int dp(int x, int y)
{
int &v = f[x][y];
if (v != -1) return v;
v = 1;
for (int i = 0; i < 4; i ++ )
{
int a = x + dx[i], b = y + dy[i];
if (a >= 1 && a <= n && b >= 1 && b <= m && g[x][y] > g[a][b])
v = max(v, dp(a, b) + 1);
}
return v;
}
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= m; j ++ )
scanf("%d", &g[i][j]);
memset(f, -1, sizeof f);
int res = 0;
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= m; j ++ )
res = max(res, dp(i, j));
printf("%d\n", res);
return 0;
}
作者:yxc
链接:https://www.acwing.com/activity/content/code/content/64186/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
-----提高课-----
12.数字三角形模型
摘花生
https://www.acwing.com/video/624/
传纸条
https://www.acwing.com/problem/content/description/277/
13.最长上升子序列模型
合唱队形
https://www.acwing.com/problem/content/description/484/
导弹防御系统
https://www.acwing.com/problem/content/189/
最长公共上升子序列
https://www.acwing.com/problem/content/274/
14.背包模型
采药
https://www.acwing.com/problem/content/description/425/
数字组合
https://www.acwing.com/problem/content/280/
六、贪心
1.区间问题
(1)区间选点
905.区间选点
- 如果这个点不在下个线段中 , 就去选下个线段的右端点 ,因为更可能去覆盖后面的点
// 结构体排序 ---- 1 ---- 用cmp
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 100010;
int n;
struct Range
{
int l,r;
}range[N];
bool cmp(Range a, Range b) //把右端点从小到大排序 不能用左端点排序,画画图
{
return a.r < b.r;
}
int main()
{
scanf("%d",&n);
for(int i =0;i<n;i++) //读入区间
{
int l,r;
scanf("%d%d",&range[i].l,&range[i].r);
}
sort(range,range + n,cmp);
int res =0; //需要的点的个数,初始0
int ed = -2e9; //上一个点一个都没选,赋为负无穷
for(int i=0; i<n ; i++)
if(range[i].l > ed ) //如果这个区间严格大于ed,则需要选择一个新的点
{
res++;
ed = range[i].r;
}
printf("%d\n",res);
return 0;
}
// 结构体排序 ---- 2 ---- 重载小于号运算符
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 100010;
int n;
struct Range
{
int l,r;
bool operator < (const Range &W )const //因为要排序,重载一下小于号
{
return r < W.r; //按照右端点排序
}
}range[N];
int main()
{
scanf("%d",&n);
for(int i =0;i<n;i++) //读入区间
{
int l,r;
scanf("%d%d",&range[i].l,&range[i].r);
}
sort(range,range + n); //结构体排序,重载小于号
int res =0; //需要的点的个数,初始0
int ed = -2e9; //上一个点一个都没选,赋为负无穷
for(int i=0; i<n ; i++)
if(range[i].l > ed ) //如果这个区间严格大于ed,则需要选择一个新的点
{
res++;
ed = range[i].r;
}
printf("%d\n",res);
return 0;
}
112.雷达设备
(2)最大不相交区间数量
- 将右端点从小到大排序
- ed 初始为无穷小 ,res 记录ed更新的次数
- 遍历区间,当某个区间的左端点小于 ed 时,跳过
- 当某个区间的左端点大于 ed 时,则更新ed 为这个区间的右端点值
// 结构体排序 ---- 2 ---- 重载小于号运算符
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 100010;
int n;
struct Range
{
int l,r;
bool operator < (const Range &W )const //因为要排序,重载一下小于号
{
return r < W.r; //按照右端点排序
}
}range[N];
int main()
{
scanf("%d",&n);
for(int i =0;i<n;i++) //读入区间
{
int l,r;
scanf("%d%d",&range[i].l,&range[i].r);
}
sort(range,range + n); //结构体排序,重载小于号
int res =0; //需要的点的个数,初始0
int ed = -2e9; //上一个点一个都没选,赋为负无穷
for(int i=0; i<n ; i++)
if(range[i].l > ed ) //如果这个区间严格大于ed,则需要选择一个新的点
{
res++;
ed = range[i].r;
}
printf("%d\n",res);
return 0;
}
(3)区间分组
有若干个活动,第i个活动开始时间和结束时间是 [ S i , f i ] , 同一个教室安排的活动之间不能交叠,求要安排所有活动,少需要几个教室?
- 将所有区间按照左端点从小到大排序
- 从前往后处理每个区间
- 判断能否将其放入某个现有的组中( L[ i ] > Min_r (组中右端点的最小值) )
- 1.如果不存在这样的组,则开一个新的组,再放入
- 2.如果存在这样的组,随便挑一个将它放入,并更新当前组的 Min_r
- 判断能否将其放入某个现有的组中( L[ i ] > Min_r (组中右端点的最小值) )
证明:
ans == cnt // ans表示最终答案 , cnt 表示按照这样的算法得到的 (是一种合法方案)
因为 ans 是所有合法方案里的最小值 ,所以 ans <= cnt
/*
empty():如果队列为空的话,返回true。
top():访问优先级队列中第一个元素的引用。
- 若是小根堆排序,则 9(rear)、7、5、4、2、1(top/front)
- 若是大根堆排序,则 1(rear)、2、4、5、7、9(top/front)
push() :插入元素到队尾 (并排序)
pop():移除队列的最顶层元素top/front。该函数仅用于删除元素。
*/
#include<iostream>
#include<algorithm>
#include<queue>
using namespace std;
const int N = 100010;
int n;
struct Range
{
int l,r;
}range[N];
int cmp( Range a, Range b) //左端点从小到大排序
{
return (a.l < b.l) ;
}
int main()
{
scanf("%d",&n);
for(int i=0;i<n;i++)
scanf("%d%d",&range[i].l,&range[i].r);
sort(range,range + n ,cmp);
priority_queue<int,vector<int>,greater<int>>heap;//定义一个小根堆
for(int i=0;i<n;i++)
{
auto range0 = range[i];
if(heap.empty() || heap.top() >= range0.l) //小根堆中top()访问的是第一个元素:小根堆这里是最小值
heap.push(range0.r);
else
{
heap.pop();
heap.push(range0.r);
}
}
printf("%d",heap.size());
return 0;
}
111.畜栏预定
(4)区间覆盖
//应用双指针算法,实现在左端点 l 都小于等于st的情况下,取右端点最大的区间,
//双指针模板 后的具体细节较多
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 100010;
struct Range
{
int l,r;
}range[N];
int n;
int st,ed;
int cmp(Range a,Range b) //左端点从小到大排序
{
return a.l < b.l ;
}
int main()
{
scanf("%d%d",&st,&ed);
scanf("%d",&n);
for(int i=0;i<n;i++)
{
scanf("%d%d",&range[i].l,&range[i].r );
}
sort(range,range +n ,cmp);
bool success = false; //完全覆盖的标志
int res = 0; //记录去覆盖的区间数
for(int i=0;i<n;i++)
{
int j = i,R = -2e9; //j去寻找i后面的r的最大值,R=-2e9清理防止上次的影响
while(j<n && range[j].l <= st) //遍历所有左端点在start左边的区间里面右端点的最大值
{
R = max( R , range[j].r );
j++;
}
if(R < st) //每次当前右端点的最远位置不能到达下一线段的左端点,一定不能覆盖
{
res = -1;
break;
}
res ++ ; //能覆盖st的情况下区间数才能加1
if(R >= ed)
{
success = true;
break; //完活!
}
st = R;
i = j-1; //因为上面j++的原因,多走了一个,减1的话,本身大的for循环通过i+1刚好到下一组区间(新st = R)
}
if(!success)
res = -1;
cout << res << endl;
return 0;
}
2.Huffman树
148.合并果子
找一个完全二叉树使得 (xa + yb zc +md + ne … ) 满足最小
- 最小的两个点,深度一定最深,并且可以互为兄弟
局部最优解,
//与合并石子问题不同的是,合并石子是每次必须合并相邻的,而合并果子是随便合并
//深度越深的果堆越小,因为深度越深,所乘系数越大
//每次都去合并剩下的果堆中最小的两堆(用优先队列来实现)
#include<iostream>
#include<algorithm>
#include<queue>
using namespace std;
const int N = 10010;
int n;
int main()
{
scanf("%d",&n);
priority_queue<int,vector<int>,greater<int>> heap; //定义小根堆
while(n--)
{
int x ;
scanf("%d",&x);
heap.push(x);
}
int res = 0;
while(heap.size()>1) //只要不是合并到最后一个
{
int a = heap.top();heap.pop();
int b = heap.top();heap.pop();
res += (a+b);
heap.push(a+b);
}
cout << res <<endl;
return 0;
}
3.排序不等式
913.排队打水
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 100010;
int n ;
int t[N];
int main()
{
scanf("%d",&n);
for(int i=0;i<n;i++)
scanf("%d",&t[i]);
sort(t,t+n);
long long res = 0;
for(int i=0;i<n;i++)
res = res + t[i]*(n-1-i);
cout << res <<endl;
return 0;
}
4.绝对值不等式
104.货仓选址
- 排序 + 取中位数
找中位数
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 100010;
int n;
int a[N] ;
int main()
{
scanf("%d",&n);
for(int i=0;i<n;i++)
scanf("%d",&a[i]);
sort(a,a+n);
int res= 0;
for(int i=0;i<n;i++)
res = res + abs( a[i] - a[n/2] );
cout << res << endl;
return 0;
}
122.糖果传递
#include<iostream>
#include<algorithm>
typedef long long LL;
using namespace std;
const int N = 1000010;
int n;
int a[N];
LL c[N];
int main()
{
scanf("%d",&n);
LL A = 0;
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
A += a[i] ;
}
A = A / n;
c[1] = 0;
for(int i = 2 ; i<=n ;i++ )
c[i] = c[i-1] - A + a[i];
sort(c+1,c + n +1 );
LL res = 0;
for(int i=1 ;i <=n ;i++)
res += abs(c[i] - c[(n+1)/2]);
cout << res << endl;
return 0;
}
5.推公式
125.耍杂技的牛
求所有牛最大的危险系数的最小值
- 按照 Wi + Si 从小到大的顺序排,最大的危险系数一定是最小的
证明:
1.贪心得到的答案 >= 最优解
化简为:
因为 s i < w i + s i ,又有假设 w i + s i > w( i+1 ) ,所以 交换后 危险系数降低,所以 w i + s i 大的 (第i个位置的牛) 要放在下面
2.贪心得到的答案 <= 最优解
#include<iostream>
#include<algorithm>
using namespace std;
typedef pair<int,int> PII ; //pair的实现是一个结构体,主要的两个成员变量是first second
const int N = 50010;
int n;
PII cow[N]; //first = w[i] + s[i],second = w[i]
int main()
{
scanf("%d",&n);
for(int i=0; i<n ;i++ )
{
int w,s;
scanf("%d%d",&w,&s);
cow[i] = {w+s,w};
}
sort( cow,cow+n ); //默认按照第一个值升序排序
int res = -2e9;
int sum = 0; //统计上面牛的重量
for(int i=0;i<n;i++)
{
int w = cow[i].second ,s = cow[i].first -w ;
res = max(res , sum -s );
sum += w;
}
cout << res << endl;
return 0;
}
Leetcode
1贪心算法
455.Assign cookies
实际是双指针问题
这里有三种不同的方法:
//****不晓得错在哪里*** 1 *******************************//
int cmp(int*a,int*b)
{
return *a-*b;
}
int findContentChildren(int* g, int gSize, int* s, int sSize)
{
int i = 0,j = 0,child = 0;
qsort(g,gSize,sizeof(int),cmp);
qsort(s,sSize,sizeof(int),cmp);
for( i=0 ;i<gSize ; i++)
{
while( s[j]< g[i])
j++;
j++;
if( j<sSize )
child++;
else
break;
}
return child;
}
//*************************** 2 *******************************//
int findContentChildren(vector<int>& children, vector<int>& cookies)
{
sort(children.begin(), children.end());
sort(cookies.begin(), cookies.end());
int child = 0, cookie = 0;
while (child < children.size() && cookie < cookies.size())
{ //child 和 cookies 只要一个到了最后就停,相当于两个for
if (children[child] <= cookies[cookie]) ++child;
++cookie;
}
return child;
}
//***************************** 3 ****************************//
int cmp(int* a, int* b) {
return *a - *b;
}
int findContentChildren(int* g, int gSize, int* s, int sSize)
{
int i = 0 ,j = 0 , child = 0;
qsort(g,gSize,sizeof(int),cmp);
qsort(s,sSize,sizeof(int),cmp);
for(i=0; i<gSize ; i++ )
{
for( ; j < sSize ;j+)
{
if(g[i]<= s[j] )
break; //直到找到比胃口大的饼干才换另一个孩子,这时的j是累加的
++j;
}
if //child 是多余的,i 即为孩子的个数
return i;
}
135.Candy
其难点就在于贪心的策略,如果在考虑局部的时候想两边兼顾,就会顾此失彼。
那么本题采用了两次贪心的策略:
一次是从左到右遍历,只比较右边孩子评分比左边大的情况。
一次是从右到左遍历,只比较左边孩子评分比右边大的情况。
这样从局部最优推出了全局最优,即:相邻的孩子中,评分高的孩子获得更多的糖果。
int max(int a, int b)
{
return a > b ? a : b;
}
int candy(int* ratings, int ratingsSize){
int *left2Right = (int *)malloc(ratingsSize * sizeof(int));
int *right2Left = (int *)malloc(ratingsSize * sizeof(int));
int i = 0;
for (i = 0; i < ratingsSize; i++) {
left2Right[i] = 1;
right2Left[i] = 1;
}
//都初始化为1
for (i = 0; i < ratingsSize - 1; i++) {
if (ratings[i + 1] > ratings[i]) {
left2Right[i + 1] = left2Right[i] + 1;
}
}
//从左向右遍历
for (i = ratingsSize - 1; i > 0; i--) {
if (ratings[i - 1] > ratings[i]) {
right2Left[i - 1] = right2Left[i] + 1;
}
}
//从右向左遍历
int sum = 0;
for (i = 0; i < ratingsSize; i++) {
sum += max(right2Left[i], left2Right[i]); //找大的加上
}
return sum;
}
435.无重叠区间
思路:贪心算法是一种逐步构造最优解的方法,何谓贪心?简单的说就是每一步都做局部最优选择,如此迭代n次,最终结果在某种意义上就是全局最优的。
所以直接按 end 排序就可以了
// 二维数组排序
int MyCmp(const void *pa, const void *pb)
{
return (*(int**)pa)[1] - (*(int**)pb)[1];
}
int eraseOverlapIntervals(int** intervals, int intervalsSize, int* intervalsColSize){
// 贪心算法
if (intervalsSize == 0) {
return 0;
}
// end递增排序
qsort(intervals, intervalsSize, sizeof(int*), MyCmp);
int x_end = intervals[0][1];
int start;
int count = 1;
for (int i = 1; i < intervalsSize; i++) {
start = intervals[i][0];
if (start >= x_end) {
// 不相交
count++;
// 更新不重复区间end
x_end = intervals[i][1];
}
}
// 返回重复区间数
return intervalsSize - count;
}
qsort(intervals, intervalsSize, sizeof(int*), cmp);
int cmp(const void *a, const void *b) {
int *aa = *(int**)a, *bb = *(int**)b;
return aa[0] > bb[0];
}
/**************自己写的,有问题*****************/
//直到找到 end <= start ,每次找不到使 zhedie++,所以直接反应了有多少重叠项
int eraseOverlapIntervals(int** intervals, int intervalsSize, int* intervalsColSize)
{
int start,end;
qsort(intervals, intervalsSize, sizeof(int*), MyCmp);
end = intervals[0][1];
int zhedie = 0;
if(intervalsSize == 0)
return 0;
int i= 0;
while(i<intervalsSize-1)
{
start = intervals[++i][0];
while(end > start)
{ i++;
zhedie++;
if(i<intervalsSize)
start = intervals[i][0] ;
else
break;
}
end = intervals[i][1];
}
return zhedie;
}
//int cpm(*a , *b)
//{
// return *a - *b;
//}
int MyCmp(const void *pa, const void *pb)
{
return (*(int**)pa)[1] - (*(int**)pb)[1];
}
int eraseOverlapIntervals(int** intervals, int intervalsSize, int* intervalsColSize)
{
int start,end;
qsort(intervals, intervalsSize, sizeof(int*), MyCmp);
end = intervals[0][1];
int fzhedie = 0;
int yicichongdie = 0;
if(intervalsSize == 0)
return 0;
int i= 0;
for(i = 1; i<intervalsSize-1 ; i++)
{
start = intervals[i][0] ;
if(end <= start)
{
++fzhedie;
end = intervals[i][1];
}
}
return intervalsSize-fzhedie;
}
605.种花问题
/*方法一:***我用了7个if.....else....一个for整的**************************/
bool canPlaceFlowers(int* flowerbed, int flowerbedSize, int n){
int i;
if((flowerbedSize ==1)&&(flowerbed[0]==1))
{ if(n==0) return true;
else return false;
}
else
{if((flowerbedSize ==1)&&(flowerbed[0]==0))
n--;
else
for( i=0; i<flowerbedSize ;i++)
{
if((i==0) && (flowerbed[i]==0) && (flowerbed[i+1] == 0))
{ n--;flowerbed[0] = 1;}
else
{
if( (i>0) && (i<flowerbedSize-1) && (flowerbed[i-1] ==0) && (flowerbed[i+1]==0)&&(!flowerbed[i]))
{ n--;flowerbed[i] =1;}
else
if((i==flowerbedSize-1)&& (flowerbed[i-1] ==0) && (flowerbed[i]==0))
n--;
}
}
}
if(n>0)
return false;
else
return true;
}
/*************大佬的算法*****************/
for(int i = 0; i < m; ++i) //遍历时查看其是否可以种花
if(!flowerbed[i]&&(i==m-1||!flowerbed[i+1])&&(i==0||!flowerbed[i-1])){
flowerbed[i] = 1;
--n; //可以n就减少一位
}
return n <= 0; //返回n是否小于等于0
处。
452.用最少数量的箭引爆气球
int cmp(const void *a, const void *b) {
int *aa = *(int**)a, *bb = *(int**)b;
return aa[1] > bb[1];
}
int findMinArrowShots(int** points, int pointsSize, int* pointsColSize){
int j =0,cout = 0,biu;
qsort(points,pointsSize,sizeof(int*),cmp);
while(j<pointsSize)
{
biu = points[j][1];
++cout;
while(j<pointsSize && points[j][0]<=biu)
++j;
}
return cout;
}
2双指针
167.两数之和-输入有序数组
(暴力法)一个普通的 O(n^2)的
int* twoSum(int* numbers, int numbersSize, int target, int* returnSize){
int i,j;
int*b = (int*)malloc( 2*sizeof(int) );
*returnSize = 2;
for(i=1;i<numbersSize ;i++)
for(j=0; j<i ; j++)
{
if((numbers[i] + numbers[j])==target)
{
b[0] = j+1;
b[1] = i+1 ;
break;
}
}
return b;
}
双指针法:
int* twoSum(int* numbers, int numbersSize, int target, int* returnSize){
int low=0, high = numbersSize-1 ;
*returnSize = 2;
int *array = (int *)malloc( sizeof(int)*2 );
while(low< high)
{
if( numbers[low] + numbers[high] == target)
{
array[0] = low+1 ;
array[1] = high+1 ;
break;
}
else
{ if( numbers[low] +numbers[high] > target )
high--;
else
if(numbers[low] +numbers[high] < target )
low++;
}
}
return array;
}
二分法
//***********不晓得哪里出问题了**************//
int twofind(int *numbers,int numbersSize,int i,int other)
{
int m = i,med ; //保存i值
int j = numbersSize-1;
while(i<=j)
{
med=(j + i)/2 ;
if(numbers[med] == other )
{ m = med; break ;}
else
{
if(numbers[med] > other)
i = med +1 ;
else
j = med -1 ;
}
}
return m;
}
int* twoSum(int* numbers, int numbersSize, int target, int* returnSize){
int i ,j, other ;
int*b = (int*)malloc( sizeof(int)*2 );
*returnSize = 2;
for(i = 0;i<numbersSize ; i++)
{
other = target - numbers[i];
if( i!=twofind(numbers,numbersSize,i,other) )
break;
}
b[0] = i+1;
b[1] = twofind+1;
return b;
}
88.合并两个有序数组
//****************一个一个插入的方法**失败******//
void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n){
int i = 0,j = 0; // i 去历遍要加进去的数组
for(i=0 ; i<n ;i++) //n为要加进去的数
{
if(nums2[i]>nums1[n-1+i])
nums1[n+i]= nums2[i];
for(j = 0;j<nums1Size-1; j++ )
{
if( (nums1[j]<=nums2[i]) && (nums2[j+1]>nums2[i]) )
for(int k =n+i ; k>j;k--)
{
nums1[k] =nums1[k-1];
nums1[j+1]=nums2[i];
}
}
}
return *nums1 ;
}
void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n) {
int hebing[m+n] ;
int i=0,j=0,k=0;
while(k<m+n)
{
while(i<m&&j<n)
{
if(nums1[i] >= nums2[j])
hebing[k++] = nums2[j++];
else
hebing[k++] = nums1[i++];
}
if(j>=n)
for(;i<m;i++)
hebing[k++] = nums1[i];
else
for(;j<n;j++)
hebing[k++] = nums2[j];
}
for(i=0; i<m+n ;i++)
nums1[i] = hebing[i];
}
142.Linked List Cycle
理解快慢指针
深入理解快慢指针算法 – 链表环路检测 - 知乎 (zhihu.com)
//**************快慢指针*********** 不晓得为什莫用 !fast->next? ********//
struct ListNode *detectCycle(struct ListNode *head) {
struct ListNode *slow = head, *fast = head;
// 判断是否存在环路,若有环路,在slow = fast的时候结束
do {
if (!fast || !fast->next) return NULL;
// !fast 是判断链表是否为空,!fast->next 是判断是否是无环链表
fast = fast->next->next;
slow = slow->next;
} while (fast != slow);
// 如果存在,查找环路节点
fast = head;
while (fast != slow){
slow = slow->next;
fast = fast->next;
}
return fast;
}
76.滑动窗口
3二分查找
详解:[算法总结] 二分查找 - 知乎 (zhihu.com)
总结:
- 1.最基本的二分查找
//*********经典算法*********//
int low=0; //定义初始最小
int high=len-1; //定义初始最大
int mid; //定义中间值
while(low<=high)
{
mid=(low+high)/2; //找中间值
if(key==arr[mid]) //判断min与key是否相等
return mid;
else if(key>arr[mid]) //如果key>mid 则新区间为[mid+1,high]
low=mid+1;
else //如果key<mid 则新区间为[low,mid-1]
high=mid-1;
}
return -1; //如果数组中无目标值key,则返回 -1 ;
1. 循环的判定条件是:low <= high
2. 为了防止数值溢出*** mid = low + (high - low)/2 ***
3. 当 arr[mid] 不等于 key 时,high = mid - 1或low = mid + 1
-
2.查找目标值区域的左边界/查找与目标值相等的第一个位置/查找第一个不小于目标值数的位置
例: A = [1,3,3,5, 7, 7,7,7,8,14,14] target = 7 return 4 //***************************************// public int binarySearchLowerBound(int[] A, int target, int n){ int low = 0, high = n, mid; while(low <= high){ mid = low + (high - low) / 2; if(target <= A[mid]){ //当target = A[mid] 时还往下走,表示找第一个出现的 high = mid - 1; }else{ low = mid + 1; //如果没找到还有符合的,low会不断往上靠,直到最后一次,low靠到符合的那个位置(即最后一刻靠到 high 之前) } } if(low < A.length && A[low] == target) return low; //限定能够返回 low 的情况,因为low已经越界了,所以设置了 low < A.target ,使之在合理区域内 else return -1; }
-
查找目标值区域的右边界/查找与目标值相等的最后一个位置/查找最后一个不大于目标值数的位置
A = [1,3,3,5,7,7,7, 7 ,8,14,14]
target = 7
return 7
int low = 0, high = n, mid;
while(low <= high){
mid = low + (high - low) / 2;
if(target >= A[mid]){
low = mid + 1;
}else{
high = mid - 1;
}
}
if(high >= 0 && A[high] == target)
return high;
else
return -1;
实例:找相等的处在最前面的值和最后面的值
//********************不晓得为啥不能通过************************//
int first(int* , int, int);
int last(int* , int, int);
int* searchRange(int* nums, int numsSize, int target, int* returnSize){
*returnSize = 2;
first( nums, numsSize, target);
last( nums, numsSize, target);
int * a = malloc(sizeof(int)*2) ;
a[0] = first;
a[1] = last;
return a;
}
int first(int* nums, int numsSize, int target)
{
int low = 0, high = numsSize-1, mid;
while(low <= high){
mid = low + (high - low) / 2;
if(target <= nums[mid]){ //当target = A[mid] 时还往下走,表示找第一个出现的
high = mid - 1;
}else{
low = mid + 1;
//如果没找到还有符合的,low会不断往上靠,直到最后一次,low靠到符合的那个位置(即最后一刻靠到 high 之前)
}
}
if(low < numsSize && nums[low] == target)
return low; //限定能够返回 low 的情况
else
return -1;
}
int last(int* nums, int numsSize, int target)
{
int low = 0, high = numsSize-1, mid;
while(low <= high){
mid = low + (high - low) / 2;
if(target >=nums[mid]){ //当target = A[mid] 时还往下走,表示找第一个出现的
low = mid + 1;
}else{
high = mid - 1;
//如果没找到还有符合的,low会不断往上靠,直到最后一次,low靠到符合的那个位置(即最后一刻靠到 high 之前)
}
}
if(high>=0 && nums[high] == target)
return high; //限定能够返回 low 的情况
else
return -1;
}
69.Sqrt(x) 开平方
//****** x 是非负整数,所以是在 1 之后*****//
用二分法能提高效率,但是细节的处理要仔细
int mySqrt(int x)
{
int l = 1,r=x ,mid,sqrt;
if(x==0)return 0;
while(l<=r)
{
mid = l+ (r-l )/2;
sqrt = x/mid;
if(mid == sqrt)
return mid;
else
{
if(mid>sqrt)
r = mid-1;
else
l = mid+1;
}
}
return l-1;
}
4排序算法
215.数组中第k个最大元素
方法一:暴力解法
时间复杂度
-
排序:O(nlog n )
-
返回 k-1 下标O(1)
空间复杂度:O(1)
方法二:基于快速排序的选择方法
inline int partition(int* a, int l, int r) { //找轴值的位置
int x = a[r], i = l - 1; //把小于 x 的放到前面去
for (int j = l; j < r; ++j) {
if (a[j] <= x) {
int t = a[++i];
a[i] = a[j], a[j] = t;
}
}
int t = a[i + 1];
a[i + 1] = a[r], a[r] = t;
return i + 1;
}
inline int randomPartition(int* a, int l, int r) { //随机找一个轴值,并且返回轴值的位置
int i = rand() % (r - l + 1) + l;
int t = a[i];
a[i] = a[r], a[r] = t;
return partition(a, l, r);
}
int quickSelect(int* a, int l, int r, int index) {
//递归直到找到 index(即 numsSize-k 也就是第k大的)
int q = randomPartition(a, l, r);
if (q == index) {
return a[q]; //返回那个轴值位置刚好等于第k大位置的那个轴值
} else {
return q < index ? quickSelect(a, q + 1, r, index)
: quickSelect(a, l, q - 1, index);
}
}
int findKthLargest(int* nums, int numsSize, int k) {
srand(time(0));
return quickSelect(nums, 0, numsSize - 1, numsSize - k);
}
时间复杂度:O(n)
空间复杂度:O(log n)
方法三:维护一个堆
/* 交换 */
void swap(int* a, int* b) {
int tmp = *a;
*a = *b;
*b = tmp;
}
/* 从堆下层向上交换元素,使得堆为小根堆 */
void swim(int* nums, int k) {
while (k > 1 && nums[k] < nums[k / 2]) {
swap(&nums[k], &nums[k / 2]);
k /= 2;
}
}
/* 从堆上层向下层交换元素,使得堆为小根堆 */
void sink(int* nums, int k, int numsSize) {
while (2 * k < numsSize) {
int child = 2 * k;
if (child < numsSize && nums[child] > nums[child + 1]) {
child++;
}
if (nums[k] < nums[child]) {
break;
}
swap(&nums[k], &nums[child]);
k = child;
}
}
/* 定义堆的结构体 */
typedef struct Heap {
int* data;
int szie;
int capacity;
}T_Heap, *PT_Heap;
/* 初始化一个堆 */
PT_Heap createHeap(int k) {
PT_Heap obj = (PT_Heap)malloc(sizeof(T_Heap));
obj->data = (int*)malloc(sizeof(int) * (k + 1));
obj->szie = 0;
obj->capacity = k + 1;
return obj;
}
/* 判断堆是否为空 */
bool isEmpty(PT_Heap obj) {
return obj->szie == 0;
}
/* 获得堆的当前大小 */
int getHeapSize(PT_Heap obj) {
return obj->szie;
}
/* 将元素入堆 */
void pushHeap(PT_Heap obj, int elem) {
/* 新加入的元素放入堆的最后 */
obj->data[++obj->szie] = elem;
/* 对当前堆进行排序,使其成为一个大根堆 */
swim(obj->data, obj->szie);
}
/* 获得堆顶元素 */
int getHeapTop(PT_Heap obj) {
return obj->data[1];
}
/* 将堆顶元素出堆 */
int popHeap(PT_Heap obj) {
/* 保存堆顶元素 */
int top = obj->data[1];
/* 将堆顶元素和堆底元素交换,同时堆长度减一 */
swap(&obj->data[1], &obj->data[obj->szie--]);
/* 将原先的堆底元素赋值为INT_MIN */
obj->data[obj->szie + 1] = INT_MIN;
/* 从堆顶开始重新堆化 */
sink(obj->data, 1, obj->szie);
return top;
}
int findKthLargest(int* nums, int numsSize, int k){
/* 初始化一个大小为k的堆 */
PT_Heap heap = createHeap(k);
/* 将输入数组前k个元素堆化 */
for (int i = 0; i < k; i++) {
pushHeap(heap, nums[i]);
}
/* 将输入数组剩下的元素依次插入小根堆,得出最大的k个数 */
for (int i = k; i < numsSize; i++) {
if (nums[i] > getHeapTop(heap)) {
popHeap(heap);
pushHeap(heap, nums[i]);
}
}
/* 维护的是一个小根堆,堆顶元素即第K大的元素 */
延申
TopK 问题
-
方法一:对源数据中所有数据进行排序,取出前K个数据,就是TopK。
但是当数据量很大时,只需要k个最大的数,整体排序很耗时,效率不高。
-
方法二:维护一个K长度的数组a[],先读取源数据中的前K个放入数组,对该数组进行升序排序,再依次读取源数据第K个以后的数据,和数组中最小的元素(a[0])比较,如果小于a[0]直接pass,大于的话,就丢弃最小的元素a[0],利用二分法找到其位置,然后该位置前的数组元素整体向前移位,直到源数据读取结束。
这比方法一效率会有很大的提高,但是当K的值较大的时候,长度为K的数据整体移位,也是非常耗时的。
对于这种问题,效率比较高的解决方法是使用最小堆。
一棵深度为k的有n个结点的 二叉树 ,对树中的结点按从上至下、从左到右的顺序进行编号,如果编号为i(1≤i≤n)的结点与 满二叉树 中编号为i的结点在二叉树中的位置相同,则这棵二叉树称为完全二叉树
451.根据字符出现频率排序
类似于347.前k个高频元素,用结构体保存每种元素及其出现的次数。然后根据频率输出字符(每个字符输出 tong[i].number 次)
347.荷兰国旗问题
- 简单方法:
统计数组中0,1,2 的个数,然后再根据他们的数量,重写整个数组
-
单指针
我们可以考虑对数组进行两次遍历。在第一次遍历中,我们将数组中所有的 00 交换到数组的头部。在第二次遍历中,我们将数组中所有的 11 交换到头部的 00 之后。此时,所有的 22 都出现在数组的尾部,这样我们就完成了排序。
void swap(int *a, int *b) {
int t = *a;
*a = *b, *b = t;
}
//********因为 i 比 ptr 快,所以 i 与 ptr之间的都是一些不为零的,ptr安全*****//
void sortColors(int *nums, int numsSize) {
int ptr = 0;
for (int i = 0; i < numsSize; ++i) {
if (nums[i] == 0) {
swap(&nums[i], &nums[ptr]);
++ptr;
}
}
for (int i = ptr; i < numsSize; ++i) {
if (nums[i] == 1) {
swap(&nums[i], &nums[ptr]);
++ptr;
}
}
}
时间复杂度是 O(n)
空间复杂度为 O(1)
-
双指针
void swap(int *a, int *b) {
int t ;
t= *a;
*a = *b; *b = t;
}
void sortColors(int *nums, int numsSize) {
int p0 = 0 , p2 = numsSize-1;
if(numsSize==0)
return NULL; //首先分析数组为零的情况
for(int i =0; i<=p2 ;i++ ) //因为 p2 以上的元素都是2,所以当 i 大于p2后就没有意义了
{
if(nums[i]==0){
swap(&nums[i],&nums[p0]);
++p0;
}
else if(nums[i]==2){
swap(&nums[i],&nums[p2]);
--p2;
if(nums[i]==2||nums[i]==0) //如果刚好碰到 p2 处本身也为2或0,则换完后还是换一遍,所以提前--i,这样等他++i后,会再访问一遍此位置。
--i;
}
}
return nums;
}
5搜索
(1)深度优先
695.岛屿的最大面积
//****不晓得为什么只能通过10个********************//
int IslandDFS(int **grid,int i,int j,int gridSize,int* gridColSize){
if((i<gridSize) && (i>=0) && (j<*gridColSize) && (j>=0)){
if(grid[i][j] == 1){
grid[i][j] = 0;
return 1 + IslandDFS(grid,i,j-1,gridSize,gridColSize)
+ IslandDFS(grid,i,j+1,gridSize,gridColSize)
+ IslandDFS(grid,i-1,j,gridSize,gridColSize)
+IslandDFS(grid,i+1,j,gridSize,gridColSize);
/****************上下左右****************/
}
else{
return 0;
}
}
else{
return 0;
}
}
int maxAreaOfIsland(int** grid, int gridSize, int* gridColSize){
//gridSize 是行宽, *gridColSize 是列宽
int i ,j ,Max = 0;
for(i=0 ; i<gridSize ; i++){
for(j=0 ; j<*gridColSize ; j++){
Max = (IslandDFS(grid,i,j,gridSize,gridColSize) > Max? IslandDFS(grid,i,j,gridSize,gridColSize):Max) ;
}
}
return Max ;
}
深度优先+栈
int maxAreaOfIsland(int** grid, int gridSize, int* gridColSize){
int stack[5000][2];
int p = -1; // 栈顶指针
int ans = 0;
for (int i = 0; i < gridSize; ++i) {
for (int j = 0; j < *gridColSize; ++j) {
int t = 0;
stack[++p][0] = i; //*******先将最开始的元素入栈
stack[p][1] = j;
while (p > -1) { //*******直到栈里的元素全部弹出(p=-1)才允许下一个*********//
int row = stack[p][0]; //****访问这个数据,然后将其弹出*******//
int col = stack[p][1];
p--; //******弹出开始一个元素************//
if (row < 0 || col < 0 || row == gridSize || col == *gridColSize || grid[row][col] == 0) {
continue; //****如果这不是块陆地,则退出 while 循环 ****//
}
grid[row][col] = 0; //*******如果是块陆地,把它变成海洋*********//
t++; //******* 是陆地就 t++ *****************//
int x[4] = {-1, 0, 1, 0};
int y[4] = {0, 1, 0, -1};
for (int k = 0; k < 4; ++k) {
stack[++p][0] = row + x[k];
stack[p][1] = col + y[k]; //**** 把上下左右邻都入栈 ****//
}
}
ans = ans > t ? ans : t;
}
}
return ans;
}
547.朋友圈(相连城市为省份)
//************深度搜索某个城市时,访问到的城市的 visited 会变成1;********//
void dfs(int** isConnected, int* visited, int provinces, int i) {
for (int j = 0; j < provinces; j++) {
if (isConnected[i][j] == 1 && !visited[j]) { // !visited[j] 是为了防止形成死循环
visited[j] = 1; //此城市被访问了
dfs(isConnected, visited, provinces, j); //当找到一个可以连接的城市时,先顺藤摸瓜深下去。
}
}
}
int findCircleNum(int** isConnected, int isConnectedSize, int* isConnectedColSize) {
int provinces = isConnectedSize;
int visited[provinces]; //visited[] 记录每个城市是否被访问过
memset(visited, 0, sizeof(visited)); //快速使数组初始化为0
int circles = 0;
for (int i = 0; i < provinces; i++) { //遍历每个城市
if (!visited[i]) { //每次访问都是去访问之前没有被访问过的城市
dfs(isConnected, visited, provinces, i);
circles++; //圈数加一
}
}
return circles;
}
417. 太平洋大西洋水流问题
//**************我的代码(没跑出来)******************//
/**
* Return an array of arrays of size *returnSize.
* The sizes of the arrays are returned as *returnColumnSizes array.
* Note: Both returned array and *columnSizes array must be malloced, assume caller calls free().
*/
int m,n; //全局变量
int direct[4][2] ={{0,-1},{0,1},{-1,0},{1,0}};
void riverDFS(int** heights,int heightsSize,int* heightsColSize,int i,int j,int** Ocean){
if((i>=0)&&(i<heightsSize)&&(j>=0)&&(j<*heightsColSize)&&(Ocean[i][j]==0)){
Ocean[i][j] = 1;
for(int k = 0;k<4;k++){
int newI = i+direct[k][0];
int newJ = j+direct[k][1];
if(heights[newI][newJ]>=heights[i][j]){
riverDFS(heights,heightsSize,heightsColSize,i,j-1,Ocean);
}
}
}
}
int** pacificAtlantic(int** heights, int heightsSize, int* heightsColSize, int* returnSize, int** returnColumnSizes){
*returnSize = 0;
if(heights == NULL || heightsSize <= 0 || heightsColSize == NULL) {
return NULL;
}
int i,j;
int m= heightsSize ,n=*heightsColSize;
int **ans = (int**)malloc(m*n*sizeof(int*)) //开辟一个装m*n*sizeof(int*)数组
for(i=0 ; i<m*n ; i++){
ans[i] = (int*)malloc(2*sizeof(int)) //接着说明ans的每个int* 指的是两个int
memset(ans[i],0,2*sizeof(int*)); //顺便把它们初始化为零
}
//同样的方式开辟大西洋和太平洋
int **atlantic = (int**)malloc(m * sizeof(int*));
int **Pacific = (int**)malloc(m * sizeof(int*));
for(i = 0; i < m; i++) {
atlantic[i] = (int*)malloc(n * sizeof(int));
Pacific[i] = (int*)malloc(n * sizeof(int));
memset(atlantic[i], 0, n * sizeof(int));
memset(Pacific[i], 0, n * sizeof(int));
}
for(i=0; i < m; i++){
riverDFS(heights,m,n,i,0,atlantic); //左
riverDFS(heights,m,n,i,n-1,Pacific);//右
}
for(i=0; i < n; i++){
riverDFS(heights,m,n,0,i,Pacific); //上
riverDFS(heights,m,n,m-1,i,atlantic);//下
}
for( i=0;i<m;i++)
for( j =0;j<n;j++){
if((atlantic[i][j]==1)&&(Pacific[i][j]==1)){
ans[(*returnSize)][0] = i;
ans[(*returnSize)][1] = j;
(*returnSize)++;
}
}
*returnColumnSizes = (int*)malloc((*returnSize)*sizeof(int));
//
for(int i = 0; i < *returnSize; i++) {
(*returnColumnSizes)[i] = 2;
}
for(int i = 0; i < m; i++) {
free(atlantic[i]);
free(Pacific[i]);
}
return ans;
}
/**
* Return an array of arrays of size *returnSize.
* The sizes of the arrays are returned as *returnColumnSizes array.
* Note: Both returned array and *columnSizes array must be malloced, assume caller calls free().
*/
// 417. 太平洋大西洋水流问题。
// DFS
// 有题目可知,可以对所有可能的点进行搜索,然后判断该点是否符合要求,但是这样的复杂度会比较高。
// 所以,考虑沿着水流的相反方向进行搜索,那么只需要遍历矩形的四个边即可。
int directions[4][2] = {{0, -1}, {-1, 0}, {0, 1}, {1, 0}};
void Dfs(int** reach, int** heights, int m, int n, int x, int y) {
if(reach[x][y] == 1) {
return;
}
reach[x][y] = 1;
for(int i = 0; i < 4; i++) {
int newX = x + directions[i][0];
int newY = y + directions[i][1];
if(newX >= 0 && newX < m && newY >= 0 && newY < n &&
heights[x][y] <= heights[newX][newY]) {
Dfs(reach, heights, m, n, newX, newY);
}
}
}
int** pacificAtlantic(int** heights, int heightsSize, int* heightsColSize,
int* returnSize, int** returnColumnSizes){
*returnSize = 0;
if(heights == NULL || heightsSize <= 0 || heightsColSize == NULL) {
return NULL;
}
int m = heightsSize, n = heightsColSize[0];
int **ans = (int**)malloc(m * n * sizeof(int*));
for(int i = 0; i < m * n; i++) {
ans[i] = (int*)malloc(2 * sizeof(int));
memset(ans[i], 0, 2 * sizeof(int));
}
int **reachP = (int**)malloc(m * sizeof(int*));
int **reachA = (int**)malloc(m * sizeof(int*));
for(int i = 0; i < m; i++) {
reachP[i] = (int*)malloc(n * sizeof(int));
reachA[i] = (int*)malloc(n * sizeof(int));
memset(reachP[i], 0, n * sizeof(int));
memset(reachA[i], 0, n * sizeof(int));
}
// 只需要搜索矩形的四个边
for(int i = 0; i < m; i++) {
Dfs(reachP, heights, m, n, i, 0);
Dfs(reachA, heights, m, n, i, n - 1);
}
for(int i = 0; i < n; i++) {
Dfs(reachP, heights, m, n, 0, i);
Dfs(reachA, heights, m, n, m - 1, i);
}
for(int i = 0; i < m; i++) {
for(int j = 0; j < n; j++) {
if(reachP[i][j] == 1 && reachA[i][j] == 1) {
ans[(*returnSize)][0] = i;
ans[(*returnSize)][1] = j;
(*returnSize)++;
}
}
}
*returnColumnSizes = (int*)malloc((*returnSize) * sizeof(int));
for(int i = 0; i < *returnSize; i++) {
(*returnColumnSizes)[i] = 2;
}
for(int i = 0; i < m; i++) {
free(reachP[i]);
free(reachA[i]);
}
return ans;
}
(2)回溯法
回溯框架:
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
全排列:
/**
* Return an array of arrays of size *returnSize.
* The sizes of the arrays are returned as *returnColumnSizes array.
* Note: Both returned array and *columnSizes array must be malloced, assume caller calls free().
*/
/* 定义当前遍历深度count为全局变量 */
int count;
/* 进行深度优先遍历DFS
* 参数1:输入数组
* 参数2:输入数组长度,既数的个数
* 参数3:当前遍历的深度,一开始为0
* 参数4:当前走过的路径
* 参数5:当前结点使用情况,初始都为0未使用
* 参数6:返回数组
*/
void DFS(int *nums, int numsSize, int depth, int *path, bool *used, int **res) {
/* 6.1、当前路径已经全部搜索完 */
if (depth == numsSize) {
/* 6.1.1、分配返回数组中此次遍历行的列数 */
res[count] = (int *)malloc(sizeof(int) * numsSize);
/* 6.1.2、将此行走过的路径赋给res返回数组 */
memcpy(res[count++], path, sizeof(int) * numsSize);
/* 6.1.3、返回上一层决策树 */
return;
}
/* 6.2、当前路径未达到树底
* 遍历搜索列表(既输入数组),找到下一个结点
*/
for (int i = 0; i < numsSize; i++) {
/* 6.2.1、当前节点已经使用过,跳过 */
if (used[i] == true) {
continue;
}
/* 6.2.2、当前结点未使用
* 将当前结点存入走过的路径path中
*/
path[depth] = nums[i];
/* 6.2.3、标记当前结点已使用过 */
used[i] = true;
/* 6.2.4、进入下一层决策树 */
//depth++;
DFS(nums, numsSize, depth + 1, path, used, res);
/* depth表示排列树的深度,回溯时应该回到上一次递归时的结果:
* 如果depth在dfs之前depth++了,则在每次dfs之后都应depth--,来恢复其状态;
* 如果depth在下一次传入dfs时直接使用depth+1的结果传入递归,则递归结束后,无需恢复其状态(本来就是原本的状态)。
*/
//depth--;
/* 6.2.5、返回上一层决策树之前需要取消当前结点的标记 */
used[i] = false;
}
}
int** permute(int* nums, int numsSize, int* returnSize, int** returnColumnSizes){
/* 1、计算返回数组的行数为全排列的种类数:n! */
(*returnSize) = 1;
for (int i = 1; i <= numsSize; i++) {
(*returnSize) *= i;
}
/* 2、计算返回数组中每行的列数 */
*returnColumnSizes = (int *)malloc(sizeof(int) * (*returnSize)); //开辟行数个的int(4字节)空间,用来装每行的列数
for (int i = 0; i < (*returnSize); i++) {
(*returnColumnSizes)[i] = numsSize; //每行的列数都为 numsSize
}
/* 3、定义返回数组 */
int **res = (int **)malloc(sizeof(int *) * (*returnSize));
/* 4、定义走过的路径 */
int *path = (int *)malloc(sizeof(int) * numsSize);
/* 5、定义结点标记,初始化为0 */
bool *used = (bool *)calloc(numsSize, sizeof(bool));
/* 当前遍历深度需要在此赋值
* 不能在全局定义时赋值,否则会出现内存溢出,就很😵
*/
count = 0;
/* 6、进行深度优先遍历DFS
* 参数1:输入数组
* 参数2:输入数组长度,既数的个数
* 参数3:当前遍历的深度,一开始为0
* 参数4:当前走过的路径
* 参数5:当前结点使用情况,初始都为0未使用
* 参数6:返回数组
*/
DFS(nums, numsSize, 0, path, used, res);
/* 7、返回 */
return res;
}
组合:
int* temp;
int tempSize;
int** ans;
int ansSize;
void dfs(int cur, int n, int k) {
// 剪枝:temp 长度加上区间 [cur, n] 的长度小于 k,不可能构造出长度为 k 的 temp
if (tempSize + (n - cur + 1) < k) {
return;
}
// 记录合法的答案
if (tempSize == k) {
int* tmp = malloc(sizeof(int) * k);
for (int i = 0; i < k; i++) {
tmp[i] = temp[i];
}
ans[ansSize++] = tmp;
return;
}
// 考虑选择当前位置
temp[tempSize++] = cur;
dfs(cur + 1, n, k);
tempSize--;
// 考虑不选择当前位置
dfs(cur + 1, n, k);
}
int** combine(int n, int k, int* returnSize, int** returnColumnSizes) {
temp = malloc(sizeof(int) * k);
ans = malloc(sizeof(int*) * 10001);
tempSize = ansSize = 0;
dfs(1, n, k);
*returnSize = ansSize;
*returnColumnSizes = malloc(sizeof(int) * ansSize);
for (int i = 0; i < ansSize; i++) {
(*returnColumnSizes)[i] = k;
}
return ans;
}
回溯算法资料:
https://mp.weixin.qq.com/s?__biz=MzUxNjY5NTYxNA==&mid=2247485237&idx=1&sn=1bae4c3d0d3965af44878093a5a49f58&chksm=f9a23464ced5bd72ff9ddcc9c70f69131a9e57e5c1aa674cc62551cd434c64f10a88395dce60&mpshare=1&scene=1&srcid=0906LCKDwVYcWIeSDmfH3gbA&sharer_sharetime=1630914258080&sharer_shareid=dd26a0a2603bb02d0e961fb98af8b567#rd
(3)广度优先
695.最大岛屿面积
//*******************只不过是把深度优先中的栈变为了队列*******************************//
int maxAreaOfIsland(int** grid, int gridSize, int* gridColSize){
int stack[20000][2];
int ans = 0;
//队列的特点是队尾插入,队头删除
for (int i = 0; i < gridSize; ++i) {
for (int j = 0; j < *gridColSize; ++j) {
int t = 0, ph = -1, pt = -1; // ph队列头指针,pt队尾指针
stack[++pt][0] = i; //*******先将最开始的元素入栈********//
stack[pt][1] = j;
while (ph != pt) { //*******只要队中还有元素***********//
int row = stack[++ph][0] ; //*******队头访问一个元素,同时删除一个元素****//
int col = stack[ph][1] ;
if (row < 0 || col < 0 || row == gridSize || col == *gridColSize || grid[row][col] == 0) {
continue;
}
grid[row][col] = 0;
t++;
int x[4] = {-1, 0, 1, 0};
int y[4] = {0, 1, 0, -1};
for (int k = 0; k < 4; ++k) {
stack[++pt][0] = row + x[k];
stack[pt][1] = col + y[k];
}
}
ans = ans > t ? ans : t;
}
}
return ans;
}
CCF-CSP
第27次
第一题
#include <iostream>
#include <cmath>
using namespace std;
double sum;
/* run this program using the console pauser or add your own getch, system("pause") or input loop */
int main(int argc, char** argv)
{
double rate;
int n;
scanf("%d %lf",&n,&rate);
for(int i=0;i<=n;i++)
{
double pay;
scanf("%lf",&pay);
sum = sum*(1+rate) + pay;
}
while(n--)
{
sum /= (1+rate);
}
printf("%lf\d",sum);
return 0;
}
第二题
#include <iostream>
#include <cmath>
#include <cstring>
using namespace std;
const int N = 110;
int depend[N]; // 依赖
int a[N]; // 最早开始时间
int b[N]; // 最晚开始时间
int need[N]; // 每个任务需要的时间
/* run this program using the console pauser or add your own getch, system("pause") or input loop */
int main(int argc, char** argv)
{
int n,m;
cin >> n >> m;
for(int i=1;i<=m;i++)
{
scanf("%d",&depend[i]);
}
for(int i=1;i<=m;i++)
{
scanf("%d",&need[i]);
}
a[0] = 1;
for(int i=1;i<=m;i++)
{
a[i] = a[depend[i]] + need[depend[i]];
cout << a[i] << " ";
}
cout << endl;
memset(b,0x3f,sizeof b);
for(int i = m ;i>=0 ;i--)
{
int temp = n - need[i] +1;
if(b[i] > temp)
b[i] = temp ;
int tofront = depend[i];
if( b[tofront] > ( b[i] - need[tofront] ) )
b[tofront] = b[i] - need[tofront];
}
int flag = 1;
for(int i = 1;i<=m ;i++)
{
if(b[i]<=0)
flag = 0;
}
if(flag)
{
for(int i=1;i<=m;i++)
cout << b[i] << " ";
}
return 0;
}
第三题 树形结构 超时 / 全部记录+sort 超空间
#include <iostream>
#include<vector>
#include<algorithm>
using namespace std;
typedef long long LL;
int main()
{
LL a[300001]={0};
LL n;
scanf("%lld",&n);
LL t;
vector<LL> p;
p.push_back(0);
p.push_back(1);
for(LL i=2;i<=n;i++)
{
scanf("%lld",&t);
p.push_back(t);
}
for(LL i=1;i<=n;i++)
scanf("%lld",&a[i]);
vector<vector<LL> >tree;
for(LL i=0;i<=n;i++)
{
vector<LL>b;
b.push_back(a[i]);
tree.push_back(b);
}
for(LL i=n;i>1;i--)
{
tree[p[i]].insert(tree[p[i]].end(),tree[i].begin(),tree[i].end());
}
for(LL i=1;i<=n;i++)
{
sort(tree[i].begin(),tree[i].end());
LL N_x=tree[i].size();
LL Ans_x=0;
if(N_x!=1)
{
for(LL j=0;j<N_x;j++)
{
if(j==0)
Ans_x+=(tree[i][j+1]-tree[i][j])*(tree[i][j+1]-tree[i][j]);
else if(j==N_x-1)
Ans_x+=(tree[i][j]-tree[i][j-1])*(tree[i][j]-tree[i][j-1]);
else
Ans_x+=min((tree[i][j+1]-tree[i][j])*(tree[i][j+1]-tree[i][j]),(tree[i][j]-tree[i][j-1])*(tree[i][j]-tree[i][j-1]));
}
}
cout<<Ans_x;
if(i!=n)
cout<<endl;
}
}
第26次
第一题
第二题
- 暴力:dfs 也可以 , 位运算模拟简单的 01 dfs
#include <iostream>
#include <algorithm>
#include <cstring>
#include <vector>
using namespace std;
typedef pair<int ,int> PII;
const int N = 31;
int cnt[31];
int a[31];
int n,x,m = 1;
vector<PII> tmp;
int main() {
cin >> n >> x;
for(int i = 1;i<=n;i++)
{
cin >> a[i];
m *= 2;
}
for(int i=1 ;i <=m ;i++)
{
int sum = 0;
int c = 0;
for(int j = 0 ; j<n ;j++ )
{
if( i >> j & 1)
{
sum += a[j+1];
c ++;
}
}
if(sum >= x)
tmp.push_back({ sum , c});
}
sort(tmp.begin(),tmp.end());
cout << tmp[0].first<< endl;
return 0;
}
-
裸 01背包
设sum为所有参考书价格总和,题目可以理解为在sum-x价格内,最大化被删除的书价格总和,这样就可以把这个问题看作经典的01背包问题
// dp[i][j]为 前i本书在j的价格上限内,可以选择删掉书最大的价格总和
for(int i = 1; i<=n ;i++)
for(int j = 0 ; j <= sum ; j++)
{
// 选或不选
// dp[i][j]=max(dp[i-1][j-a[i]]+a[i],dp[i-1][j])
if(j > a[i]) //可选的条件下
d[i][j] = max(dp[i-1][j-a[i] ] + a[i] , dp[i-1][j]);
else
d[i][j] = dp[i-1][j];
}
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 300010;
int a[31];
int d[31][N];
int n,x;
int main() {
cin >> n >> x;
int sum0 = 0;
for(int i = 1;i<=n;i++)
{
cin >> a[i];
sum0 += a[i];
}
// dp[i][j]为 前i本书在j的价格上限内,可以选择删掉书最大的价格总和
int sum = sum0 - x;
for(int i = 1; i<=n ;i++)
for(int j = 0 ; j <= sum ; j++)
{
// 选或不选
// dp[i][j]=max(dp[i-1][j-a[i]]+a[i],dp[i-1][j])
if(j > a[i]) //可选的条件下
d[i][j] = max(d[i-1][j-a[i] ] + a[i] , d[i-1][j]);
else
d[i][j] = d[i-1][j];
}
cout << sum0 - d[n][sum];
return 0;
}
第25次
1.未初始化警告
#include<iostream>
#include<cstring>
using namespace std;
const int N = 100010;
int ax[N];
int n,m;
int main()
{
scanf("%d %d",&n,&m);
int res = 0;
memset(ax,-1,sizeof ax);
while(m--)
{
int x,y;
scanf("%d %d",&x,&y);
if(y!=0 && ax[y]==-1)
res++;
ax[x] = 1;
}
cout << res;
return 0;
}
2.出行计划
//用二分,只能过一半的数据,10^5 * 10^5
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N = 100010;
int n,m,Time,temp;
int ct[N],caq[N];
int Max=0; //活动中核酸的最大宽限
int main()
{
scanf("%d%d%d",&n,&m,&Time);
for(int i = 1; i<=n; i++)
{
int t,c;
scanf("%d%d",&t,&c);
ct[i] = t;
caq[i] = c;
Max = max(Max,c);
}
/*
for(int i = 1;i<=40;i++)
{
cout << c_t[i] <<endl ;
//cout << ct[i] << " " << caq[i] << " " << c_t[i]<<endl;
}
*/
while(m--)
{
int q,q1,start,end;
scanf("%d",&q);
q1 = q + Time; //获得核酸结果的时间
//cout << "aquire:" << q1 <<endl;
start = c_t[q1];
//cout << "start = " << start << endl;
int res = 0;
for(int i = start ; i<= n ; i++)
{
if(( q1+caq[i]-1 ) >= ct[i])
{
//cout << q1+caq[i]-1 << " >= " << ct[i] <<endl;
res++;
}
}
cout << res <<endl;
//cout << "-------------------" <<endl;
}
}
3.计算资源调度器
第23次
2.非零段划分
- unique函数的函数原型如下:
1.只有两个参数,且参数类型都是迭代器:
iterator unique(iterator it_1,iterator it_2);
这种类型的unique函数是我们最常用的形式。其中这两个参数表示对容器中[it_1,it_2)范围的元素进行去重(注:区间是前闭后开,即不包含it_2所指的元素),返回值是一个迭代器,它指向的是去重后容器中不重复序列的最后一个元素的下一个元素。
岛屿问题
/*岛屿问题: 海平面下降,露出的岛屿,只有高于某个数的数会露出来 */
/* 只遍历 a[] 中出现的值,降低时间复杂度 */
#include<iostream>
#include<algorithm>
#include<cstring>
#include<vector>
using namespace std;
const int N = 500010,M = 10010;
int n;
int a[N];
int cnt[M]; //cnt[i] 高度为i的位置的 顶/底数 ,随后从上往下加和就是 i 位置的岛屿数
vector<int> tmp;
int main()
{
cin >> n;
for(int i= 1;i<=n;i++)
{
scanf("%d",&a[i]);
if(a[i])
tmp.push_back(a[i]);
}
n = unique(a+1, a+n+1) - a - 1;
a[0] = a[n + 1] = 0;
for(int i=1;i<=n;i++)
{
int x = a[i-1],y = a[i] ,z = a[i+1];
if(x>y && z> y) cnt[y] --; //遇到 a[i] 是谷底的情况,说明两个谷底连接起来了,岛屿数减1
else if(x<y && z<y) cnt[y]++; //遇到 a[i] 是顶的情况,说明出现了一个岛屿
//其他情况:上升、下降、相平
}
int res=0 ,sum=0;
sort(tmp.rbegin(),tmp.rend());
tmp.erase(unique(tmp.begin(), tmp.end()), tmp.end());
/*
如果不剪枝,则从 M-1 到 1 挨个遍历
for(int i=M-1;i;i--)
{
sum += cnt[i];
res = max(res , sum);
}
*/
for(int i=0;i<tmp.size();i++)
{
//cout << tmp[i] << endl;
int h = tmp[i];
sum += cnt[h];
res = max(res , sum);
}
cout << res ;
return 0;
}
暴力解法
4.收集卡片
/* 用记忆化搜索 */
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 16,M = 1 << 16; //状态压缩
int n,m;
double p[N]; //抽中的概率
double f[M][N*5+1]; //卡牌的最大数量是 15*5 + 1
//值表示:期望
double dp(int state,int coins,int r)
// 返回值是状态为state,硬币数量为coins,还剩r个牌没抽到 时的期望(到这个state的概率乘抽牌次数)
{
auto& v = f[state][coins]; //别名表示
if(v>=0) return v; //概率已经计算过了就不再重复(记忆化搜索的关键)
if(coins >= r*m) return v = 0; //这是判断是否结束(到达叶子节点)的标志
v = 0; //先把v由 -1 改为 0
for(int i=0;i<n;i++)
{
if(state >> i & 1) //当第i位为1时,如果再抽到这个卡片,就会变成一个硬币
v += p[i] * (dp(state,coins+1,r) + 1);
else
v += p[i] * (dp(state | 1 << i,coins,r-1) + 1);
}
return v;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i =0;i<n;i++) scanf("%lf",&p[i]);
memset(f,-1,sizeof f);
printf("%.10lf\n",dp(0,0,n));
return 0;
}
第22次
1、
2、
- 预处理前缀和
3、校门外的树 (DP)
骗分技巧
一、输出 " -1 "
二、暴搜
三、打表
当数据比较小的时候,可以先用暴力跑出来,然后打表
四、非递归函数加 inline
蓝桥杯
一、递归与递推
1.递归实现指数型枚举
/* dfs */
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 16;
int st[N]; // 0 为待选 ,1 为选 , 2为不选
int n;
void dfs(int u)
{
if(u>n)
{
for(int i=1;i<=n;i++) //得益于dfs才能完整的保留st[1~N]
if(st[i]==1)
cout << i << " ";
cout << endl;
return;
}
st[u] = 2; // 第u个选
dfs(u+1);
st[u] = 0; //恢复现场
st[u] = 1;
dfs(u+1);
st[u] = 0;
}
int main()
{
cin >> n;
dfs(1);
return 0;
}
2.递归实现排列型枚举
/* dfs 实现全排列 ,按字典序 */
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 10;
int n;
int st[N]; //记录第i个坑放哪个数
bool has[N]; //记录已经用过了的数
void dfs(int u)
{
if(u>n)
{
for(int i = 1;i<= n ;i++)
{
cout << st[i] << " ";
}
puts("");
}
for(int i=1;i<=n;i++)
{
if(has[i]==false)
{
has[i] = true;
st[u] = i;
dfs(u+1);
st[u] = 0;
has[i] = false;
}
}
}
int main()
{
cin >> n;
dfs(1);
return 0;
}
3.递归实现组合型枚举
- 不考虑顺序
//组合数多加一个参数表示下一次 从哪里开始
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 26;
int n,m;
int way[N]; //放置取出的m个数
void dfs(int u,int start) // 再从第start个结点选,已经选了u个了
{
if( u + n - start < m ) return ; // 剪枝
// 正在选第u个,已经选了 u-1 个,u-1+(n-start+1) = u+n-start < m
// 正在选第1个 u=1,start=1, u-1+(n-start+1)=1-1+5-1+1=5 没问题
// 正在选第2个 u=2,当第二个选4时,start=i+1=5,u-1+(n-start+1)=2-1+(5-5+1)=2 < 3 有问题
if(u>m)
{
for(int i=1;i<=m;i++)
cout << way[i] << " ";
puts("");
return;
}
for(int i = start;i<=n;i++)
{
way[u] = i;
dfs(u+1,i + 1); //注意是 i+1
}
}
int main()
{
cin >> n >> m;
dfs(1,1);
return 0;
}
1209.带分数
/* dfs全排列 + 分出三个数 */
#include <iostream>
using namespace std;
const int N = 10;
int target; // 题目给出的目标数
int num[N]; // 保存全排列的结果
bool used[N]; // 生成全排列过程中标记是否使用过
int cnt; // 计数,最后输出的结果
// 计算num数组中一段的数是多少
int calc(int l, int r) {
int res = 0;
for (int i = l; i <= r; i++) {
res = res * 10 + num[i];
}
return res;
}
// 生成全排列
// 当全排列生成后进行分段
void dfs(int u) {
// 用两层循环分成三段
if (u == 9) {
for (int i = 0; i < 7; i++) {
for (int j = i + 1; j < 8; j++) {
int a = calc(0, i);
int b = calc(i + 1, j);
int c = calc(j + 1, 8);
// 注意判断条件,因为C++中除法是整除,所以要转化为加减乘来计算
if (a * c + b == c * target) {
cnt++;
}
}
}
return;
}
// 搜索模板
for (int i = 1; i <= 9; i++) {
if (!used[i]) {
used[i] = true; // 标记使用
num[u] = i;
dfs(u + 1);
used[i] = false; // 还原现场
}
}
}
int main() {
scanf("%d", &target);
dfs(0);
printf("%d\n", cnt);
return 0;
}
作者:Daniel丶y
链接:https://www.acwing.com/solution/content/6724/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
95.费解的开关(位运算 + 枚举 / BFS+哈希表 / DFS + 二进制枚举)
方法一(位运算模拟+枚举)
#include<iostream>
#include<cstring>
using namespace std;
const int N=6;
char g[N][N],backup[N][N];
int dx[5]={-1,0,1,0,0},dy[5]={0,1,0,-1,0};
void turn(int x,int y)
{
for(int i=0;i<5;i++)
{
int a=x+dx[i],b=y+dy[i];
if(a>=0&&a<5&&b>=0&&b<5)
g[a][b]^=1;
}
}
int main()
{
int T;
cin>>T;
while(T--)
{
for(int i=0;i<5;i++)cin>>g[i];
int res=10;
for(int op=0;op<32;op++)
{
memcpy(backup,g,sizeof g);
int step=0;
for(int i=0;i<5;i++)
{
if(op>>i&1)
{
step++;
turn(0,i);
}
}
for(int i=0;i<4;i++)
{
for(int j=0;j<5;j++)
{
if(g[i][j]=='0')
{
turn(i+1,j);
step++;
}
}
}
bool dark=false;
for(int i=0;i<5;i++)
{
if(g[4][i]=='0')
{
dark=true;
break;
}
}
if(!dark)res=min(res,step);
memcpy(g,backup,sizeof g);
}
if(res>6)res=-1;
cout<<res<<endl;
}
}
方法二(BFS+哈希表)
- 逆向搜索,从开关全亮的状态开始搜索,将6步以内的的所有状态记录下来。
- 用25位的二进制数来记录5*5的地图,对输入的状态进行哈希得到步数;。
- 读入数据,判断步数。
#include <iostream>
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 5e7+10;
const ll mod = 1000000007;
unordered_map<int,int>vis;
int build(int x,int p)
{
x ^= ( 1<< p);
if(p % 5) x ^= (1 << (p - 1));
if(p >= 5) x ^= (1 << (p - 5));
if(p < 20) x ^= (1 << (p + 5));
if((p % 5) < 4) x ^= (1 << (p + 1));
return x;
}
void bfs()
{
queue<int> que;
int now = (1 << 25) - 1;
vis[now] = 1;
que.push(now);
while(!que.empty())
{
int top = que.front();
que.pop();
if(vis[top] == 7) break;
for(int i = 0;i < 25;i++)
{
now = build(top,i);
if(!vis.count(now))
{
vis[now] = vis[top] + 1;
que.push(now);
}
}
}
}
int main()
{
bfs();
int T;
scanf("%d",&T);
while(T--)
{
int sum = 0;
for(int i = 0;i < 25;i++)
{
char t;
cin >> t;
sum += ((t - '0') << i);
}
printf("%d\n",vis[sum] - 1);
}
return 0;
}
// 哈希表的速度不如数组
#include<bits/stdc++.h>
using namespace std;
int mapp[1<<25]; //int 内存超限 short int 不会
int change(int temp,int x)
{
int t = temp;
temp^=(1<<x);
if(x%5!=0) temp^=1<<(x-1); //判断左右上下边界
if(x%5!=4) temp^=1<<(x+1);
if(x/5!=0) temp^=1<<(x-5);
if(x/5!=4) temp^=1<<(x+5);
if(mapp[temp]!=-1)
return -1;
mapp[temp] = mapp[t]+1;
return temp;
}
void bfs() //BFS搜索所有状态
{
queue<int>que;
memset(mapp,-1,sizeof(mapp)); // 先把所有状态都置为 -1
int temp=(1<<25)-1;
que.push(temp);
mapp[temp] = 0;
while(!que.empty()){
temp = que.front();
que.pop();
if(mapp[temp]>=6) continue;
for(int i=0;i<25;i++){
int x = change(temp,i);
if(x==-1) continue; // 与之前有重复
que.push(x);
}
}
}
int main()
{
std::ios::sync_with_stdio(false);std::cin.tie(0);std::cout.tie(0); //cin cout 加速
bfs();
int T;
cin>>T;
while(T--){
int index=0;
string str;
for(int i=0;i<5;i++){
cin>>str;
for(int j=0;j<5;j++){
index<<=1;
index ^= (str[j]-'0');
}
}
cout<<mapp[index]<<endl;
}
return 0;
}
116.飞行员兄弟( 位运算+ 枚举 )
二、二分与前缀和
- 一个题目具有单调性的话,一定可以用二分来做,如果不具有单调性,有时可以用二分来做
730.机器人跳跃问题
/*
因为每个数 E的增长是指数级别的 ,所以啥都能爆,
经过放缩得到,只要有 E 大于最高值Max,以后的所有 E 都大于 Max
所以就可以提前true了
*/
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 100010;
int h[N],E;
int n;
int Max;
int flag;
//check函数为
/*
flag = turn;
for(int i=1;i<=n;i++)
if( 2*E - h[i+1] < 0 )
flag = false;
=>
*/
bool check(int E)
{
for(int i=1; i<=n ; i++)
{
E = 2*E - h[i];
if(E >= Max) //防爆long long ,放缩化简条件,也提升了速度
return true;
if(E<0)
return false;
}
return true;
}
int main()
{
cin >> n;
for(int i=1;i<=n;i++)
{
scanf("%d",&h[i]);
if(h[i]>Max)
Max = h[i];
}
//因为是单调的,所以用二分找最小,就不用从 1 开始找一直遍历到答案
int l = 0,r = 100000;
while(l<r)
{
int mid = (l+r)>>1;
if(check(mid)) r = mid;
else l = mid+1;
}
cout << l;
return 0;
}
1227.分巧克力
- 最主要的是要知道公式:边长为 a 时,最大切割数量为[w / a] * [h / a] ;
- 然后用二分的方法来找 个数大于 k 的最小值 ,然后横坐标就是边长 a
/*
- 因为 每块大小 与 分的块数 有递减的函数关系,所以找到大于等于人数的最小块数就好了,
然后对应的块大小就是答案,
- 注意:这里的二分是递减情况下的,
*/
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 100010;
int n,k;
int h[N],w[N];
bool check(int mid) //以mid*mid 给每个巧克力分块,分的个数
{
int res = 0;
for(int i=0;i<n;i++)
{
res += (h[i]/mid)*(w[i]/mid); //神奇的公式,求指定长度的块数
if(res >= k)
return true;
}
return false;
}
int main()
{
scanf("%d%d",&n,&k);
for(int i=0;i<n;i++)
scanf("%d%d",&h[i],&w[i] ); //读入每个巧克力块的长宽
int l = 1 ,r = 1e5;
while(l<r)
{
int mid = (l+r+1)/2;
if(check(mid)) l = mid; // 当这种大小下的个数大于等于人数时,往右走(同时可以满足的,尺寸越大越好)
else r = mid-1;
}
cout << l;
}
1221.四平方和
- 预处理(用结构体存 + sort 排序 ) + 二分查找
/*
前两个循环遍历;
后两个先预处理,结构体的方式存进,然后用二分或者哈希表查找
*/
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 5000010;
int n,m ;
struct Sum{
int s,c,d;
// bool operator < (const Sum &t)const
// {
// if(s!=t.s) return s < t.s;
// if(c!=t.c) return c < t.c;
// return d < t.d ;
// }
}sum[N];
bool cmp(Sum& a,Sum& b)
{
// if(a->s!=b->s) return a->s < b->s;
// if(a->c!=b->c) return a->c < b->c;
// return a->d < b->d ;
if(a.s!=b.s) return a.s < b.s;
if(a.c!=b.c) return a.c < b.c;
return a.d < b.d ;
}
int main()
{
cin >> n;
for(int c = 0; c*c <= n ; c++)
for(int d = c; c*c + d*d <= n ;d++)
sum[m++] = {c*c + d*d ,c,d};
sort(sum+1, sum + m,cmp); // 排序
for(int a = 0; a*a <= n ;a++) // 查找 a,b
for(int b = a; a*a + b*b <= n; b++)
{
int t = n- a*a -b*b ;
int l=1,r = m; // 二分找结构体的 c,d
while(l<r)
{
int mid = (l+r)/2; // 找字典序最小的
if(sum[mid].s >= t) r = mid;
else l = mid+1;
// int mid = (l+r+1)/2; //这种是、、、、、
// if(sum[mid].s <= t) l = mid;
// else r = mid-1;
}
if(t==sum[l].s)
{
printf("%d %d %d %d",a,b,sum[l].c,sum[l].d);
return 0;
}
}
return 0;
}
1230.K倍区间
/* 简单来讲,sum[r] % k 和 sum[l-1] % k 的余数如果相等,那么sum[r] - sum[l-1]的差值必然是k的倍数 */
/* 前缀和保存 前 i 个的和 sum[i] , 记录sum[i] % k ,如果 count[ sum[i] % k ]>1, 组合数,从中取两个 */
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
typedef long long LL;
const int N = 100010;
LL sumk[N] ; // sum % k
int a[N];
int n,k;
int main()
{
cin >> n >> k;
LL temp = 0;
LL res = 0;
for(int i=0;i<n;i++)
{
scanf("%d",&a[i]);
temp += a[i];
int t = temp % k;
sumk[t] ++;
}
res = sumk[0];
for(int i=0;i<N;i++)
{
if(sumk[i] > 1)
res += sumk[i]*(sumk[i]-1)/2 ;
}
cout << res <<endl;
return 0;
}
三、数学与简单DP
1211.蚂蚁感冒
/* 相撞后掉头相当于直接穿过去 */
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 110;
int x[N];
int n;
int main()
{
cin >> n;
for(int i=0;i<n ;i++)
{
cin >> x[i];
}
int ltor = 0;
int rtol = 0;
int temp = x[0];
for(int i = 1 ; i<n; i++)
{
if( x[i] < 0 && abs(x[i]) > abs(temp) ) // 右边向左
rtol ++;
if( x[i] > 0 && abs(x[i]) < abs(temp)) // 左边向右
ltor ++;
}
if( rtol ==0 && temp > 0 ) cout << 1 << endl;
else if( ltor ==0 && temp < 0) cout << 1 << endl;
else
cout << ltor + rtol +1 << endl;
return 0;
}
1015.摘花生
- 边界:下标中有 i - 1 从 1开始
// 时间 : 100*100*100 = 1000000 1百万
// 1000*200 < int
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 110 ;
int T;
int a[N][N],d[N][N];
int main()
{
cin >> T ;
while(T--)
{
int r,c;
cin >> r >> c;
for(int i = 1;i<= r ;i++)
for(int j=1;j<= c ;j++)
scanf("%d",&a[i][j]);
for(int i = 1;i<= r ;i++)
for(int j=1;j<= c ;j++)
d[i][j] = max(d[i-1][j] + a[i][j] , d[i][j-1] + a[i][j]);
cout << d[r][c] <<endl;
}
return 0;
}
1212.地宫取宝 (线性DP)
- 用暴力吧,但是记忆化搜索
#include<bits/stdc++.h>
using namespace std;
const int mod = 1000000007;
int a[55][55];
#define ll long long
ll dp[55][55][15][15]; //记忆化搜索,用空间换时间 dp[n][m][此时携带物品中的最大价值maxn][已携带宝物数量t(不包括当前位置)]
ll n,m,k;
ll dfs(int x,int y,int maxn,int t){
if(dp[x][y][maxn][t]!=-1) //先看之前记忆过吗
return dp[x][y][maxn][t];
ll ans=0;
if(t>k||y>m||x>n)
return 0;
if(x==n&&y==m){
if(t==k||(t==k-1&&a[x][y]>maxn)) {
return 1;
}
return 0;
}
if(a[x][y]>maxn){
ans+=dfs(x+1,y,a[x][y],t+1);
ans+=dfs(x,y+1,a[x][y],t+1);
}
ans+=dfs(x,y+1,maxn,t);
ans+=dfs(x+1,y,maxn,t);
ans%=mod;
dp[x][y][maxn][t]=ans;
return ans;
}
int main(){
cin>>n>>m>>k;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
cin>>a[i][j];
a[i][j]+=1; //对结果没有影响
}
}
memset(dp,-1,sizeof(dp));
cout<<(dfs(1,1,0,0)%mod);
}
正解:
- 注意 : 只有两个需要 初始化 f(1,1,1,w(1,1) ) = 1 、 f( 1,1,0,-1 ) = 1
- 这里这个 -1 是因为在第一个(1,1)不取的时候,后面取值时可以取任何数(包括 0),所以写为 -1 而不是 0 ,
- 但是 数组的下标不能为 -1 ,所以都加 1 ,-1~13 变为 0~14
/* 50*50*12*13*n f[i][j][k][c] n 是决策的复杂度 */
/* 所有从起点走到(i,j),且已经取了k件物品,且最后一件物品的价值时C的合法方案的集合 */
/* 摘花生 + 最长子序列(线性DP) */
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 55,MOD = 1000000007;
int n,m,k;
int w[N][N];
int f[N][N][13][14]; // 存在方案的个数
int main()
{
cin >> n >> m >> k;
for(int i= 1;i<= n ;i++)
for(int j = 1; j<=m ;j++ )
{
cin >> w[i][j];
w[i][j]++ ; // 为了使 -1 变为 0
}
f[1][1][1][w[1][1]] = 1; //选第一个数
f[1][1][0][0] = 1 ; // 不选第一个数 ,本来为 -1 但是
for(int i=1 ; i<= n ;i ++ )
for(int j=1 ; j <= m ; j++)
{
if(i==1 && j==1 ) continue;
for(int u = 0 ; u<= k ; u++)
for(int v = 0;v <= 13 ; v++)
{
int &val = f[i][j][u][v];
val = (val + f[i-1][j][u][v]) % MOD ; // 不取的时候可以u为0
val = (val + f[i][j-1][u][v]) % MOD ;
if(u>0 && v== w[i][j]) // 取得时候 u 不能为0 ,看看图就知道了!
{
for(int c=0 ; c < v ;c++ )
{
val = (val + f[i-1][j][u-1][c]) % MOD;
val = (val + f[i][j-1][u-1][c]) % MOD;
}
}
}
}
int res = 0;
for(int i=0 ; i<=13 ; i++) res = (res + f[n][m][k][i] ) % MOD;
cout << res <<endl;
return 0;
}
四、枚举、模拟与排序
1236.递增三元组
#前缀和做法
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
typedef long long LL;
const int N = 100010 ;
int s1[N],s2[N],s3[N]; // s[i] : 0~i 出现的总个数
int a[N],b[N],c[N]; // a[i] :为 i 的个数
int cnt1[N],cnt2[N],cnt3[N];
int n;
LL res = 0;
int main()
{
cin >> n;
for(int i = 1;i<=n ; i++)
{
scanf("%d",&a[i]);
cnt1[a[i]] ++ ;
}
for(int i = 1;i<=n ; i++)
{
scanf("%d",&b[i]);
cnt2[b[i]] ++ ;
}
for(int i = 1;i<=n ; i++)
{
scanf("%d",&c[i]);
cnt3[c[i]] ++ ;
}
s1[0] = cnt1[0];
s2[0] = cnt2[0];
s3[0] = cnt3[0];
for(int i = 1 ;i<=N ; i++)
{
s1[i] = s1[i-1] + cnt1[i];
s2[i] = s2[i-1] + cnt2[i];
s3[i] = s3[i-1] + cnt3[i];
}
for(int i = 1 ;i<=n ;i++)
{
res += (LL)s1[b[i]-1]*( s3[100000] - s3[b[i]] );
}
printf("%lld",res);
return 0;
}
#排序 + 二分
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
typedef long long LL;
const int N = 100010 ;
int a[N],b[N],c[N]; // a[i] :为 i 的个数
int n;
LL res = 0;
int main()
{
cin >> n;
for(int i = 1;i<=n ; i++)
{
scanf("%d",&a[i]);
}
for(int i = 1;i<=n ; i++)
{
scanf("%d",&b[i]);
}
for(int i = 1;i<=n ; i++)
{
scanf("%d",&c[i]);
}
sort(a+1,a+n+1); // 注意 sort要从 [1,n+1] 或 [0,n] 反正要多一个就行
sort(b+1,b+n+1);
sort(c+1,c+n+1);
for(int i = 1;i<=n ;i++)
{
int t = b[i];
int num1 = 0 , num2 =0;
int l = 1 ,r = n;
while(l<r)
{
int mid = (l+r+1)/2;
if(t > a[mid]) l = mid;
else r = mid-1;
}
if(a[l]>=t) num1 = 0;
else num1 = l;
l = 1,r = n;
while(l<r)
{
int mid = (l+r)/2;
if(t<c[mid]) r = mid;
else l = mid+1;
}
if(c[l]<=t) num2 = n+1;
else num2 = r;
res += ((LL)num1* (LL)(n-num2+1));
}
printf("%lld",res);
return 0;
}
466.回文日期
- 时间暴力 这个不对 ,因为没有考虑日期的正确性
// 反转
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
int time1,time2 ;
int change(int a)
{
int temp = 0 ;
while(a)
{
temp = ( temp + a % 10) * 10 ;
a/=10;
}
return temp/10;
}
int main()
{
cin >> time1 >> time2;
int t = time1;
int res = 0;
for(int i=t ;i<= time2 ; i++ )
if(i - change(i) == 0 )
res ++;
cout << res;
return 0;
}
- 正解
- 1.枚举回文数
- 2.判断是否再区间内
- 3.再判断日期是否合法
- 月份在1~12
- 号存在 month[ 13 ] = { 0 , 31 , 28 ,30 , 31 … }
- 闰年
- ① 年份不能被 100 整除 ,且能被 4 整除
- ② 年份能被 400 整除
// 只从 1000 ~ 9999
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
int time1,time2 ;
int days[13] = {0,31,28,31,30,31,30,31,31,30,31,30,31};
bool check_valid(int date)
{
int year = date / 10000 ;
int month = date % 10000 / 100;
int day = date % 100;
if(month==0 || month > 12 ) return false;
if(day==0 || (month != 2 && day > days[month]) ) return false;
if(month == 2)
{
int leap = 0 ;
if(year % 100 && year % 4==0 || year % 400 ==0)
leap = 1;
else
leap = 0;
if(day > leap+28 ) return false ;
}
return true;
}
int main()
{
cin >> time1 >> time2;
int res = 0;
for(int i = 1000 ;i< 10000 ; i++ )
{
int date = i ,x = i ;
for(int j = 0 ;j < 4 ;j++ )
date = date * 10 + x % 10 , x/= 10;
if( time1 <= date && date <= time2 && check_valid(date) )
res++;
}
cout << res;
return 0;
}
1219.移动距离
- 欧几里得距离 : 直线距离
- 曼哈顿距离 : 只能按照直线走
和为T(位运算版暴搜)
#include<bits/stdc++.h>
using namespace std;
int main(){
int n;
int T;
cin>>n;
int nums[n];
for (int i = 0; i < n; i++)cin>>nums[i];
cin>>T;
//用1左移n,用于方便表示长度为n的数组的二进制序列表,比如1<<3=1000,而长度为3的数组需要从001到111
int max = 1<<n;
int count = 0;
for(int i=1;i<max;i++){
int x = i;
int sum = 0;
//计算所选取组合的sum
for (int j = 0; j < n; j++){
//一旦x为0,则1取完
if(x==0)
break;
if(x&(1<<j)){
sum += nums[j];
//每次消耗一个1
x = x&(x-1);
}
}//若sum和为目标值T则输出
if(sum==T){
int t = i;
for(int j = 0; j < n; j++){
if(t==0)
break;
if(t&(1<<j)){
cout<<nums[j]<<" ";
//每次消耗一个1
t = t&(t-1);
}
}
count++;
cout<<endl;
}
}
cout<<count;
return 0;
}
五、树状数组与线段树
树状数组(快速求前缀和)
对比前缀和算法:
- 优势在于可以快速动态修改值 由O(n)->O(log n)
- 实现方法不一样,前缀和算法 sum[i] 保存的是 a[1~i] 的和,而 树状数组中 c[ i ] = a[ i-lowbit(i) ] + a[ i-lowbit(i)+1 ] … + a[ i ] ;
O(log n)
- 给某个位置加上一个数
- 求某个前缀和
构造方式
lowbit(int x ) // 如果x的二进制最后有k个0,则返回 2^k
{ // 该函数用于定位一个节点的父节点,因为父节点编号为当前节点编号减去 lowbit 所得的值。
return x & -x ; // 记住就好,原理不用
}
c[x] = a[x-lowbit(x)] + ...... +a[x]; // 核心!!!!!!
单点修改
// 修改a[x] , 只需要O(log n),不需要O(n)
for(int i = x;i<=n;i+=lowbit(i))
c[i] += v;
区间查询
// 求前缀和,前x个
int res = 0;
for(int i=x ; i > 0 ; i -= lowbit(i) )
res += c[i] ;
通过差分的方式可以转换为:区间修改、单点查询
也可以通过差分的方式可以转换为:区间修改、区间查询
例子
1264. 动态求连续区间和
#include<iostream>
using namespace std;
const int N = 100010;
int n,m;
int a[N];
int c[N];
int lowbit(int x) // x二进制时 尾部 有多少个零,一个0 返回2^1,两个0 返回2^2
{
return x & (-x);
}
int query(int l,int r) // 求两次前缀和,相减
{
int sum = 0 ;
for(int i = r; i>0 ; i-=lowbit(i))
sum += c[i];
for(int i = l-1; i>0 ; i-=lowbit(i))
sum -= c[i];
return sum;
}
int modify(int x,int v)
{
for(int i=x ; i<=n ;i+= lowbit(i)) // 只更新父节点,i+=lowbit(i)
c[i] += v;
}
void build() // 建树过程可以理解为 modify(i,a[i]),这样建树的效率为 O(nlogn)
{ // 而如果用定义建树 c[x] = a[x-lowbit(x)]+...+a[x];效率 O(n^2)
for(int i=1;i<=n ; i++)
{
modify(i,a[i]);
}
}
int main()
{
cin >> n >> m ;
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
build();
while(m--)
{
int k,a,b;
scanf("%d%d%d",&k,&a,&b);
if(k==0)
cout << query(a,b) <<endl ;
else
modify(a,b);
}
return 0;
}
1265. 数星星
树状数组思路:
- 第 m 个星星的左下方点一定是从 序号为 1~m 中选 , y 是最大不考虑,只需考虑 序号为 1~m 中哪个横坐标小于第m个星星的横坐标就可以
- ** ** ** 所以开一个数组 : a[ i ] ,表示横坐标为 i 下,当前有多少个星星 (当前的意思是说,以后a[i] 可能会增加 ) ** ** ** **
- 所以 第 m 个星星的左下星星个数为:a[1] + … + a[ m的横坐标 ] ,转变为前缀和问题
- 对应两个操作:a[ i ] 增加操作 、查询前缀和操作
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 15010,M = 32010 ;
int n;
int a[N];
int c[M]; // 因为树状数组存的是横坐标为 i 的点个数,横坐标范围是32000
int level[N] ;
int lowbit(int x)
{
return x & (-x);
}
int query(int x) // 查询前 x 个的和
{
int res = 0;
for(int i = x;i>0;i -= lowbit(i))
{
res += c[i];
}
return res;
}
int add(int x) // 修改 增加a[i]
{
for(int i=x; i<M; i+=lowbit(i) )
{
c[i]++ ;
}
}
int main()
{
cin >> n ;
for(int i=0;i<n;i++)
{
int x,y;
cin >> x >> y; // 边输入边操作,也可以用a[N]先存下来
x++; // 树状数组要求从 1 开始
level[ query(x) ]++; // 先查询这个结点的level,这样就不会包含自己了
add(x);
}
for(int i= 0;i<n;i++)
cout << level[i] <<endl;
return 0;
}
暴力前缀和思想:
- 输入是按照 y 的增序 ,若y相同 x 增序
- 看第 i 个点是否在 其后面点的左下方,如果是,就将后面点的 level 加 1
for(int i = 1;i<=n ;i++)
{
for(int j = i+1 ; j<=n ; j++)
if(a_x[j] >= a_x[i]) // y的顺序自动增序,不用考虑
level[j] ++;
}
用x增序排序的暴力做法,用到sort
// 排序大有用处,不必完全扫描 N*N 个,扫描 n*(n+1)/2 个,其实都暴力了,还要什么自行车
#include<iostream>
#include<algorithm>
using namespace std;
#define x first
#define y second
const int N=1502;
typedef pair<int,int> PII;
int arr[N];
PII order[N];
int main()
{
int n;
cin>>n;
for(int i=0; i<n; i++)
{
cin>>order[i].x>>order[i].y;
}
sort(order,order+n);
for(int i=n-1; i>=0; i--)
{
int cnt=0;
for(int j=0; j<i; j++)
{
if(order[i].x>=order[j].x&&order[i].y>=order[j].y)
{
cnt++;
}
}
arr[cnt]++;
}
for(int i=0; i<n; i++)
{
cout<<arr[i]<<endl;
}
}
1215. 小朋友排队(贪心+树状数组 / 归并排序)
- 贪心证明得到:所有小朋友交换的总次数等于总逆序对的个数 , 并且 每个小朋友的交换次数等于 前面身高更高小朋友数 + 后面身高更低的小朋友数
树状数组解法
#include<iostream>
#include<cstring>
using namespace std;
const int N = 1e6 + 10;
typedef long long ll;
int n;
int c[N], a[N];
int sum[N];//sum数组存储每个小朋友的不高兴度
int lowbit(int x) {
return x & -x;
}
void add(int x, int v) {
for (; x < N; x += lowbit(x)) {
c[x] += v;
}
}
int ask(int x) {
int ans = 0;
for (; x; x -= lowbit(x)) {
ans += c[x];
}
return ans;
}
int main() {
cin >> n;
for (int i = 0; i < n; i++) {
cin >> a[i];
a[i]++; //身高是从0开始,所以++ 从1开始
}
// 求每个数前面有多少个数比它大
for (int i = 0; i < n; i++) {
sum[i] = ask(N - 1) - ask(a[i]);
add(a[i], 1);
}
//清空树状数组
memset(c, 0, sizeof c);
//找出比这个数小的数有多少个
//注意这里必须倒着更新,否则无法算出高的层的数值
for (int i = n - 1; i >= 0; i--) {
sum[i] += ask(a[i] - 1);
add(a[i], 1);
}
ll res = 0;
for (int i = 0; i < n; i++) {
//等差数列求和 不高兴度的总和为从1+2+..+sum[i]
res += (ll) sum[i] * (sum[i] + 1) / 2;
}
cout << res << endl;
return 0;
}
作者:Bug-Free
链接:https://www.acwing.com/solution/content/22104/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
归并排序解法
- 这种strut 忽视追踪方式很好
// 通过贪心可以证明 ,归并排序(求逆序对个数)可以顺便把每个小朋友的交换次数确定
// 这里的难点在于 归并过程中小朋友的位置不断变化,不能用位置来对应小朋友
// 使用结构体:包含身高,上一次位置,已经移动的距离
// 妙,从整个过程中位置的变化考虑,而不是盯着每一次细节交换
#include<iostream>
using namespace std;
const int N = 1e5 + 10;
typedef long long ll;
int n;
typedef struct Node{
int h; //身高
int idx;//上一次位置
ll m; //移动长度
}node;
node q[N], tmp[N];
void merge_sort(int l, int r) {
if (l >= r) {
return;
}
int mid = l + r >> 1;
merge_sort(l, mid), merge_sort(mid + 1, r);
int k = l, i = l, j = mid + 1;
while (i <= mid && j <= r) {
if (q[i].h <= q[j].h) {
tmp[k++] = q[i++];
} else {
tmp[k++] = q[j++];
}
}
while (i <= mid) {
tmp[k++] = q[i++];
}
while (j <= r) {
tmp[k++] = q[j++];
}
//更新所在位置 idx
//将本次移动长度累加到总长度
for (i = l; i < k; i++) { // 妙!!!
//从整个过程中位置的变化考虑,而不是盯着每一次细节交换
q[i] = {tmp[i].h, i, tmp[i].m + abs(tmp[i].idx - i)};
}
}
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> q[i].h;
q[i].idx = i;
}
merge_sort(1, n);
ll cnt = 0;
for (int i = 1; i <= n; i++) {
cnt += (q[i].m + 1) * q[i].m / 2;
}
cout << cnt << endl;
return 0;
}
作者:Bug-Free
链接:https://www.acwing.com/solution/content/22104/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
暴力解法
- 这个题实际上是逆序对,只不过稍稍变形,我们来看对于每个数来说他的逆序对怎么算,在这个数之前比这个数大数+这个数之后比这个数小的数就是这个数的逆序对的个数(好绕口。。。慢慢体会)。这个逆序对个数其实代表了该数换位置的次数。
#include<bits/stdc++.h>
using namespace std;
const int N = 100010;
int n;
long long ans;
long long a[N],num[N];
long long summary(long long x){
int k;
k=(1+x)*x/2;
return k;
}
int main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
}
for(int i=1;i<=n;i++){
long long res=0;
for(int j=1;j<i;j++){
if(a[j]>a[i]){
res++;
}
}
for(int j=i+1;j<=n;j++){
if(a[j]<a[i]){
res++;
}
}
res=summary(res);
num[i]=res;
}
for(int i=1;i<=n;i++){
ans+=num[i];
}
cout<<ans<<endl;
return 0;
}
线段树(维护区间sum)
- 像一个二叉树
struct node{
int l;
int r;
int sum;
}tr[4*N];
用子节点更新当前结点
void pushup(int u)
{
tr[u].sum = tr[u*2].sum + tr[2*u+1].sum ;
}
构造方式
void build(int u,int l ,int r)
{
if( l==r )
tr[u] = {l,r,w[r]}; // 叶子节点才赋值
else
{
tr[u]={l,r}; //这里记得赋值一下左右边界的初值
int mid = l + r >> 1 ;
build(u*2,l,mid),build(u*2+1,mid+1,r);
pushup(u); // 下面的两个儿子都建立好了,把这个结点的sum 更新一下
}
}
单点修改
O(log n )
void modify(int u ,int x, int v) // 在x的位置上加上 v
{
if( tr[u].l == tr[u].r ) // 判断是不是到达叶节点,也就是u是x吗
tr[u].sum += v;
else // u结点不是x的时候,x在u的左边还是右边
{
int mid = tr[u].l + tr[u].r >> 1;
if(x <= mid ) // 如果是在u的左边
modify(u*2,x,v) ; // 递归找左边
else
modify(u*2+1,x,v); // 不在左边就是在右边
pushup(u);
}
}
区间查询
O(log n)
递归
int query(int u ,int l ,int r) // 查询u结点在 l,r 区间内的sum
{
if( tr[u].l >= l && tr[u].r <= r ) return tr[u].sum ; // 如果整个都在区间里面,则返回整个u结点的sum
int mid = (tr[u].l + tr[u].r )/2;
int sum = 0;
if( l <= mid )
sum = query(u*2, l , r) ; //左孩子为 u*2
if( r > mid ) // 因为右区间的左端点是 mid+1, 所以这里要大于 mid
sum += query(u*2+1,l,r) ; //右孩子为 2*u+1
return sum ;
}
线段树(维护区间max)
- 在不将序列排序的情况下,找最大值,一般就是全部枚举一遍,时间复杂度为 O(n)
- 但是线段树维护区间 max 实现了不排序的情况下 时间复杂度在 O(log n) ,这就是主要用途
#include<iostream>
#include<algorithm>
#include <climits>
using namespace std;
const int N = 100010,M = 1000010;
int n,m;
int q[N];
struct Node{
int l,r;
int maxium;
}tr[N*4];
void pushup(int u)
{
tr[u].maxium = max(tr[u*2].maxium , tr[u*2+1].maxium ) ;
}
int build(int u ,int l ,int r)
{
if(l==r)
{
tr[u] = {l,r,q[l]};
}
else
{
tr[u].l = l,tr[u].r = r;
int mid = ( l + r ) >> 1 ;
build(u*2,l,mid );
build(u*2+1,mid+1,r);
pushup(u);
[1,n+1] 或 [0,n] 反正要多一个就行
sort(b+1,b+n+1);
sort(c+1,c+n+1);
for(int i = 1;i<=n ;i++)
{
int t = b[i];
int num1 = 0 , num2 =0;
int l = 1 ,r = n;
while(l<r)
{
int mid = (l+r+1)/2;
if(t > a[mid]) l = mid;
else r = mid-1;
}
if(a[l]>=t) num1 = 0;
else num1 = l;
l = 1,r = n;
while(l<r)
{
int mid = (l+r)/2;
if(t<c[mid]) r = mid;
else l = mid+1;
}
if(c[l]<=t) num2 = n+1;
else num2 = r;
res += ((LL)num1* (LL)(n-num2+1));
}
printf("%lld",res);
return 0;
}
466.回文日期
- 时间暴力 这个不对 ,因为没有考虑日期的正确性
// 反转
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
int time1,time2 ;
int change(int a)
{
int temp = 0 ;
while(a)
{
temp = ( temp + a % 10) * 10 ;
a/=10;
}
return temp/10;
}
int main()
{
cin >> time1 >> time2;
int t = time1;
int res = 0;
for(int i=t ;i<= time2 ; i++ )
if(i - change(i) == 0 )
res ++;
cout << res;
return 0;
}
- 正解
- 1.枚举回文数
- 2.判断是否再区间内
- 3.再判断日期是否合法
- 月份在1~12
- 号存在 month[ 13 ] = { 0 , 31 , 28 ,30 , 31 … }
- 闰年
- ① 年份不能被 100 整除 ,且能被 4 整除
- ② 年份能被 400 整除
// 只从 1000 ~ 9999
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
int time1,time2 ;
int days[13] = {0,31,28,31,30,31,30,31,31,30,31,30,31};
bool check_valid(int date)
{
int year = date / 10000 ;
int month = date % 10000 / 100;
int day = date % 100;
if(month==0 || month > 12 ) return false;
if(day==0 || (month != 2 && day > days[month]) ) return false;
if(month == 2)
{
int leap = 0 ;
if(year % 100 && year % 4==0 || year % 400 ==0)
leap = 1;
else
leap = 0;