2019.6.28 一场史上翻车翻的最彻底的考试【including 入门OJ·买汽水,奶牛编号,宠物之战

初见安。嗯,安。

这次考试真的……其实题目比较水吧……毕竟机房里有两位大佬都AK了呢……

我呢?因为这是一场3.5h的考试,然而事实上做了3h大家就都交了,自我感觉也挺好,一激动,在提交的时候交了个空文件夹然后就把电脑关机了……任由硬盘保护吞噬掉了我考试3h写出来的代码……所以就相当于爆0了。嗯。应该是历史上第一次爆0吧,以往再怎么不堪至少也能够猜对个10分的吧。

不过毕竟是操作失误,所以打击远没有昨天那场那么大……【明天补上】

后期在听题解讲评前,手动凭印象还原了前两题的代码,第一题过了,第二题爆long long了50分,第三题*&&……打了个100+行的树剖暴力,原本就想着顶多拿70分所以就懒得还原了……

买汽水

传送门:入门OJ P2128

Description

暑期集训一共N天,大家都辛苦了,Symbol准备给大家买汽水,但是钱只有M。每天买汽水的花销都是不固定的,如
果不够钱,买到的汽水不够大家一起喝,那样子不太好对不对?所以我们要买的话,就得让每个人都能喝到汽水要
不我们那天就不买了。现在给出每天买汽水的花销,请问我们一共最多能够花掉Symbol多少钱呢?暑假最多不超过
40天,Symbol给大家花的钱最多有一亿。

Input

输入第一行有两个整数N,M。 1<=N<=40,0<=M<=100000000。
接下来一行有N个数,表示某一天的汽水花销。每天汽水的花销p<=100000000。

Output

输出一个整数,表示我们能够花掉Symbol多少钱。

Sample Input

3 10
1 2 3	

Sample Output

6

Sol

其实很水的。机房某大佬说可以直接排序过后贪心做……【听起来不太靠谱?】竟然还能过。

看到n<=40,就有一丝熟悉的感觉——这不和天平那个题一样的嘛!从大到小排序过后暴力搜,枚举当天是买还是不买,加上剪枝就能过了。

#include<algorithm>
#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
int read() {
    int x = 0, f = 1, ch = getchar();
    while(!isdigit(ch)) {if(ch == '-') f = -1; ch = getchar();}
    while(isdigit(ch)) x = (x << 1) + (x << 3) + ch - '0', ch = getchar();
    return x * f;
}
 
int n, m;
int a[50];
long long sum[50];
bool cmp(int a, int b) {return a > b;}
 
long long out = 0;//怕爆掉……毕竟sum在累加的一瞬间可能爆int
void dfs(int now, long long ans) {
    if(now > n) {out = max(out, ans); return;}//正常返回
    if(ans + sum[n] - sum[now - 1] < out) return;//前缀剪枝:如果全都能买都不如答案优就别搜了
    if(out == m) return;//这里的剪枝很有必要,一句就能从TLE拉回到AC
    if(ans + a[now] < m) dfs(now + 1, ans + a[now]);
    else if(ans + a[now] == m || out == m) {out = m; return;}//这里也要同时剪一刀
    dfs(now + 1, ans);
}
 
int main() {
    n = read(), m = read();
    for(int i = 1; i <= n; i++) a[i] = read();
    sort(a + 1, a + 1 + n, cmp);
    for(int i = 1; i <= n; i++) sum[i] = sum[i - 1] + a[i];//sum存前缀
    dfs(1, 0);
    printf("%lld\n", out);
    return 0;
}

奶牛编号

传送门:入门OJ P2129

Description

作为一个神秘的电脑高手,Farmer John 用二进制数字标识他的奶牛。然而,他有点迷信,标识奶牛用的二进制数
字,必须只含有K位"1"(1 <= K <= 10)。 当然,每个标识数字的首位必须为"1"。FJ按递增的顺序,安排标识数字
,开始是最小可行的标识数字(由"1"组成的一个K位数)。不幸的是,他没有记录下标识数字。请帮他计算,第N
个标识数字(1 <= N <= 10^7)。

Input

第1行:空格隔开的两个整数,N和K。 

Output

如题,第N个标识数字

Sample Input

7 3

Sample Output

10110

Sol

刚拿到这个题其实很懵——因为很容易思考的时候思路岔开,比如明明在讨论位数,怎么就变成了数的大小了……

进入正题。首先可以先手动枚举一下这个过程,就拿样例来说,写出来就是这个样子的:

1111
21011
3

1101

41110
510011
610101
710110
811001
911010
1011110

如果我们单看一定位数的数,有没有发现:除去最高位,这就是一个组合数的问题?比如4位数的时候,除去最高位为1,那么剩下的三位就相当于是在3个位置中选两个的组合。也就是C_3^2 = 3,正好四位数的情况也就三种。所以我们可以得到一个结论:在确定有m位的时候,选k个作为1并且保证最高位为1的情况数量为C_{m - 1} ^ {k - 1}

