牛客网练习赛14

A-约数个数

t次询问,每次给你一个数n,求在[1,n]内约数个数最多的数的约数个数

输入描述:

第一行一个正整数t
之后t行,每行一个正整数n

输出描述:

输出t行,每行一个整数,表示答案

解析

我知道要用到约数个数定理,但是写出的代码还是超时了,因为还有一个重要的定理即正整数唯一分解定理,即任意一个大于1的自然数都可以分解成质数的积,到这里还不算什么。重要的是求约数个数最大多的,那么应该发现这样的事实,就是低质因子的幂越高,对应的约数个数就越多。你想啊,一个数乘以2肯定比乘以5递增的慢把,这样就能产生更多的约数。

X = p_1^a*p_2^b*....p_n^n

s.t. \space p_1 \geq p_2 ....\geq p_n

s.t. \space a \geq b .... \geq n 

所以我们的目标即是上式子,即尽可能使低质数的幂次要高些,同时后面的质数的幂次小于等于前面。因为如果存在低质数的幂次小于高质数的幂次(假设此时约数个数为n),那么这个数的范围内,必然存在低质数的幂次高于高质数的幂次也可以使约数个数为n,故同等效果,这里这样强制,为了就是减少不必要的计算

代码

package com.special.test14;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.util.StringTokenizer;

/**
 * Create by Special on 2018/4/6 14:22
 */
public class ProA {

    static int[] prime = {2,3,5,7,11,13,17,19,23,29,31,37,41,43,47};
    static long n, max;
    static int limit = 18; 

    /**
     *
     * @param num 当前数的大小
     * @param sum 当前数的约数个数
     * @param limit 上一个质因子的幂次
     * @param index 考虑到哪个质数了
     */
    static void dfs(long num, long sum, int limit, int index){
        max = Math.max(max, sum);
        if(index == 15) { return; } //多往后考虑几个质数也无碍吗,万一最大约数的质因子种类很多呢
        long tmp = n / prime[index];
        for(int i = 1; i <= limit; i++){
            if(num <= tmp) { //当前的质数还可以往上乘,则继续
                num *= prime[index];
                dfs(num,sum * (i + 1), i, index + 1);
            }
        }
    }

    public static void main(String[] args){
        FastScanner input = new FastScanner();
        PrintWriter out = new PrintWriter(System.out);
        int t = input.nextInt();
        while(t-- > 0){
            n = input.nextLong();
            max = 0;
            dfs(1, 1, limit, 0);
            out.println(max);
        }
        out.close();
    }
}

E-无向图最短距离

链接:https://www.nowcoder.com/acm/contest/82/E
来源:牛客网

有一个n个点的无向图,有m次查询,每次查询给出一些(xi,yi)

令dist(x,y)表示x和y点在图中最短距离,dist(x,x)=0,如果x,y不连通则dist(x,y) = inf

每次查询图中有多少个点v与至少一个这次询问给出的(xi,yi)满足dist(v,xi)<=yi

输入描述:

第一行三个数表示n,m,q
之后m行每行两个数x,y表示有一条x与y之间的边,边权为1
之后q次询问,每个询问先给你一个数a
之后一行2a个数,第2i-1个数xi和第2i个数yi表示一个二元组(xi,yi)

输出描述:

输出q行,每行一个数表示这次询问的答案

解析

本题是多源最多路径问题,我的最开始做法是flody算法 + 排序,然后遍历所有点到到输入的点的距离是否小于最大值来做的。但超时了。因为复杂度为$O(n^3 + nlogn)$,无疑会超时。
观察此题很特殊,因为两条边的距离都为1,所以最短路径问题退化为BFS,因为BFS的天然的每次只走一步,先到的点,必定是初始点到这个点的最短距离。但这还远远不够。
假设我们已经知道了各点到各点的最短距离,那么对于q次的查询,仍然需要遍历所有的点到给定的点的距离是否小于等于y。这无疑又是n的平方的复杂度。
那么如何解决到某点的距离的判断呢?采用状态压缩的做法来做,即我们用一个二进制表示到i点距离为j的点是哪个点,bit[i][dis[i][j]][j] = 1,表示j点到i点的距离为dis[i][j]。这样我们求一个前缀的并即可得到小于等于dis[i][j]的所有的j点。那么在之后的查询中,对所有bit[x][y]求并,即可得到y集合的至少一次点了,复杂度为O(c)。

代码

java版


  • Bitset类的应用

