《挑战程序设计竞赛》--初级篇习题POJ部分【2.4 - 2.6】

6 篇文章 0 订阅
1 篇文章 0 订阅

这次是延续上次的《挑战程序设计竞赛》初级篇,总结部分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;
    }
    
  • 并查集

    简介:
    并查集主要是管理元素分组情况的一种高效结构。其通常操作包括:

    1. 查询a与b是否为同一组
    2. 合并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)输入消息,有以下两种:

    1. D [a] [b]
      其中[a]和[b]是两名罪犯的人数,他们属于不同的帮派。

    2. 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-Ford

    int 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;
    		}
    	}
    }
    
    1. 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 60

    Sample 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写文章好像一篇文字太多了,会巨卡,这个总结写到后面真的挺卡的
    在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值