假设我们已经知道了如何求组合数,那么接下来我们就可以依靠上述的结论来得到第n大的数的位数了——即当遇到当前的所有情况数 + 当前位数的所有情况数 > 所求的n时,就不用累加了,已经到了n所在的位数范围

找到过后,我们就可以开始逐个判断每一位是0还是1了。我们再次观察上述表格中的数——相同的位数的为一个整体地观察其最高位的后一位,有没有发现:次高位为0的话,出现的次数就是去掉这个0过后后面几位对于剩下的0的排列?当然,观察若这一位为1,可以得到近似的结论。就比如说4位数的时候,若这一位为0,除去最高位还有2个1,后面有两位,所以就有0个0可用于排列,所以次高位为0的情况数仅有一个。

也就是说我们可以直接利用上述的类似于找中间点划分开来的结论来判定当前位是0还是1。如果当前累计的情况数 + 当前这一位为0 的情况数 < 我们求的第n大,那么第n大的当前位就一定是1了。否则为0。如此,挨个筛过去,就可以得到ans了。

当然ans是必须用数组存的。

那么现在是时候颠覆我们的假设了——组合数怎么求?你想怎么求?也不让你取模,用不了Lucas定理,你除了暴力还能怎求?是的,暴力组合数公式就行了,C_n^m = \frac{n!}{(n-m)!m!}。当然如果先累乘再整除的话还是会爆long long的,所以——边乘边除就好啦!!!判断能否整除,能则除。按组合公式的正确性,最后是一定可以刚好整除完的!!!

没了。当然,对于求组合数的部分,应该dp也是可以做到的【反正我不会……】

上代码……

#include<algorithm>
#include<iostream>
#include<cstring>
#include<cstdio>
#define maxn 10000005
using namespace std;
int read() {
    int x = 0, f = 1, ch = getchar();
    while(!isdigit(ch)) {if(ch == '-') f = -1; ch = getchar();}
    while(isdigit(ch)) x = (x << 1) + (x << 3) + ch - '0', ch = getchar();
    return x * f;
}
 
typedef long long ll;
ll C(ll n, ll m) {
    if(!m) return 1;//可以特判一下的
    ll f1 = 1, f2 = 2;
    for(int i = m + 1; i <= n; i++) {
        f1 *= i;
        while(f1 % f2 == 0 && f2 <= n - m) f1 /= f2, f2++;//边乘边除,天然保证答案正确性
    }
    return f1;
}
  
int n, k, cnt = 0;
bool ans[maxn];
int main() {
    n = read(), k = read(); k--;//因为最高位固定,所以可用于组合的只有k - 1个1
    register int now = 0;
    for(int i = k; ; i++) {
        ll c = C(i, k);
        if(now + c >= n) {//确定到位数为i + 1【因为第一位确定了不参与枚举
            ans[++cnt] = 1;
            for(int j = i; j > 0; j--) {//所以后面每一位的枚举从i开始
                ll mid = C(j - 1, j - 1 - k);//求若这一位为0,则会有的情况数
                if(now + mid < n) ans[++cnt] = 1, k--, now += mid;//记得k要-1,最后自然可以减到0
                else ans[++cnt] = 0;//当然直接cnt++也行的
            }
            break;//记得结束循环
        }
        else now += c;//否则继续累加
    }
     
    for(int i = 1; i <= cnt; i++) if(ans[i]) putchar('1'); else putchar('0');
    //这里不用putchar而是用printf的话输出都可以超时的……
    return 0;
}

第二题也挺简单……【为什么我总是遇到dp的题想到组合数呢……】

宠物之战

传送门:入门OJ P2130

Description

众所周知,moreD的宠物已经被moreD奴役得体无完肤。这只宠物实在忍无可忍,把自己每天走魔法树的经历告诉了
自己的宠物。同时他还说明了自己爬树是多么地慢,以至于moreD每天都残酷地训练他爬树。幸运的是moreD的宠物
的宠物不是moreD的宠物,moreD的宠物深知"宠物是用来宠的而不是用来奴役的"这一点,所以moreD的宠物对待自
己的宠物很有爱。所以moreD的宠物与其宠物商量着要推翻moreD的暴政,方法是把moreD告上法庭,就以自己每天
被迫爬树来做证据。由于魔法树是树,训练树当然也是树啦。moreD的训练有着GX的文化,每天moreD会把自己的宠
物通灵到树的一个端点上,这个通灵点可能与之前的通灵点相同。然后moreD命令他的宠物从这个点开始走,让这
只宠物随便访问自己该天之前没有访问过的节点,一直走到该天无路可走,训练才会结束,宠物才可以休息。more
D的宠物每天都会在这棵树上训练,幸运的是他每天走过的训练路径都不同,直到若干天后,所有可能的训练路径
都被走遍了。你,作为moreD宠物的宠物,一只被moreD的宠物宠着的宠物,当然想帮moreD的宠物算出他总共走过
的路径长度啦。【输入格式】第一行包含两个正整数N,M,表示树的点数与边数。接下来M行,每行三个正整数表示
Li,bi,ci分别表示树上有一条长度为Li的连接bi,ci两个结点的边。所有输入的整数均不大于100,000,输入的树保
证连通,无重边,无自环。

