【周赛】第一周周赛——欢迎16级的新同学题解(题目出自codeforces 318A,546A,431C,665E,HDU 4104)

A题:

A题题目链接

A题题目描述:

Home W的数学

TimeLimit:1000MS  MemoryLimit:256MB
64-bit integer IO format: %I64d

Problem Description

    我们都知道,Home W的数学最厉害了。有一天,他又开始开动脑筋了,他想:“为什么数字总是要从1排列到n呢?”于是,Home W开始研究自己排列数字的方法。首先,他写下了1-n中所有的奇数(按照升序排列),然后他又写下了1-n中所有的偶数(按照升序排列),那么问题来了,在这样的排列方式下第k个数是什么呢?

Input

输入只有一行,包括n和k(1 ≤ k ≤ n ≤ 1012).

  

注意:长整型声明形式为: long long a;

    在这里的输入形式为:scanf("%I64d",&a);    (或者使用cin也可)

         输出形式为:printf("%I64d\n",a);  (或者使用cout也可)

Output

输出只有一行,输出第k个数即可。

SampleInput 1
10 3
SampleOutput 1
5
样例说明(非输出部分): 按照Home W的排列方式即为{1,3,5,7,9,2,4,6,8,10},那么显然第三个数是5
SampleInput 2
7 7
SampleOutput 2
6
解析:

题意很明显,我们可以分奇偶的情况进行考虑:

当n为偶数的时候,显然前n/2个数为奇数,后n/2个数为偶数,假设第1-n个数的下标分别是1~n,则前n/2个数分别为2*k-1,而后n/2

个数2*(k - n/2)。

当n为奇数的时候,则前(n+1)/2个数为奇数,后(n+1)/2-1个数为偶数(相当于在最后补上缺少的那个偶数,实际上不存在),同样的

(n+1)/2个数分别为2*k-1,后(n+1)/2-1个数为2*(k-n/2)。

完整代码实现:

#include 
typedef long long LL;
void solve(LL sum, LL index){
    LL mid = (sum + 1) / 2;
    if(index <= mid){
        printf("%I64d\n",index*2-1);
    }else{
        printf("%I64d\n",(index - mid)*2);
    }
}

int main(){
    LL n,k;
    while(scanf("%I64d %I64d",&n,&k)!=EOF){
        solve(n,k);
    }
    return 0;
}

B题:

B题题目链接

题目描述:

QAQ和香蕉

TimeLimit:1000MS  MemoryLimit:256MB
64-bit integer IO format: %I64d

Problem Description

  QAQ是个吃货,这一次他来到了一个商店,想买w根香蕉,但是这个商店实在是黑,他需要支付k元买第一根香蕉,2k元买第二根香蕉....(也就是说,当他买第k根香蕉时,他需要支付i*k元)。

  可是QAQ钱包里只有n元,你能帮助他计算一下,他要借多少钱才能买下w根香蕉吗?

Input

第一行包括三个整数 k, n, w (1  ≤  k, w  ≤  10000 ≤ n ≤ 109), 分别是第一根香蕉的单价,QAQ钱包里的钱总数,以及他想要买的香蕉总数。

Output

输出只有一行,包含一个整数——QAQ需要借多少钱,如果他不需要借钱,输出0。

SampleInput
3 17 4
SampleOutput
13
解析:

我们可以先计算w根香蕉的总价格为:k+2*k+...+w*k = (1+w) * w / 2 * k,因此如果QAQ钱包里的钱大于总价格,显然输出0,否

则的话则输出其差值。

完整代码实现:

#include 

void solve(int unitPrice,int allMoney,int bananaAmount){
    int borrowMoney = bananaAmount * (bananaAmount + 1) / 2 * unitPrice - allMoney;
    if(borrowMoney <= 0){
        printf("0\n");
    }else{
        printf("%d\n",borrowMoney);
    }
}

int main(){
    int k,n,w;
    while(scanf("%d %d %d",&k,&n,&w)!=EOF){
        solve(k,n,w);
    }
    return 0;
}

C题

C题题目链接

题目描述:

QAQ的数学题

TimeLimit:1000MS  MemoryLimit:32768KB
64-bit integer IO format: %I64d

