这次是延续上次的《挑战程序设计竞赛》初级篇,总结部分poj上的练习题,主要是2.4 ~ 2.6部分:
2.4 加工并存储的数据结构
-
优先队列
简介: 优先队列实际上就是堆的一种实现,STL中在#include< queue >中提供了priority_queue 来操作优先队列,默认是大堆(大值优先出队);
如果想要实现小堆,priority_queue<Type, Contain, Functional > Functional 可以使用
#include< functional> 内置的greater< T > 来使用小堆(只对基本变量类型,pair…);如果优先队列中是 自定义结构,想要放入优先队列中,必须自定义比较函数:
- 可以在struct中重载 < 运算符(注意 重载 > 会出问题),自定义比较规则
- 或者声明一个结构体,内部是比较自定义结构的函数(实际上,就是自定义 Functional ),声明优先队列使用priority_queue<Type, Contain, Functional >形式
优先队列一般是使用在,容器不断变化(不断有新值进,旧值出),每次操作希望取出容器中的最值,而不希望每次需要遍历一遍容器的情况。
比如单源最短路的Dijkstra算法,每次找起点到未使用的其他点的最短距离,就可以用优先队列。习题:
Sunscreen
题目
有C个奶牛去晒太阳 (1 <= C <= 2500),每个奶牛各自能够忍受的阳光强度有一个最小值和一个最大值 (1 ≤ minSPFi ≤ 1,000; minSPFi ≤ maxSPFi ≤ 1,000),
刚开始的阳光的强度非常大,奶牛都承受不住就得涂抹防晒霜,防晒霜的作用是让阳光照在身上的阳光强度固定为某个值。那么为了不让奶牛烫伤,又不会没有效果。
给出了L(1 ≤ L ≤ 2500)种防晒霜,以及每种的数量和固定的阳光强度(1 ≤ SPFi ≤ 1,000),每个奶牛只能抹一瓶防晒霜,最后问能够享受晒太阳的奶牛有几个?思路
贪心题
策略:尝试把每个防晒霜分配给尽可能多的牛(而不是为每个牛分配防晒霜)
将牛按MAXspf、防晒霜按spf从大到小排序 ;
对每种 防晒霜 找出其所有能够涂抹的牛;
每次选取所有牛中Minspf最大的先涂(一开始选的防晒霜SPF较大,对Minspf较小的牛,后面可能有小的防晒霜能够满足用,但是对对Minspf较大的牛的牛就不一定了)
这里取 所有牛中Minspf最大 就需要用到优先队列节约时间;代码
#include<cstdio> #include<algorithm> #include<queue> #define MAX_C 2505 #define MAX_L 2505 using namespace std; int C,L; typedef pair<int,int> Sunscreen; struct Cow{ int minS,maxS; }; priority_queue<int> Q;//大堆 bool cmp(const Cow& x,const Cow& y){ //牛按MAXspf if(x.maxS == y.maxS) return x.minS > y.minS; else return x.maxS > y.maxS; } bool cmp2(const Sunscreen& x,const Sunscreen& y){ //防晒霜按spf从大到小 return x.first > y.first; } Sunscreen sunscreen[MAX_L]; Cow cow[MAX_C]; void solve(){ sort(cow,cow+C,cmp); sort(sunscreen,sunscreen+L,cmp2); int res = 0; for(int i = 0,j = 0;i < L;++i){ //对每种防晒霜 for(;j < C;++j){ //牛按MAXspf从大到小 算过的牛不要再算 所以j声明在外面 if(cow[j].maxS < sunscreen[i].first) break; Q.push(cow[j].minS); //把符合的牛的minSPF放入队列中 } while(!Q.empty()) { if(sunscreen[i].second == 0){ //这种防晒霜用完了就别取了 break; } int a = Q.top(); //取出当前符合的最大 minSPF Q.pop(); if(sunscreen[i].first < a) //这种防晒霜这头牛 用不了 continue; sunscreen[i].second--; res++; } } printf("%d\n",res); } int main(){ scanf("%d%d",&C,&L); for(int i = 0;i < C;++i) scanf("%d%d",&cow[i].minS,&cow[i].maxS); for(int j = 0;j < L;++j){ scanf("%d%d",&sunscreen[j].first,&sunscreen[j].second); } solve(); return 0; }
MooUniversity-FinancialAid
题目
奶牛学校招生,c头奶牛报名,要选n头(n为奇数)录入,学校是义务制,所以每头奶牛的学费都由学校负责。
每头奶牛都由自己的考试分数和它需要花的学费,学校总共有 F 的资金,问合法招生方案中,中间分数(即排名第(n+1)/2)最高的是多少。
输入
*第1行:三个用空格分隔的整数N, C和F
*行2 . .C+1:每行两个空格分隔的整数。第一个是小牛的CSAT得分;第二个整数是小牛需要的财政援助输出
一个整数,是贝西能达到的最大中位数。如果没有足够的资金来接纳N犊牛,产出为-1。思路
这道题看起来好像很复杂,但是实际上,贪心穷竭就行;
因为中间分数肯定是所有分数中的某一个,而对于中位数而言(尤其是题目中所说的奇数个),中位数前面肯定有 (奇数-1) / 2 个比中位数小的数,后面也类似。
所以选定了中位数后,不管 中位数前面和后面如何选取,对中位数选取结果毫无影响,我们只需贪心地拿到资金最小的 取法,
然后 遍历所有中位数选取的情况,选择里面开销符合的,且中位数最大的情况,如果全无,则输出-1然后傻乎乎的用优先队列结果TLE了···(因为我每个中位数情况,都重新开新队列取求···)
其实我们用优先队列只不过是想知道 前面和后面的开销···那一次优先队列得到结果存在数组里不就行了吗,何必重复那么多次呢?代码
#include<cstdio> #include<algorithm> #include<queue> #include<functional> #define MAX_C 100005 #define MAX_N 20000 using namespace std; typedef pair<int,int> Cow; int N,C,F; Cow cow[MAX_C]; int front[MAX_C]; int behind[MAX_C]; priority_queue<int> Q; //大堆 void solve() { //枚举中位数的位置,在其左右各选n/2个最小的数 int ans = -1; for(int i = C - 1 - N/2; i >= N/2; i--) { if(front[i]+ behind[i] + cow[i].second <= F) { ans = cow[i].first; //逆序遍历拿符合的 最大中位数 break; } } printf("%d\n",ans); } int main() { scanf("%d%d%d",&N,&C,&F); for(int i = 0; i < C; ++i) { scanf("%d%d",&cow[i].first,&cow[i].second); // 分数, 需要资金 } sort(cow,cow+C); if(N == 1){ //特判情况 printf("%d\n",cow[C-1].first); return 0; } while(!Q.empty()) Q.pop(); int sum_F = 0; front[0] = 0; for(int i = 0; i < C; ++i) //计算front ,前i个数贪心最小解 { if(i < (N - 1) / 2) //中位数的前i个数还不足 { sum_F += cow[i].second; Q.push(cow[i].second);//队列存在前i个数的开销 front[i+1] = sum_F; } else { if(Q.top() > cow[i].second) //队列中前i个数的开销最大值大于当前值,交换 { sum_F -= Q.top() - cow[i].second;//减去减少的值 Q.pop(); Q.push(cow[i].second); } front[i+1] = sum_F; } } //对后面再来一次,类似 sum_F = 0; while(!Q.empty()) Q.pop(); behind[C - 1] = 0; for(int i = C - 1; i > 0; i--) { if(i >= C - (N - 1) / 2) { sum_F += cow[i].second; Q.push(cow[i].second); behind[i-1] = sum_F; } else { if(cow[i].second < Q.top()) { sum_F -= Q.top() - cow[i].second; Q.pop(); Q.push(cow[i].second); } behind[i-1] = sum_F ; } } solve(); return 0; }
-
并查集
简介:
并查集主要是管理元素分组情况的一种高效结构。其通常操作包括:- 查询a与b是否为同一组
- 合并a与b所在的组
那么怎么判断a和b是不是同一组的呢?对并查集的每个元素,我们都给它们分配一个父亲par,每一组看成一棵树,它们有共同的根节点。对a、b我们判断它们的根节点是否一致就知道是否为同一组了(通过父亲自下而上的访问)
为了更高效的使用并查集,我们通常会进行路径压缩 和 高度记录(避免树退化为链表):路径压缩 是指在查找一个节点的根节点时,将查询时向上经过的所有节点,全部直接与根节点相连。高度记录是指 合并操作时高度小的树连着高度大的树下,高度大的为父亲。
一般模板:
int par[MAX_N]; int rank[MAX_N]; void init(int n){ for(int i = 0;i < n;++i){ par[i] = i;//一开始每个节点都是自己的根,根节点的父亲为自己 rank[i] = 0; } } int find(int n){ //查找根节点 + 路径压缩 return par[n] == n ? n : par[n] = find(par[n]); } bool same(int x,int y){ return find(x) == find(y); } void unite(int x,int y){ int px = find(x),py = find(py); if(px == py) return; if(rank[px] < rank[py]){ par[px] = py; }else { par[py] = px; if(rank[px] == rank[py]) rank[px]++; } }
习题:
Wireless-Network
题目
由于硬件的限制,每台计算机只能与不超过d米远的计算机直接通信。在网络修复的过程中,工作人员可以随时进行两种操作,一种是修复一台计算机,另一种是测试两台计算机是否可以通信。
您的工作是回答所有的测试操作是否能够成功。输入
第一行包含两个整数N和d (1 <= N <= 1001, 0 <= d <= 20000)。这里N为计算机数量,从1到N, D为两台计算机直接通信的最大距离。
在接下来的N行中,每一行都包含两个整数xi, yi (0 <= xi, yi <= 10000),这是N台计算机的坐标。从(N+1)第一行到输入末尾,依次执行操作。每行包含以下两种格式之一的操作:
1.“O p” (1 <= p <= N)表示修理计算机p。
2.“S p q”(1 <= p, q <= N),即测试计算机p和q是否可以通信。
输入不会超过300000行。输出
对于每个测试操作,如果两台计算机可以通信,则打印“yes”;如果不能通信,则打印“no”。思路
非常典型的并查集题目
每次维修新的电脑时,检测是否之前的电脑 有连接(距离是否小于等于d),如果有就合并,维修的时候检测是否连接即可代码
#include<cstdio> #include<algorithm> #define MAX_N 1005 #define MAX_M 300000 using namespace std; int par[MAX_N]; int ran[MAX_N]; int N,d; typedef pair<int,int> CPT; CPT cpt[MAX_N]; int cins[MAX_M]; //输入 int getD(const CPT& x,const CPT& y){ return (x.first - y.first) * (x.first - y.first) + (x.second - y.second) * (x.second - y.second); // 10000 * 10000 * 2不会溢出int } void init(int n){ for(int i = 0;i < n;++i){ par[i] = i; ran[i] = 0; } } int find(int x){ return par[x] == x ? x : par[x] = find(par[x]); } bool same(int x,int y){ return find(x) == find(y); } void unite(int x,int y){ int px = find(x),py = find(y); if(px == py) return; if(ran[px] < ran[py]){ par[px] = py; }else{ par[py] = px; if(ran[px] == ran[py]) ran[px]++; } } int main(){ scanf("%d%d",&N,&d); for(int i = 1;i <= N;++i){ scanf("%d%d",&cpt[i].first,&cpt[i].second); } //处理O or S char c; int a,b; init(N+1); int repairNum = 0; while(scanf("%c",&c) != EOF){ if(c == 'O'){ scanf("%d",&a); getchar();//吃回车 cins[repairNum++] = a; for(int i = 0;i < repairNum;++i){ //检测是否之前的电脑 有连接 if(getD(cpt[a],cpt[cins[i]]) <= d * d){ unite(a,cins[i]); } } } else if(c == 'S'){ scanf("%d%d",&a,&b); getchar();//吃回车 if(same(a,b)){ printf("SUCCESS\n"); } else printf("FAIL\n"); } } return 0; }
Find-them,Catch-them
题目
假设有N (N <= 10^5)个罪犯在Tadu市,编号从1到N,当然至少有一个是Gang Dragon帮派,还有Gang Snake帮派。
你将依次得到M条(M <= 10^5)输入消息,有以下两种:-
D [a] [b]
其中[a]和[b]是两名罪犯的人数,他们属于不同的帮派。 -
A [A] [b]
其中[a]和[b]是两个罪犯的号码。这要求您判断 a和b 是否属于同一组。Sample Input 1 //测试用例的数量 5 5 //罪犯数 信息数 A 1 2 D 1 2 A 1 2 D 2 4 A 1 4 Sample Output Not sure yet. In different gangs. In the same gang.
思路
并查集题目:
可以采用类似于 食物链poj-1182 的解法,并查集应该是保存所有的可能性
我们 让并查集为 2 * N,前N个表示为第一个帮派,后面N个表示在第二个帮派
那么 D x y 就等于两种情况,把所有可能性放入:unite(x,y+N) unite(x+N,y)
而 A 1 2 我就判断他们是不是同一个帮派,
判断两次 same(x,y),same(x+N,y+N) 相同为同一帮派
判断两次 same(x,y+N),same(x+N,y) 为不同帮派
剩余情况为不能确定代码
#include<cstdio> #define MAX_N 100005 int T,N,M; using namespace std; int par[MAX_N * 2]; int ran[MAX_N * 2]; void init(int n){ for(int i = 0;i < n;++i) { par[i] = i; ran[i] = 0; } } int find(int x){ return par[x] == x ? x : par[x] = find(par[x]); } bool same(int x,int y){ return find(x) == find(y); } void unite(int x,int y){ int px = find(x),py = find(y); if(px == py) return; if(ran[px] < ran[py]){ par[px] = py; }else{ par[py] = px; if(ran[px] == ran[py]) ran[px]++; } } int main(){ scanf("%d",&T); for(int i = 0;i < T;++i){ scanf("%d%d",&N,&M); init(N * 2); for(int j = 0;j < M;++j){ getchar(); char c; int x,y; scanf("%c%d%d",&c,&x,&y); if(c == 'A'){ // 有人说 2 1 A 1 2 应该返回In different gangs.···· // 我这种思路没有确保每个帮派至少有一人,这里为了严谨性补一句 // 实际我个人感觉每个帮派至少一个人,这个条件应该是由输入确保的,我们不检测输入数据带来的矛盾性··· if(N == 2 && x * y == 2){ printf("In different gangs.\n"); } else if(same(x,y+N) || same(x+N,y)){ printf("In different gangs.\n"); }else if(same(x,y) || same(x+N,y+N)){ printf("In the same gang.\n"); } else printf("Not sure yet.\n"); } else if(c == 'D'){ unite(x,y+N); unite(x+N,y); } } } return 0; }
2.5 它们其实都是‘图’
-
最短路
简介:
最短路问题是图论问题里最常见的了,一般时求给定两个点,求从某个点到另一个点的路程开销最小的路径。
根据问题可以分为:1. 单源最短路 2. 任意两点最短路
单源最短路有两个最著名的算法,一个是Bellman-Ford算法,支持带负边的图;
另一个是Dijstra算法,速度更快,但不支持带负边的图。简单介绍一下这两个算法:
1. Bellman-Ford算法: 记起点S到点i的最短距离为d[i],有 d[i] = min{ d[j] + e(j,i) } e(j,i) 为j到i的路权值 初始时,d[s] = 0,d[i] = INF,不断使用上式来更新d数组, 只要图中没有负圈,d的更新就是有限的。 实际上d的更新循环应该最多是顶点数 - 1次, 因为每次d的一整次更新,一定能确认某个点的最终d[i] 利用这一点,可以判断图是否有负圈,如果有,在第V次循环d仍会更新。 它的算法复杂度为O(V * E),其优化算法叫做SPFA``` 2. Dijstra算法:(没有负边) 先找出最短距离已经确定的点,从它出发更新相邻点的最短距离, 然后再找出更新后的最短距离里没用过的最小的(之前用过的不算) 重复第一步更新操作,直到所有最短距离都找到 如果每次找最短距离都是遍历查找的话,这个算法的复杂度最差有也有可能O(V * E) 但是我们可以用优先队列来优化查 ---> O(logV * E)
简单的代码实现:
1.Bellman-Fordint d[MAX_V]; struct Edge{ int from,to,cost; } Edge edge[MAX_E]; int V,E; void Bellman(int start){ for(int i = 0;i < V;++i) d[MAX_V] = INF; d[start] = 0; int up = 0; while(true){ bool flag = false; for(int i = 0;i < E;++i){ Edge e = edge[i]; if(d[e.from] != INF && d[e.from] + e.cost < d[e.to]){ d[e.to] = d[e.from] + e.cost; flag = true; } } if(!flag){ break; } up++; if(up > n){ printf("有负圈"\n); break; } } }
- Dijkstra算法
int d[MAX_V]; typedef pair<int,int> P; //cost 、to vector<P> G[MAX_V]; int V; void Dijstra(int start){ for(int i = 0;i < V;++i) d[MAX_V] = INF; priority_queue<P,vector<P>, greater<P> > Q;//小堆 d[start] = 0; Q.push(pair(0,start)); while(!Q.empty()){ P p = Q.top();Q.pop(); int v = p.second,cost = p.first; if(d[v] < cost) continue; //更新邻近点 for(int i = 0;i < G[v].size(); ++i){ P e = G[v][i]; if(d[e.second] > d[v] + e.first){ d[e.second] = d[v] + e.first; Q.push(pair(d[e.second],e.second)); } } } }
任意两点最短路:比较有名的是dp算法----Floyd-Warshall算法,这个算法另:
d[k+1][i][j] 只使用0-k的点,求得i->j的最短路径值 容易得到:(分经过或不经过k) d[k+1][i][j] = min{d[k][i][j],d[k][i][k] +d[k][k][j] } 使用二维数组节省空间 d[i][j] = min{[i][j],d[i][k] +d[k][j] } 注意循环的状态转移 初始化:dp[i][j]=cost[i][j] or INF 实现: int d[MAX_V][MAX_V]; int N; for(int k = 1; k <= N; ++k) { for(int i = 1; i <= N; ++i) for(int j = 1; j <= N; ++j) if(d[i][j] > d[i][k] + d[k][j]) d[i][j] = d[i][k] + d[k][j]; //注意 i j k的顺序 }
习题:
Six-Degrees-of-Cowvin-Bacon
题目
N (2 <= N <= 300)头奶牛对找出哪头奶牛与其他奶牛的平均距离最小感兴趣,当然不包括她自己。奶牛已经拍了M (1 <= M <= 10000)部电影,保证每一对奶牛之间都有一定的关系路径。
输入
*第1行:两个用空格分隔的整数:N和M
*行2 . .M+1:第一个整数是参与所描述电影的奶牛数量(如Mi);随后的Mi整数告诉我们哪些奶牛参演。输出
*第1行:单个整数,是任何奶牛的最小平均距离的100倍。Sample Input 4 2 3 1 2 3 2 3 4 Sample Output 100
思路
理解题意:
我们首先获得图(根据电影参演。互相连线)
然后根据图,获得每个点到其他点的最短路径 使用Floyd-Warshall算法,算法复杂度为O(n^3),对题目的 N <= 300,可以满足 ~
然后对每个点,求其到其他店的所有距离和 * 100 / 其他点数(N-1),取里面最小的代码
#include<cstdio> #define MAX_V 305 #define INF 1e9 using namespace std; int d[MAX_V][MAX_V]; //初始化为 i 到 j 的 权值,不存在时为INF i->i 为 0 int N,M; int cow[MAX_V]; void init() { for(int i = 1; i <= N; ++i) { for(int j = 1; j <= N; ++j) { d[i][j] = ( i == j ? 0 : INF); } } } int main() { scanf("%d%d",&N,&M); init(); for(int i = 0; i < M; ++i) { int num; scanf("%d",&num); for(int j = 0; j < num; ++j) { scanf("%d",&cow[j]); } for(int k = 0; k < num - 1; ++k) //画图连线 { for(int m = k + 1; m < num; ++m) { d[cow[k]][cow[m]] = 1; //双向图 d[cow[m]][cow[k]] = 1; } } } for(int k = 1; k <= N; ++k) { for(int i = 1; i <= N; ++i) for(int j = 1; j <= N; ++j) if(d[i][j] > d[i][k] + d[k][j]) d[i][j] = d[i][k] + d[k][j]; //注意 i j k的顺序 } int min_v = INF; for(int i = 1; i <= N; ++i) { int this_v = 0; for(int j = 1; j <= N; ++j) { this_v += d[i][j]; } min_v = (this_v < min_v ? this_v : min_v); } printf("%d\n",min_v * 100 / (N-1) ); return 0; }
Wormholes
题目
农夫约翰在探索他的许多农场,发现了一些惊人的虫洞。虫洞是很奇特的,因为它是一个单向通道,可让你进入虫洞的前达到目的地!
他的N(1≤N≤500)个农场被编号为1…N,之间有M(1≤M≤2500)条路径,W(1≤W≤200)个虫洞。
作为一个狂热的时间旅行的爱好者,他要做到以下几点:开始在一个区域,通过一些路径和虫洞旅行,他要回到最开时出发的那个区域出发前的时间。也许他就能遇到自己了…
为了帮助FJ判断是否是可以达到,他会为你提供F个农场的完整的映射到(1≤ F ≤5)。
所有的路径所花时间都不大于10000秒,虫洞也是。思路
理解题意:其实就是判断从起点走是否有可达负圈
道路是正权 双向边
虫洞是负权 单向边
找负圈····
使用SPFA(Bellman-Ford)算法,判断从s开始是否有达负圈代码
#include<cstdio> #include<algorithm> #define MAX_N 505 #define MAX_M 2505 #define MAX_W 205 #define INF 100000000 using namespace std; struct Edge { int from,to,cost; }; Edge edges[MAX_M * 2 + MAX_W]; int d[MAX_N]; int F,N,M,W; void SPFA(){ for(int i = 1;i <= N;++i) { d[i] = INF; } d[1] = 0; int upNum = 0; while(true){ bool update = false; for(int i = 0;i < 2 * M + W;++i){ Edge e = edges[i]; if(d[e.from] != INF && d[e.to] > d[e.from] + e.cost){ d[e.to] = d[e.from] + e.cost; update = true; } } if(!update) break; else{ upNum++; if(upNum == N ){ //若没有负圈,最短路不会经过同一个点2次 ==> 循环的更新次数最多为 N - 1次(因为每次循环,至少relax一边),多了说明有负圈 printf("YES\n"); return; } } } printf("NO\n"); } int main(){ scanf("%d",&F); while(F--){ scanf("%d%d%d",&N,&M,&W); for(int i = 0;i < 2 * M;i += 2){ //道路 int f,t,c; scanf("%d%d%d",&f,&t,&c); edges[i].from = f; edges[i].to = t; edges[i].cost = c; edges[i+1].from = t; edges[i+1].to = f; edges[i+1].cost = c; } for(int j = 2 * M ;j < 2 * M + W;++j){ //虫洞 int f,t,c; scanf("%d%d%d",&f,&t,&c); edges[j].from = f; edges[j].to = t; edges[j].cost = -c; } //printE(); SPFA(); } return 0; }
Silver-Cow-Party
题目
一只母牛从N块田中的任一块(1≤N≤1000)去参加盛大的母牛聚会,这个聚会被安排在X号田(1≤X ≤N)。
一共有M(1 ≤ M ≤ 100,000)条单行道分别连接着两块田,且通过路i需要花Ti(1≤Ti≤100)的时间。
每头母牛必需参加宴会并且在宴会结束时回到自己的领地,但是每头牛都很懒而喜欢选择化是最少的一个方案。来时的路和去时的可能不一样。
求每头牛来回的最短时间中,最长的。思路
因为是有向图,我们可以先求目标点到所有其他点的最短路(从宴会回来),
但是由于来时的路和去时的可能不一样,从其他每个点到目标点都来一次最短路的话,可以用Floyd但是会超时O(10^9)可以换个思路,我们要的是:
目标点为起点,到其他所有点的最短路 + 其他所有点各为起点,到目标点终点de最短路前者我们用1次Dijkstra就能得到,后者我们不太好获取得·····可以转化一下
可以把地图道路方向反向,这样后者就等价于 前者的思路了···(因为从起点走到终点的 最短单向路 === 从终点到起点的反方向最短单向路)
所以我们要准备两份地图 ····进行二次Dijkstra代码
#include<cstdio> #include<vector> #include<queue> #include<functional> #define MAX_N 1005 #define MAX_M 100005 #define MAX_T 105 #define INF 100000000 using namespace std; int N,M,X; //dijkstra 一般需要用到邻接表 struct Edge{ int to,cost; bool operator< (const Edge &b) const //写在里面只用一个b,但是要用const和&修饰,并且外面还要const修饰; { return cost > b.cost; //最小堆 } }; //邻接表 vector<Edge> edge[MAX_N],rev_edge[MAX_N]; int d[MAX_N],rev_d[MAX_N]; priority_queue<Edge> Q,r_Q; void dijkstra(){ fill(d+1,d+N+1,INF); fill(rev_d+1,rev_d+N+1,INF); d[X] = 0; rev_d[X] = 0; Edge e1; e1.to = X;e1.cost = 0; Q.push(e1); r_Q.push(e1); while(!Q.empty()){ Edge min_n = Q.top(); Q.pop(); if(d[min_n.to] < min_n.cost) continue;//遗留值不管,之前可能入队一些过往值,在之后的更新中,过往值已经不是真正的最小值了 for(int i = 0;i < edge[min_n.to].size();++i){ Edge e = edge[min_n.to][i]; if(e.cost + d[min_n.to] < d[e.to]){ d[e.to] = e.cost + d[min_n.to]; Edge e1; e1.cost = d[e.to];e1.to = e.to; Q.push(e1); } } } while(!r_Q.empty()){ Edge min_n = r_Q.top(); r_Q.pop(); if(rev_d[min_n.to] < min_n.cost) continue; for(int i = 0;i < rev_edge[min_n.to].size();++i){ Edge e = rev_edge[min_n.to][i]; if(e.cost + rev_d[min_n.to] < rev_d[e.to]){ rev_d[e.to] = e.cost + rev_d[min_n.to]; Edge e1; e1.cost = rev_d[e.to];e1.to = e.to; r_Q.push(e1); } } } int max_z = 0; for(int i = 1;i <= N;++i){ max_z = max_z > d[i] + rev_d[i] ? max_z : d[i] + rev_d[i];//最长的 } printf("%d\n",max_z); } int main(){ scanf("%d%d%d",&N,&M,&X); for(int i = 0;i < M;++i){ int f,t,c; scanf("%d%d%d",&f,&t,&c); Edge e,r_e; e.to = t; e.cost = c; edge[f].push_back(e); //反向图 r_e.to = f; r_e.cost = c; rev_edge[t].push_back(r_e); } dijkstra(); return 0; }
-
最小生成树
简介:
MST 边上权值最小的生成树,一般用于修路问题的求解上。
算法主要有:(需要注意的是这些算法都只能获得从起点开始,能到达的连通分量的最小生成树,有的图不一定是全体点连通的)1. Prim 从某个点作为初始树T出发,贪心地选取T与其他顶点间相连的最小权值边。 将其点加入T中,再更新T到其他邻近点的权值,重复至所有点都在最小生成树中。 其实现过程类似于Dijstra算法:
int mincost[MAX_N]; bool use[MAX_N]; typedef pair<int,int> P; //cost to priority_queue<P, vector<P>,greater<P> > Q; int cost[MAX_N][MAX_N]; #define INF 0x3f3f3f3f void Prim() { //初始化 fill(mincost,mincost+MAX_N,INF); fill(use,use+MAX_N,false); mincost[0] = 0;//从0顶点开始 int res = 0; Q.push(P(mincost[0],0)); while(!Q.empty()){ P p = Q.top(); Q.pop(); if(use[p.second] == true) continue; use[p.second] = true;//加入该点 res += p.first; for(int i = 0;i < N;++i){ if(use[i] == false && mincost[i] > cost[p.second][i]){ mincost[i] = cost[p.second][i]; Q.push(P(mincost[i],i)); } } } printf("%d\n",res); }
2. Kruskal 按边的权值顺序从小到大排序,每次取最小的边, 如果加入这条边,生成树中不会有圈就加入,至所有边访问完毕 实现代码:(并查集代码见之前内容)
void Kruskal(){ sort(edge,edge+Enum,cmp);//边从小到大 init(MAX_N);//初始化并查集 int res = 0; for(int i = 0; i < Enum; ++i) { Edge e = edge[i]; if(!same(e.from,e.to)) //这条线两点不相连 { unite(e.from,e.to); res += e.cost; } } printf("%d\n",res); }
习题:
Agri-Net
题目
农场铺网络,希望得到铺设网络路线最小的开销,使得所有农村都能连通
任何两个农场之间的距离都不会超过10万。输入
输入包括几种情况。
对于每种情况,第一行包含农场的数量N (3 <= N <= 100)。
下面的线条包含了N x N的邻接矩阵,其中每个元素表示农场到另一个农场的距离。输出
对于每种情况,输出单个整数长度,该长度是连接整个农场所需的最小光纤长度的总和。思路
就是一个典型的最小生成树问题
两种算法: Prim or Kruskal 算法代码
#include <cstdio> #include <algorithm> #include <queue> #include <functional> #define MAX_N 105 using namespace std; int cost[MAX_N][MAX_N]; int N; struct Edge { int from,to,cost; }; bool cmp(const Edge& x,const Edge& y) { return x.cost < y.cost; } Edge edge[MAX_N * MAX_N]; int par[MAX_N],ran[MAX_N]; void init(int n) { for(int i = 0; i < n; ++i) { par[i] = i; ran[i] = 0; } } int find(int x) { return par[x] == x ? x : par[x] = find(par[x]); } bool same(int x,int y) { return find(x) == find(y); } void unite(int x,int y) { int px = find(x),py = find(y); if(px == py) return; if(ran[px] < ran[py]) { par[px] = py; } else { par[py] = px; if(ran[px] == ran[py]) ran[px]++; } } void Kruskal() { //要获得农村的边,由题意知,所有农村都有边相连,由邻接表转化为边 int Enum = 0; for(int i = 0; i < N; ++i) { for(int j = 0; j < N; ++j) { if(i == j) continue; edge[Enum].from = i; edge[Enum].to = j; edge[Enum++].cost = cost[i][j]; } } sort(edge,edge+Enum,cmp);//边从小到大 init(MAX_N); int res = 0; for(int i = 0; i < Enum; ++i) { Edge e = edge[i]; if(!same(e.from,e.to)) //这条线两点不相连 { unite(e.from,e.to); res += e.cost; } } printf("%d\n",res); } int mincost[MAX_N]; bool use[MAX_N]; typedef pair<int,int> P; //cost to priority_queue<P, vector<P>,greater<P> > Q; #define INF 100000000 void Prim() { //初始化 fill(mincost,mincost+MAX_N,INF); fill(use,use+MAX_N,false); mincost[0] = 0;//从0顶点开始 int res = 0; Q.push(P(mincost[0],0)); while(!Q.empty()){ P p = Q.top(); Q.pop(); if(use[p.second] == true) continue; use[p.second] = true;//加入该点 res += p.first; for(int i = 0;i < N;++i){ if(use[i] == false && mincost[i] > cost[p.second][i]){ mincost[i] = cost[p.second][i]; Q.push(P(mincost[i],i)); } } } printf("%d\n",res); } int main() { while(scanf("%d",&N) != EOF) { fill(cost[0],cost[0] + MAX_N * MAX_N,0); for(int i = 0; i < N; ++i) { for(int j = 0; j < N; ++j) { scanf("%d",&cost[i][j]); } } //Kruskal(); Prim(); //两种都实现了,都可AC } return 0; }
Bad-Cowtractors
题目
求最大生成树
N (2 <= N <= 1,000) M (1 <= M <= 20,000) C (1 <= C <= 100,000).
输入
*第1行:两个用空格分隔的整数:N和M
*行2 . .M+1:每行包含三个用空格分隔的整数A、B和C,描述了成本为C的谷仓A和B之间的连接路径.
输出
*第1行:单个整数,包含连接所有谷仓的最贵生成树树的价格。
如果不能连接所有谷仓,输出-1。思路
思路有很多:
可以类比最小生成树的做法,修改为最大生成树
或者 每条边都取负,求最小生成树然后取绝对值···
注意最后判断一下是不是所有点都连通了代码
#include<cstdio> #include<algorithm> #include<queue> #include<functional> #define MAX_N 1005 #define MAX_M 20005 #define MAX_C 100005 #define INF 100000000 using namespace std; int N,M; typedef pair<int,int> P; //cost to int mincost[MAX_N]; bool use[MAX_N]; vector<P> vetex[MAX_N]; void prim(){ //按最小生成数的思路,需要对边权值取负,最后取绝对值 fill(use,use + N,false); fill(mincost,mincost+N,INF); mincost[1] = 0; int res = 0; priority_queue<P,vector<P>,greater<P> > Q; Q.push(P(0,1)); while(!Q.empty()){ P p = Q.top(); Q.pop(); if(use[p.second] == true) continue; use[p.second] = true; //当前最小点 res += p.first; for(int i = 0;i < vetex[p.second].size(); ++i){ //检测最小点的每条边 P ne = vetex[p.second][i]; if(use[ne.second] == false && mincost[ne.second] > ne.first) { mincost[ne.second] = ne.first; Q.push(P(mincost[ne.second],ne.second) ); } } } for(int i = 1;i <= N;++i){ //检测 if(!use[i]){ printf("-1\n"); return; } } printf("%d\n",-res); } void prim2(){ //按最大生成数的思路 fill(use,use + N,false); fill(mincost,mincost+N,0); //初始化为0,维护到非使用集合的最大距离 mincost[1] = 0; int res = 0; priority_queue<P> Q;//da堆 Q.push(P(0,1)); while(!Q.empty()){ P p = Q.top(); Q.pop(); if(use[p.second] == true) continue; use[p.second] = true; //当前最大点 res += p.first; for(int i = 0;i < vetex[p.second].size(); ++i){ //检测最大点的每条边 P ne = vetex[p.second][i]; if(use[ne.second] == false && mincost[ne.second] < ne.first) { mincost[ne.second] = ne.first; Q.push(P(mincost[ne.second],ne.second) ); } } } for(int i = 1;i <= N;++i){ if(!use[i]){ printf("-1\n"); return; } } printf("%d\n",res); } struct Edge{ int from,to,cost; }; Edge edge[MAX_M]; bool cmp(const Edge& x, const Edge& y){ //最大生成树从大到小排 return x.cost > y.cost; } int par[MAX_N],ran[MAX_N]; void init(int n){ for(int i = 1; i <= n;++i){ par[i] = i; ran[i] = 0; } } int find(int x){ return par[x] == x ? x : par[x] = find(par[x]); } bool same(int x,int y){ return find(x) == find(y); } void unite(int x,int y){ int px = find(x),py = find(y); if(px == py) return; if(ran[px] < ran[py]) par[px] = py; else{ par[py] = px; if(ran[px] == ran[py]) ran[px]++; } } void Kruskal(){ //对连通图才有效·· sort(edge,edge+M,cmp); int res = 0; fill(use,use+N,false); init(N); for(int i = 0; i < M;++i){ Edge e = edge[i]; if(!same(e.from,e.to)){ unite(e.from,e.to); res += e.cost; } } int num = 0; //若连通,只有一个根节点 for(int i = 1;i <= N;++i){ if(par[i] == i) { if(num < 1) num++; else{ printf("-1\n"); return; } } } printf("%d\n",res); } int main(){ scanf("%d%d",&N,&M); for(int i = 0;i < M;++i){ int f,t,c; scanf("%d%d%d",&f,&t,&c); edge[i].from = f; edge[i].to = t; edge[i].cost = c; //vetex[f].push_back(P(-c,t)); //当前最小生成树需要负权 //vetex[t].push_back(P(-c,f)); vetex[f].push_back(P(c,t)); vetex[t].push_back(P(c,f)); } //prim(); //prim2(); Kruskal(); return 0; }
Out-of-Hay
题目
有N个农村M条双向道路,我们想从农场1出发去到其他所有农场
希望你能找出从1出发的最小生成树的最大边的长度 不超过1,000,000,000
(2 <= N <= 2,000) (1 <= M <= 10,000)思路
没啥好说的,找最小生成树的同时,记录最大边,这里用Kruskal感觉更简单。代码
#include<cstdio> #include<algorithm> using namespace std; #define MAX_N 2005 #define MAX_M 10005 #define INF 1200000000 int N,M; struct Edge{ int from,to,cost; }; Edge edge[MAX_M]; bool cmp(const Edge& x,const Edge& y){ return x.cost < y.cost; } //并查集 int par[MAX_N],ran[MAX_N]; void init(int n){ for(int i = 1;i <= n;++i){ par[i] = i; ran[i] = 0; } } int find(int x){ return par[x] == x ? x : par[x] = find(par[x]); } bool same(int x,int y){ return find(x) == find(y); } void unite(int x,int y){ int px = find(x),py = find(y); if(px == py) return; if(ran[px] < ran[py]) par[px] = py; else { par[py] = px; if(ran[px] == ran[py]) ran[px]++; } } int main(){ scanf("%d%d",&N,&M); for(int i = 0;i < M;++i){ scanf("%d%d%d",&edge[i].from,&edge[i].to,&edge[i].cost); //双向边也只判断一次 } int res = 0; sort(edge,edge + M,cmp); init(N); for(int i = 0;i < M;++i){ Edge e = edge[i]; if(!same(e.from,e.to)){ //printf("%d,%d,%d\n",e.from,e.to,e.cost); unite(e.from,e.to); res = res > e.cost ? res : e.cost; } } printf("%d\n",res); return 0; }
2.6 数学问题的解题窍门
-
辗转相除法
简介: 一般我们求取a和b的最大公约数gcd时采用的都是辗转相除法即欧几里得算法:
gcd(a,b) = gcd(b,a%b) 当b == 0时,a即是最大公约数 另:扩展欧几里得算法 AX1 + BY1 = gcd(A,B) 一定有整数特解 其中 X1 = Y2 Y1 = X2 - (A/B)*Y2 使用递归可求一组特解 知道了gcd,那么求a,b的最小最小公倍数lcm,有lcm * gcd = a * b
习题:
GCD&LCM-Inverse
题目
给定两个正整数a和b,我们可以很容易地计算出a和b的最大公约数(GCD)和最小公倍数(LCM),但它的逆呢?即给定GCD和LCM,求出a和b。
输入
输入包含多个测试用例,每个测试用例都包含两个正整数:GCD和LCM。可以假设这两个数都小于2^63。输出
对于每个测试用例,按升序输出a和b。如果有多个解,输出a + b最小的一对。Sample Input
3 60Sample Output
12 15思路
a * b = lcm * gcd;
a / gcd * b / gcd = lcm / gcd;
gcd (a/gcd , b/gcd) = 1 (a/gcd,b/gcd互质)就是把lcm / gcd分解成两个互质的因子。可以用Pollard rho分解子因子,然后再将相同的因子合并,再将因子分成两部分。
由于要判断大数的质数检测,朴素的质数检测会TLE,所以用Miller_Rabin 算法进行素数测试;用Pollard rho分解子因子;
这道题看起来简单,但写起来老复杂了,代码我这里贴一下别人的····代码
#include<iostream> #include<algorithm> #include<math.h> #include<stdio.h> #include<string.h> #include<time.h> #include<stdlib.h> typedef __int64 LL; LL a,b,sum; const int S=20;//随机算法判定次数,S越大,判错概率越小 //***************Miller_Rabin 算法进行素数测试*************** int cmp(void const *a,void const *b) { if(*(LL *)a > *(LL *)b) return 1; else return -1; } LL mult_mod(LL a,LL x,LL n)//返回(a*x) mod n,a,x,n<2^63 { a%=n;x%=n; LL ret=0; while(x) { if(x&1){ ret+=a;if(ret>=n)ret-=n; } a<<=1; if(a>=n)a-=n; x>>=1; } return ret; } LL pow_mod(LL a,LL x,LL n)//返回a^x mod n { if(x==1)return a%n; int bit[70],k=0; while(x) { bit[k++]=(x&1?1:0); x>>=1; } LL ret=1; for(--k;k>=0;k--) { ret=mult_mod(ret,ret,n); if(bit[k])ret=mult_mod(ret,a,n); } return ret; } bool judge(LL a,LL n,LL x,LL t)//以a为基,n-1=x*2^t,检验n是不是合数 费马定理 { LL ret=pow_mod(a,x,n),flag=ret; for(LL i=1;i<=t;i++) { ret=mult_mod(ret,ret,n); if(ret==1&&flag!=1&&flag!=n-1)return true; flag=ret; } if(ret!=1)return true; return false; } bool Miller_Rabin(LL n) //判断是否是质数 { if(n==2||n==5||n==7||n==11)return true; //常见的直接判断 if(n%2==0||n%5==0||n%7==0||n%11==0)return false; LL x=n-1,t=0; while((x&1)==0)x>>=1,t++; bool flag=true; if(t>=1&&(x&1)==1) { for(int i=1;i<=S;i++) { LL a=rand()%(n-1)+1; if(judge(a,n,x,t)){flag=true;break;} flag=false; } } if(flag)return false; else return true; } //*******pollard_rho(一种快速分解质因数的算法)进行质因数分解***************** LL factor[100];//质因子 int tot;//质因子个数 LL gcd(LL a,LL b) { if (a==0) return 1; if (a<0) return gcd(-a,b); while (b) { LL t=a%b; a=b; b=t; } return a; } LL Pollard_rho(LL x,LL c) { LL i=1,x0=rand()%x,y=x0,k=2; while (1) { i++; x0=(mult_mod(x0,x0,x)+c)%x; LL d=gcd(y-x0,x); if (d!=1 && d!=x) return d; if (y==x0) return x; if (i==k) { y=x0; k+=k; } } } void find_factor(LL n) //递归进行质因数分解N { if(Miller_Rabin(n)) { factor[tot++] = n;return; } LL p=n; while (p>=n) p=Pollard_rho(p,rand() % (n-1) +1); find_factor(p); find_factor(n/p); } void dfs(int k,LL ans,LL n) { if(ans>n)return ;//ans是较小的一个,控制它小于n if(k==tot) { if(ans+sum/ans<a+b) {a=ans,b=sum/ans;} return; } dfs(k+1,ans,n); dfs(k+1,ans*factor[k],n); return ; } int main() { LL lcm,g,temp,flag; int k,i; while(scanf("%I64d%I64d",&g,&lcm)!=-1) { if(g==lcm) { printf("%I64d %I64d\n",g,lcm); continue; } tot=0;sum=lcm/g; find_factor(sum); qsort(factor,tot,sizeof(factor[0]),cmp); k=0;a=1;b=sum; flag=temp=factor[0]; for(i=1;i<tot;i++)//合并相同的质因子得到互质的一些因子 { if(factor[i]==temp) flag*=temp; else { factor[k++]=flag; flag=temp=factor[i]; } } factor[k++]=flag; tot=k; dfs(0,1,(LL)sqrt(1.0*sum));//将得到的互质因子分成两部分 //if(a>b)std::swap(a,b); printf("%I64d %I64d\n",a*g,b*g); } return 0; }
Dead-Fraction
题目
复原分数,将省略小数的形式 还原为 精确的分数形式
(注意:循环的部分不一定是最后一位,有可能从小数点后面全是循环部分…)输入 有几个测试用例。对于每个测试用例,都有一行形式为“0.dddd…”的输入, 其中dddd是由1到9个数字组成的字符串,不全是0。最后一个大小写后面跟着一行0。 输出 对于每种情况,输出原始分数。 Sample Input 0.2... 0.20... 0.474612399... 0 Sample Output 2/9 1/5 1186531/2500000
思路
要想解决这道题,首先应该了解如何将循环小数化为分数:
一,纯循环小数化分数:循环节的数字除以循环节的位数个9组成的整数。
考虑小数A = 0.aaa…,其中a是一个n位整数.那么就可以写成分数求和形式,
A = a / {10^n} + a / {10^{2n}}+ a/ 10^{3n}}+… 使用等比公式,由于1-q^k 极限为1,有
A = a / (10^n - 1); 循环节的数字 / 循环节的位数个9组成的整数
例如:
0.3333……= 3/9 = 1/3;
0.285714285714……= 285714/999999=2/7.
二,混循环小数:(例如:0.24333333……)不循环部分+循环节构成的的数 - 不循环部分的差,再除以循环节位数个9添上不循环部分的位数个0。
由于B = 0.b + 1 / {10^m} * 0. aaa… b是个m位整数.
利用上一步的结果,就有
B = b / {10^m} + a / (10^n - 1) / {10^m} = {(b * 10^n + a) - b } / {(10^n - 1) * 10^m}
例如:
0.24333333………… = (243-24)/900=73/300
0.9545454………… = (954-9)/990=945/990=21/22题目还要求分数要化简,所以又要用到辗转相除法。
代码 (这里参考了别人写的一场精简的代码)
#include<iostream> #include<math.h> #include<string.h> using namespace std; int gcd(int a,int b) { if(!a) return b; return gcd(b%a,a); } int main() { char str[100]; int num,k,all,a,b,i,j,mina,minb,l; while(cin>>str&&strcmp(str,"0")) { mina=minb=1000000000; for(i=2,all=0,l=0;str[i]!='.';i++) //0.123456 --> 123456 { all=all*10+str[i]-'0'; l++; } for(num=all,k=1,i=1;i<=l;i++) //枚举,从最后一位是一位循环节开始 { num /= 10; //243333 取m k *= 10; a = all - num; //分子,除了循环节第一个部分,后面循环节都是0; 219000 b = (int)pow(10.0,l-i)*(k-1); //分母,k为取9的个数,pow为取零的部分 j = gcd(a,b); //化简 if(b/j < minb) //取分母最小的 { mina = a/j; minb = b/j; } } cout<<mina<<'/'<<minb<<endl; } return 0; }
-
质数
简介:
质数一般分为:质数测试、区间质数
对于n的质数检测,一般算法O(sqrt(n)),从2~sqrt(n) 进行逐个取%,有n%i == 0则不是质数区间质数对于从[1,n]判断区间内有多少个质数,更高效的做法是埃氏筛法:
思路上是:将2~n的数写下来,最小的质数从2开始,把所有在范围内的2的倍数划掉,取下一个最小的没被划掉的数是质数,是3,将3的倍数划掉,重复至所有数都进行完。对于[a,b]中有多少质数,如果a比较大,还采用埃氏筛法计算[2,b]可能不够高效:
这时候用类似的方法,用埃氏筛法把[2,sqrt(b)]的所有质数筛出来,将其倍数再[a,b]中划去,剩下的即是[a,b]中的质数个数。习题:
Prime-Path
题目
给你两个四位数的素数,通过改变其中的一个数,每次只允许改变一位数,而且改变之后的数也必须是个素数,问你最少通过改变几次变成后一个四位的素数。
如果不能改变成后面的四位素数,则输出Impossible。用例最多100 两个数字都是四位数素数(没有前导零)。
思路
一道埃氏筛法 + BFS的题目,因为要测试多个数据所以可以先打印一张五位数内的质数表(避免重复计算超时)
然后BFS从起点开始搜索,每次变化一位,然后判断条件并确定是否放入队列,dp数组记录距离(即花费)。代码
#include<iostream> #include<cstring> #include<queue> using namespace std; const int maxn = 10000; bool isprime[maxn+1];//打表用辅助表 int prime[maxn+1]; //质数表 int dp[maxn+1];//开销表 int getnext(int num,int t,int change) //改变change 获取下一个值 { if(t==0)return num/10*10+change; //个位 else if(t==1)return num/100*100 + num%10 + change*10;//十位 else if(t==2)return num/1000*1000 + num%100 + change*100; //百位 else return num%1000 + change*1000; //千位 } int main() { //埃氏筛法打表 int p = 0; for(int i = 0; i <= maxn; i++) //一开始全是质数 isprime[i] = true; isprime[0] = isprime[1] = false; for(int i=2; i<=maxn; i++) { if(isprime[i]) { prime[p++] = i; for(int j = 2*i; j<= maxn; j+=i) { isprime[j] = false; } } } int n; cin>>n; while(n--) { int a,b; cin>>a>>b; //两个数 memset(dp,0x3f,sizeof(dp));// 0x3f = 00111111 也就是十进制的63 //0x3f3f3f3f的十进制是1061109567,是10^9级别的,而一般场合下的数据都是小于10^9的,所以它可以作为无穷大使用而不致出现数据大于无穷大的情形。 dp[a] = 0; queue<int> q; q.push(a); while(!q.empty()) { int cur = q.front(); q.pop(); for(int i=0; i<4; i++) //bfs { for(int j=0; j<10; j++) { if(i==3 && j==0)continue;//千位变为0,前导零忽略 int next = getnext(cur,i,j); if(isprime[next] == false || dp[next] <= dp[cur])continue;//下一个数不是质数or下一个数开销比之前好(不可能,发生这种情况只可能回溯了) dp[next] = dp[cur] + 1; q.push(next); } } } cout<<dp[b]<<endl; } return 0; }
X-factor-Chains
题目
一个数列,起始为1,终止为一给定数X,
1,X1,X2,…,Xn = X(长度不包括第一个1)
满足Xi < Xi+1 并且Xi | Xi+1。(Xi+1能整除Xi)
求出数列最大长度和该长度下的情况数。思路
因为题目里要求Xi | Xi+1,而Xm又限定为X,所以我们可以想到Xm-1是X除以其某个约数得到的,Xm-1也是一样。
由此我们可以知道“X-factor Chains”是通过不断乘以X的约数得到的,为了长度最大,所以约数必须是素数(贪心,因为不是质数的都可以分解)。
通过记录X有哪些素因数,以及素因子的数量,我们就可以得到链的长度。
而长度最大的链的数量则是这些数的排列数~,公式为 (素因子个数之和 的阶乘 / 每个素因子个数的阶乘)代码
#include<cstdio> #include<algorithm> using namespace std; typedef long long LL; //阶乘可能溢出int int N; LL fact(int n){ //求n的阶乘 return ((n == 0 || n == 1) ? 1 : n * fact(n - 1)); } void solve(int n){ int length = 0; LL b = 1; if(n == 1 || n == 2){ printf("1 1\n"); return; } for(int i = 2;i * i <= n;++i){ //分解质因数 int num = 0; while(n % i == 0){ n /= i; num++; } length += num; b *= fact(num); } if(n > 1){ //最后是一个单独的质因数 length++; } printf("%d %lld\n",length,fact(length) / b); } int main(){ while(scanf("%d",&N) != EOF){ solve(N); } return 0; }
Semi-prime-H-numbers
题目
An H-number(4 * k + 1,k >= 0) 分为三种: unit 1 H-primes = 只能由 1 * H-primes得到 H-composites:由两个H-number(1不算) 相乘得到 H-semi-primes: 由两个H-primes 相乘得到 输入 每一行输入包含一个≤1000,001的H-number。输入的最后一行包含0,这一行不应该被处理。 输出 对于每个输入的h-number,打印一行h-number 以及[1,h-number]之间的H-semi-primes的个数,按照示例中所示格式用一个空格隔开。 Sample Input 21 85 789 0 Sample Output 21 0 85 5 789 62
思路
类似埃氏筛法···变化一下即可
i从5开始,每次+4
然后j从i起 有 i * j ; j+=4
若i,j都是H质数 i*j就是H-semi,不然就是H-composites;代码
#include<cstdio> #include<algorithm> #define MAX_H 1000005 using namespace std; int H; int isHprime[MAX_H]; void solve(){ //emmmm,对每个结果判断一次会超时···所以直接一次求wan得了 fill(isHprime,isHprime + MAX_H,0); for(int i = 5;i <= MAX_H;i += 4){ //H-Number for(int j = i;j <= MAX_H;j += 4){ if(i >= 1001) break;//肯定超了 long long k = i * j; if(k <= MAX_H ){ //printf("%lld\n",k); if(isHprime[i] == 0 && isHprime[j] == 0) //都是H-primes isHprime[k] = 1; //H-semi-primes else isHprime[k] = -1; //H-composites }else break; } } int res = 0; for(int j = 5;j <= MAX_H;j += 4){ if(isHprime[j] == 1){ res++; } isHprime[j] = res; } } int main(){ solve();//直接打完表 while(scanf("%d",&H) != EOF){ if(H == 0) break; printf("%d %d\n",H,isHprime[H]); } return 0; }
-
快速幂运算
简介:
一般我们求幂会使用STL中的pow(x,n)方法,但是这个方法实际上时间复杂度是o(n)
使用反复平方法可以快速得到幂:
例子:x^22 首先22 = 10110(二进制)
x^22 = x^16 + x^4 + x^2实际上我们只需要计算三次即可long long(long long x,long long n){ long long res = 1; whiel(n > 0){ if(n & 1) res = res * x; x = x*x; n >>= 1; } return res; }
习题:
Pseudoprime-numbers
题目
a的p次方模p为a && p为非素数为 Pseudoprime numbers输入
输入包含几个测试用例,后面是一行包含“0 0”的代码。每个测试用例由包含p和a的一行组成。
Given 2 < p ≤ 1000000000 and 1 < a < p输出
对于每个测试用例,如果p是一个Pseudoprime numbers,输出“yes”;否则输出“no”思路
先判断p是不是质数,然后在判断 a的p次方模p为a,需要使用到快速幂取模代码
#include<cstdio> #include<algorithm> using namespace std; bool isPrime(long long N){ for(int i = 2; i * i <= N; ++i){ if(N % i == 0){ return false; } } return N != 1; //除了1 } //快速幂 一般用LL 快速求出 a^b Mod c (注意:b是一个大数) /* 数论的一个定理: (a*b) mod c = [(a mod c)*(b mod c)] mod c 那么根据上面的定理可以推导出另一个定理: (a^b) mod c = (a mod c)^b mod c 参考博客:https://blog.csdn.net/qq_36760780/article/details/80092665 */ long long quickM(long long x,long long n,long long mod){ long long res = 1; while(n > 0){ if(n & 1 == 1){ res = res * x % mod;//取模 } x = x * x % mod; //将x平方 n >>= 1; } return res; } //这题p 和 a用int会WA,我猜是测试数据里某个p超了int````` int main(){ long long p,a; while(scanf("%lld%lld",&p,&a) != EOF){ if(p == 0 && a == 0) break; if(isPrime(p)){ printf("no\n"); } else{ if(quickM(a,p,p) == a) printf("yes\n"); else printf("no\n"); } } return 0; }
Raising-Modulo-Numbers
题目
给定一组数 Ai,Bi 和M
求 所有 Ai ^ Bi 后再相加,最后再取模M的结果思路
快速幂取模问题,注意用long long代码
#include<cstdio> using namespace std; long long quickM(long long x,long long n,long long M){ long long res = 1; while(n > 0){ if(n & 1) res = res * x % M; x = x * x % M; n >>= 1; } return res; } int main(){ int T; scanf("%d",&T); while(T--){ int M,num; scanf("%d%d",&M,&num); long long res = 0; for(int i = 0;i < num;++i){ long long x,n; scanf("%lld%lld",&x,&n); res = (res + quickM(x,n,M)) % M; } printf("%lld\n",res); } }
以上就是我个人的一些总结,个人能力有限,有错误还请指出~
偷偷补一句,csdn写文章好像一篇文字太多了,会巨卡,这个总结写到后面真的挺卡的