Input

仅一行表示答案。

Output

总共走的路径长度

Sample Input

5 4
1 2 1
1 3 1
2 4 2
2 5 2

Sample Output

37
【样例解释】
可能的训练路径有(1-2-4),(1-2-5),(1-3),(2-4),(2-5),(2-1-3),(3-1-2-4),(3-1-2-5)
(4-2-5),(4-2-1-3),(5-2-4),(5-2-1-3)
长度依次为3,3,1,2,2,2,4,4,4,4,4,4。和为37.

Sol

这个题吧……其实反应出来了是树形dp但是【我也忘了当时在想些什么】因为发现好像统计答案的时候还是只有n^2的思路,所以就舍近求远地写了个树剖求路径长度,准备只要那70分……【然后代码就被吃了嘤嘤嘤】

正解也就是树形dp了。全程O(n)的。其实对于这一类的题目有一个要点就是——考虑每个点/每个边对答案的贡献。这样想的话就可以得到O(n)的解法。

翻译一下题意就是——每个路径至少一端是一个叶子节点。因为这个题问的是边权,所以我们就可以考虑每条边对于答案的贡献——因为一条路径的逆向路径也算是一条独立的路径,所以这里我们首先不用考虑去重之类的问题。再者,如图。

假设我们考虑到了边1->3,那么我们就视作这条边把这棵树划分为了两个集合,那么这条边会走的次数就是集合的两边任意一边贡献出叶子节点后的方案数的总和。什么意思呢?我们看左边这个集合有两个叶子节点4和5,右边有3,因为左边有两个叶子,所以右边集合的两个点都会有路径分别连向那两个叶子节点;同理,因为右边集合有一个叶子节点,所以左边集合的三个节点都会分别连向右边的那一个叶子节点。所以我们设节点1为确定树的形态的根节点,lef[u]为以u为根节点的子树的叶子节点数,size[u]为以u为根节点的子树大小。那么对于一条边u->v,对答案的贡献就为:(size[1] - size[v]) * lef[v] + size[v] * (lef[1] - lef[v])

dfsO(n)预处理出lef和size,然后再dfs一次累计答案,输出即可。

注意,累计答案的时候两个累乘一定要先转型为long long相乘,否则也会被爆int的……

上代码啦~

#include<algorithm>
#include<iostream>
#include<cstring>
#include<cstdio>
#define maxn 200005
#define int long long
using namespace std;
int read() {
    int x = 0, f = 1, ch = getchar();
    while(!isdigit(ch)) {if(ch == '-') f = -1; ch = getchar();}
    while(isdigit(ch)) x = (x << 1) + (x << 3) + ch - '0', ch = getchar();
    return x * f;
}
 
struct edge {
    int to, w, nxt;
    edge() {}
    edge(int tt, int ww, int nn) {to = tt, w = ww, nxt = nn;}
}e[maxn << 1];
 
int head[maxn], k = 0;
void add(int u, int v, int w) {e[k] = edge(v, w, head[u]); head[u] = k++;}
 
int size[maxn], lef[maxn], fa[maxn];
long long ans = 0;
void dfs(int u) {
    bool flag = true;//假设这是一个叶子节点
    size[u] = 1;
    for(register int v, i = head[u]; ~i; i = e[i].nxt) {
        v = e[i].to; if(v == fa[u]) continue;
        fa[v] = u;
        dfs(v);
        flag = false;//既然有合法的子节点,那么这就不是叶子节点了
        size[u] += size[v]; lef[u] += lef[v];
    }
    if(flag) lef[u] = 1;//没有合法子节点,那么就是叶子节点
}
 
void dfs2(int u) {
    for(register int i = head[u], v; ~i; i = e[i].nxt){
        v = e[i].to; if(v == fa[u]) continue;
        ans += (1ll * size[v] * (lef[1] - lef[v]) + 1ll * (size[1] - size[v]) * lef[v]) * e[i].w * 1ll;
        dfs2(v);
    }
}
 
int n, m;
signed main() {
//  freopen("in.txt", "r", stdin);
    memset(head, -1, sizeof head);
    n = read(), m = read();
    for(register int u, v, w, i = 1; i <= m; i++) //注意这里的读入顺序……
        w = read(), u = read(), v = read(), add(u, v, w), add(v, u, w);
 
    dfs(1), dfs2(1);
    printf("%lld\n", ans);
    return 0;
}

这次考试就总结完了。嗯。

迎评:)
——End——

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值