Problem Description

  Home W说他数学很好,QAQ表示不服气,于是QAQ出了一道数学题给Home W做。题目很简短:给定n个数字,每个数字最多选择一次(也可以不选),问这n个数字不能组合得到的最小的值,并输出。

Input

输入到文件结束( 即输入格式为 while(scanf(...)!=EOF)){ ... } )

第一行包含一个整数N(1 <= N <= 1000),第二行为N个整数Pi(0 <= Pi <= 10000).

Output

输出只有一个整数,表示这n个数字不能组合得到的最小的值

SampleInput
4
1 2 3 4
SampleOutput
11
解析:

       这道题的思路比较巧妙,顺着题目的角度去思考很难入手,因为这样的话要一个个的遍历过去,然后再计算已经遍历过的序列

能够表示哪些数,而且不好判断,因为可以从数组中任意选择数字,顺着题目的角度很难入手,那为什么我们不考虑相反的方向呢?

考虑数组a不能表示哪些数,那我们假如现在遍历到a[i]元素时,此时数组能组合成1~sum中的任意数字,那么此时考虑a[i+1],

怎么才能知道数组a不能表示哪些数呢?很显然,由于此时数组已经能组合成1~sum中的任意数字,那么当a[i+1] > sum + 1时,则

此时sum+1相当于被跳过了。显然sum+1是不能被数组元素组合成的。而a[i+1] <= sum + 1时,显然此时可以组合成的数字的范

围扩大为1 ~ sum + a[i+1]。所以抓住了sum+1这个临界条件,问题就解决了。

所以我们可以先排序,然后按照上述过程处理,排序的目的是为了找到最小的不能组合成的数字

完整代码实现:

#include 
#include 
using namespace std;
const int MAX_SIZE = (int) 1e3 + 10;
int a[MAX_SIZE];

void solve(int sum){
    for(int i = 0;i < sum;i++)
        scanf("%d",&a[i]);
    sort(a,a + sum);
    int ans = 0;
    for(int i = 0;i < sum;i++){
        if(a[i] > ans + 1){
            break;
        }else{
            ans += a[i];
        }
    }
    printf("%d\n",ans + 1);
}

int main(){
    int n;
    while(scanf("%d",&n)!=EOF){
        solve(n);
    }
    return 0;
}

D题:

D题题目链接

题目描述:

running jump的树

TimeLimit:1000MS  MemoryLimit:256MB
64-bit integer IO format: %I64d

Problem Description

 我们都知道,running jump数据结构最厉害了,于是这一天,他又创造了一种新的数据结构,叫做k-树.那什么是k-树呢?首先,k-树时一个无限节点的树,意

思是说这棵树是可以不断往下延伸的,并且k-树有以下的性质:

 (1).每个节点有k个子节点

 (2).每条边都有一个权重,每条边的权重从左往右一次为1,2,3,...,k.