BitSet表面上是一个位序列,通过将值对应位的索引来存储该值的状态,类似“开关信息”,值为false或者true。底层实现为long数组,具体底层不细说了,可以去看源代码。

  • new Bitset(int size) 指定要建立的位序列的长度。但真实长度为64的倍数
  • or(Bitset bitset) 与形参的bitset的进行按位或操作
  • cardinality() 返回位序列的中真值的位数。(这货很不高效,感觉就是因为这个函数导致超时,c++却可以直接进行位运算)

  • 可惜Java的代码超时了,可能Java的bitset不高效。

    package com.special.test14;
    
    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStreamReader;
    import java.io.PrintWriter;
    import java.util.*;
    
    /**
     * 状压BFS
     * Java竟然超时,我不得不放弃Java啊!
     *
     * Create by Special on 2018/4/6 11:34
     */
    public class ProE {
    
        static final int MAX = Integer.MAX_VALUE;
        static int[] dis;
        static ArrayList[] G;
        static BitSet[][] bit;
        static int n;
    
        static void bfs(int src){
            Queue queue = new LinkedList<>();
            dis = new int[n + 1];
            Arrays.fill(dis, MAX);
            queue.offer(src);
            dis[src] = 0;
            bit[src][dis[src]].set(src);
            while(!queue.isEmpty()){
                int v = queue.poll();
                List ad = G[v];
                for(int i = 0; i < ad.size(); i++){
                    int u = ad.get(i);
                    if(dis[u] == MAX) {
                        dis[u] = dis[v] + 1;
                        bit[src][dis[u]].set(u);
                        queue.offer(u);
                    }
                }
            }
        }
    
        public static void main(String[] args){
            FastScanner input = new FastScanner();
            PrintWriter out = new PrintWriter(System.out);
            n = input.nextInt();
            int m = input.nextInt();
            int q = input.nextInt();
            G = new ArrayList[n + 1];
            bit = new BitSet[n + 1][n + 1];
            for(int i = 0; i <= n; i++){
                G[i] = new ArrayList<>();
            }
            dis = new int[n + 1];
            for(int i = 0; i <= n; i++){
                for(int j = 0; j <= n; j++){
                    bit[i][j] = new BitSet(n + 1);
                }
            }
            int src, drc;
            while(m-- > 0){
                src = input.nextInt();
                drc = input.nextInt();
                G[src].add(drc);
                G[drc].add(src);
            }
            for(int i = 1;  i <= n; i++){
                bfs(i);
                for(int j = 1; j <= n; j++){
                    bit[i][j].or(bit[i][j - 1]);
                }
            }
            while(q-- > 0){
                int c = input.nextInt();
                BitSet b = new BitSet(n + 1);
                while(c-- > 0){
                    b.or(bit[input.nextInt()][input.nextInt()]);
                }
                out.println(b.cardinality());
            }
            out.close();
        }
    }
    C++

    C++代码可以过,遇到位运算的题,可以尝试转换语言。

    踩过的坑:
    - memset是按字节填充的,所以一般赋值为0,-1,可以得到理想效果。因为0和-1的补码形式多个字节拼接成int,也可以保持原值不变。
    - queue
    - queue.push(T t) 入队
    - queue.front() 返回队首元素,不删除
    - queue.pop() 删除队首元素,不返回,void
    - queue.empty() 判断队列是否为空
    - bitset
    - c++中bitset可以直接利用位运算符进行计算,不用调api
    - bitset[i] = 1, 可以直接把某一位更改为1
    - bitset.count(), 返回为1的位的个数

    #include
    using namespace std;
    typedef long long ll;
    
    const int maxn = 1005;
    int n, m, q;
    vector<int> G[maxn];
    int dis[maxn][maxn];
    bitset bit[maxn][maxn];
    
    void bfs(int src){
        dis[src][src] = 0;
        bit[src][dis[src][src]][src] = 1;
        queue<int> q;
        q.push(src);
        while(!q.empty()){
            int v = q.front();
            q.pop();
            for(int i = 0; i < G[v].size(); i++){
                int u = G[v][i];
                if(dis[src][u] == -1){
                    dis[src][u] = dis[src][v] + 1;
                    bit[src][dis[src][u]][u] = 1;
                    q.push(u);
                }
            }
        }
    }
    
    int main(){
        scanf("%d %d %d", &n, &m, &q);
        int x, y;
        memset(dis, -1, sizeof dis);
        while(m--){
            scanf("%d %d", &x, &y);
            G[x].push_back(y);
            G[y].push_back(x);
        }
        for(int i = 1; i <= n; i++){
            bfs(i);
            for(int j = 1; j <= n; j++){
                bit[i][j] |= bit[i][j - 1];
            }
        }
        int a;
        while(q--){
            scanf("%d", &a);
            bitset b;
            while(a--){
                scanf("%d %d", &x, &y);
                b |= bit[x][y];
            }
            printf("%d\n", b.count());
        }
    }
    

    B-区间连续段

    链接:https://www.nowcoder.com/acm/contest/82/B
    来源:牛客网

    给你一个长为n的序列a和一个常数k

    有m次询问,每次查询一个区间[l,r]内所有数最少分成多少个连续段,使得每段的和都 <= k

    如果这一次查询无解,输出”Chtholly”

    输入描述:

    第一行三个数n,m,k
    第二行n个数表示这个序列a
    之后m行,每行给出两个数l r表示一次询问

    输出描述:

    输出m行,每行一个整数,表示答案

    解析

    普通做法肯定是对于每一个查询,都依次遍历即可,贪心做法。但因为数据很大,所以会超时。
    区间求最值问题,首选倍增法,所以倍增法的意思我们每一次考虑的区间范围都是成倍增大或者缩小。这样查询的复杂度变为O(logn),但是倍增法需要预处理一下。常见的倍增法经常用于求区间的最小值问题。比如:

    st[i][j] = min(st[i][j - 1], st[st[i][j - 1] + 1][j - 1])

    st[i][j]表示从第i个元素开始,到i + 2^j - 1,这个区间的最小值。所以st[i][j - 1]表示[i, i + 2 ^(j - 1) - 1]的最小值,st[st[i][j - 1] + 1][j - 1],表示[i + 2 ^(j - 1), i + 2 ^(j - 1) + 2 ^(j - 1) - 1]的最小值。因为2^j 可以被分成2个2 ^(j - 1)的区间,故上式子成立。所以我们可以利用这个例子的倍增思想来解答这道题。
    我们令st[i][j]表示从第i个元素开始,分成2^j段,i能够到到达的最右端 + 1的位置。比如st[i][0]表示就是i不分段能够到达的最远位置。st[i][1]则表示i分成2^1 = 2段能够到达的最远距离。故公式为:

    st[i][j] = st[st[i][j - 1]][j - 1]

    表示i分成2^j段能走到最远位置等于: i分成2^(j - 1)段能走到最远位置,然后在这个位置上再出发,分成2^(j - 1)段能走到的最远位置。
    我们需要预处理出各个位置出发,分多少段能走的最远位置,这样再查询时即可在O(logn)完成查询。

    代码

    package com.special.test14;
    
    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStreamReader;
    import java.io.PrintWriter;
    import java.util.StringTokenizer;
    
    /**
     *
     * 倍增法
     * Create by Special on 2018/4/6 10:26
     */
    public class ProB {
        static int[][] st;
    
        public static void main(String[] args){
            FastScanner input = new FastScanner();
            PrintWriter out = new PrintWriter(System.out);
            int n = input.nextInt();
            int m = input.nextInt();
            int k = input.nextInt();
            int[] num = new int[n + 1];
            st = new int[n + 3][21];
            for(int i = 1; i <= n; i++){
                num[i] = input.nextInt();
            }
            long sum = 0;
            int id = 0;
            for(int i = 1; i <= n; i++){
                id = Math.max(id, i);
                while(id <= n && sum + num[id] <= k){
                    sum += num[id];
                    id++;
                }
                if(sum != 0) { sum -= num[i]; }
                st[i][0] = id;//i在不分段的情况下能走的最远位置 + 1处
            }
            for(int i = 1; (1 << i) <= n; i++){
                for(int j = 1; j <= n; j++){
                    st[j][i] = st[st[j][i - 1]][i - 1];
                }
            }
            while(m-- > 0){
                int l = input.nextInt();
                int r = input.nextInt();
                int ans = 0;
                for(int i = 20; i >= 0; i--){
                    //因为我们编码的处理方式,使得st[i][j] 如果能到达最远位置,那么st[i][j + 1]我们就默认为0。因为题目是最小段数嘛
                    if(st[l][i] != 0 && st[l][i] <= r){
                        ans += (1 << i);
                        l = st[l][i];
                    }
                }
                //上面的循环我们把最后一个位置卡在了r之前。所以最后还要特判一下,看是否真能走到到r
                if(l <= r){
                    l = st[l][0]; //不分段尽可能走
                    ans++;
                }
                out.println(l > r ? ans : "Chtholly");
            }
            out.close();
        }
    
    }
    

    当然对于不满足的情况,也可以利用另一个数组来做,即如果一个元素本身就大于k,那么就把数组的该位置为1,最后累加。然后判断sum[r] - sum[l - 1] == 0 来判断期间是否有不满足的点,可直接输出不可达的情况。

    你可能感兴趣的:(不刷题心里难受,状态压缩,倍增法)