(感觉好神奇的样子

下图是3-树的一部分(因为节点是无限的,所以还可以往下无限延伸)



   这时候,我们的running jump开始给我们出题了,他说:“从根节点开始,有多少条路径的权值之和为n呢?”然后他又想了想,感觉题目太容易了,于是又

加了一个限制条件,路径中至少要有一条边的权重大于等于d。那么聪明的Acmer,你能解决running jump给我们留下的这个问题吗?

   由于结果可能过大,因此将结果对1000000007(109 + 7)取余后输出.

Input

输入只有一行,包括三个整数,nk and d (1 ≤ n, k ≤ 100; 1 ≤ d ≤ k).

Output

输出只有一行,包含对1000000007 (109 + 7)取余后的结果.

SampleInput 1
3 3 2
SampleOutput 1
3
SampleInput 2
3 3 3
SampleOutput 2
1
SampleInput 3
4 3 2
SampleOutput 3
6
SampleInput 4
4 5 2
SampleOutput 4
7
解析:

由于题目给了限制条件,所以给我们的思考带来了一定的障碍,那么我们可以先将问题化简,如果没有至少含有一条权重大于

等于d的路径这个条件的话,该如何考虑呢?

如果没有了这个限制条件,那么就相当于从 1 ~ k 中选择一些数字,使得他们的和为n,每个数字可以选择多次,很直接的思路就是搜索,但是

索的时间复杂度是指数级的,而n,k范围比较大(n,k <= 100),因此搜索是不可取的。那考虑权值之和为j时,剩下需要解决的则是权值之和为n-j

的子问题。而n-j的子问题依旧可以按照以上方式继续分解,很显然,利用这种方式考虑子问题推出更大问题的解时,原问题不需要考虑子问题是如何

达到当前状态的。因此符合无后效性,并且原问题下的子问题的解也是当前找到的最优解(即统计好了所有路径和为j的路径条数),因此该问题符合最优

子结构性质。可用动态规划求解。

既然可用动态规划求解,我们利用刚刚找到的无后效性的状态,原问题拆分为j和n-j规模的子问题,而k-树共有k个分支。很显然我们可以推出状态

转移方程即是:

dp[n] = dp[n-1] + dp[n-2] + ... + dp[n-k];

再考虑限制条件:至少含有一条权值大于等于d的边,很显然,对于这种至少含有一个,一种,一条的一类的问题,正难则反,我们则考虑所有

边权值都小于d,因此我们只要将权值和为n,分支数为k的总的路径数减去权值和为n,分支数为b-1(这样的话最大权值的边也就是b-1)的路径数

即得我们所求的答案。因此只要将状态转移方程增加一维表示分支数不同的状态值求解即可。

在确定状态转移方程递推原问题的过程中,确定一些状态的初始值,很显然,dp[0]即为根节点处,权值为0的只有在这一点,因此两种分支数的

情况下dp[0] 均等于1,最后,小心取模即可。

完整代码实现:

#include 
#include 
const int MAX_SIZE = 105;
const int MOD = 1e9+7;
int dp[2][MAX_SIZE];

void solve(int n,int k,int d){
    memset(dp,0,sizeof(dp));
    dp[0][0] = dp[1][0] = 1;
    for(int i = 1;i <= n;i++){
        for(int j = 1;j <= k;j++){
            if(i - j < 0){
                break;
            }else{
                dp[0][i] += dp[0][i-j];
                if(dp[0][i] >= MOD){
                    dp[0][i] -= MOD;
                }
            }
        }

        for(int j = 1;j < d;j++){
            if(i - j < 0){
                break;
            }else{
                dp[1][i] += dp[1][i-j];
                if(dp[1][i] >= MOD){
                    dp[1][i] -= MOD;
                }
            }
        }
    }
    printf("%d\n",((dp[0][n] - dp[1][n]) + MOD) % MOD);
}

int main(){
    int n,k,d;
  //  freopen("cf 431C.txt","r",stdin);
    while(scanf("%d %d %d",&n,&k,&d)!=EOF){
        solve(n,k,d);
    }

}

E题:

E题题目链接

Value Dragon出难题了

TimeLimit:3000MS  MemoryLimit:512MB
64-bit integer IO format: %I64d

Problem Description

 有一天,Value Dragon觉得好无聊啊,所以决定出一道题目给自己做,于是他写下了一个含有n个元素的整型数组a,这n个元素分别是a1,a2,...,an。

然后呢,他就想啊,如果能找到一个连续的区间[l,r](1  ≤  l  ≤  r  ≤  n),使得该区间中所有的数的异或值大于等于k,那他就觉得这段区间是一个完美的区间。

  那么问题来了,这样的区间总共有多少个呢?于是Value Dragon陷入了无尽的思考中......

Input

第一行输入为两个数,分别是n和k (1 ≤ n ≤ 106, 1 ≤ k ≤ 109) — 分别表示整型数组元素的个数以及参数k的值

第二行输入为n个整数 ai (0 ≤ ai ≤ 109— 数组a的n个元素

Output

输出只有一行,表示在数组a中,这样的完美区间有多少个。

SampleInput 1
3 1
1 2 3
SampleOutput 1
5
SampleInput 2
3 2
1 2 3
SampleOutput 2
3
SampleInput 3
3 3
1 2 3
SampleOutput 3
2

解析:

这道题考察的是异或,那么对于异或,有一些重要的性质:

      1.0 ^ 1 = 1,0 ^ 0 = 0,以上说明,0异或任何数的结果均为0.

      2.n^n = 0,这说明任何数与其自身异或后的结果必然为0.

利用以上两个性质,显然我们可以将异或前缀和处理一下,这样的话任意两个前缀和进行异或后的结果以及其所有预处理好

的前缀和自身,就可以将所有连续区间遍历完毕。这样直接处理的话时间复杂度是O(n^2),而n <= 10 ^ 6,这样做显然是超时

的。那么怎么样降低时间复杂度呢?

试想,对于任意一个前缀和区间,我们要做的是,找到其他满足条件的前缀和区间或者0,使得两者异或后的结果大于等于k,

即对于任意的前缀异或和sum,我们要做的是找到 sum ^ [] >= k,因此这是一个匹配的过程,而对于两个数字的比较,除了直接判

断之外,我们还可以通过比较其二进制的形式来比较其大小。比如说:数字7和数字6,四位二进制形式分别为(0111) 和 (0110),用

这样的方式我们当比较到两个数的最后一位时,才能确定 0111 > 0110,而对于其二进制的存储及比较,我们可以看成对01的存储,

然后再匹配

那么这个过程我们就可以用字典树实现,初始时字典树只含有根节点,然后利用性质1,插入数据0。而后再一位一位的匹配比

较即可。注意k值的范围,从而确定树的深度。具体详细解释可看代码注释:

#include 
const int maxMoveStep = 29;          //由于kmax = 1e9,而2^30=1073741824,因此最多移位次数为29
typedef long long ll;
struct node{
    ll weight;        //表示节点权值,也就是说该节点下有多少个满足条件的数字
    node *next[2];    //分别表示01字典树的两个数位,0 1
    node(){
        weight = 0;
        next[0] = next[1] = NULL;
    }
};

void trieInsert(node *root,int value){
    node *p = root;
    for(int i = maxMoveStep;i >= 0;i--){
        int bit = value >> i & 1;        //从最高位存至最低位
        if(!p -> next[bit]){        //如果对应数位的节点不存在,则新建该节点
            p -> next[bit] = new node();
        }
        p -> weight++;
        p = p -> next[bit];         //存在则直接往下继续遍历即可
    }
    p -> weight++;
}

int trieQuery(node *root,int prefixSum,int k){
    node *p = root;
    ll ans = 0;
    for(int i = maxMoveStep;i >= 0;i--){
        int pBit = prefixSum >> i & 1;
        int kBit = k >> i & 1;
        if(kBit){             //如果k的该位为1,要使最后结果大于等于k,那么p指针就只能往pBit^1的方向移动
            pBit ^= 1;
        }else{              //如果k的该位为0,显然pBit^1方向的子树均满足条件,则可以将其结果计算进来
                            //然后p指针再往pBit方向遍历,考虑剩下半边子树满足条件的部分
            if(p -> next[pBit^1]){
                ans += p -> next[pBit^1] -> weight;
            }
        }
        p = p -> next[pBit];
        if(!p){
            return ans;
        }
    }
    return ans + p -> weight;        //考虑到了根节点,因此还要加上异或后结果等于k的部分
}

void solve(int n,int k){
    int number,sum = 0;
    ll cnt = 0;
    node *root = new node();          //01字典树的根节点
    trieInsert(root,0);             //插入初始值,以方便考虑到所有的情况,包括前缀异或和其本身
    for(int i = 0;i < n;i++){
        scanf("%d",&number);
        sum ^= number;
        cnt += trieQuery(root,sum,k);
        trieInsert(root,sum);
    }
    printf("%I64d\n",cnt);
    delete root;
}

int main(){
    int n,k;
   // freopen("cf 665E.txt","r",stdin);
    while(scanf("%d %d",&n,&k)!=EOF){
        solve(n,k);
    }
    return 0;
}

总结:这次题目的总体难度不大,考察的知识点不多,但是01字典树还是很常用的,很多异或的问题都是用01字典树解决,第四

的dp题目属于比较简单的dp题,需要找相似的子问题,然后状态转移方程也不难推导出来。

 

你可能感兴趣的:(fjut,ACM集训队周赛题解)