Codeforces Round #509 (Div. 2) 解题报告

A - Heist

某个店被抢劫了, 只记得所有的商品是从x开始标记,然后有y个,现在只知道剩下的商品的标号,求最少被偷了多少商品。

很简单了,排序,然后从头到位遍历一边,看中间缺了多少,就加多少就行

n = int(input())
keyboards = map(int, input().split())

keyboards = sorted(keyboards)

#print(keyboards)

last = keyboards[0]
res = 0
for i in keyboards[1:]:
    if last + 1 != i:
        res += i - last - 1
    last = i
print(res)

B - Buying a TV Set

已知家里的墙大小为a和b,想要买比例为x:y的电视。商场里的电视尺寸可以为任意大小。求问可能存在多少能放在家里的墙上,并且比例为x:y的电视。

求出xy的gcd,然后把x和y分别除以gcd,得到最小的电视尺寸minX,minY。剩下的就是看墙能放下多少minX或者minY了。取a/minX和b/minY里面较小的整数即可(大的那个会导致另一个方向超长)

import math
[a,b,x,y] = map(int, input().split())

gcd = math.gcd(x,y)

minX = x / gcd
minY = y / gcd

counta = int(a//minX)
countb = int(b//minY)
print(min(counta, countb))

C. Coffee Break

这个人每天都工作m分钟。然后他很喜欢喝咖啡,每次喝咖啡消耗1分钟。但是老板不喜欢他摸鱼,所以他至少要隔d分钟才能喝一次咖啡。然后这个人很矫情,想要在n个不同的分钟里面喝咖啡。。。嗯。。。太尼玛奇葩了。。。求需要多少天,才能把这n个喝咖啡的时间都体验过。

相信看了我中文描述,还是有人不懂这个题目到底说啥。。。所以我当时就没看懂这个题目,果断跳过了。

后来比赛期间有两次announcement,都是针对这个题目的,包括,1,不用考虑休息日,2,n个分钟都是独立的。

哦,所以题目是希望我们把n个分钟数,放到k个工作日里面,要求k尽可能小,同时满足每次喝咖啡间隔d分钟的条件即可。注意每天开始的时间都重算,不用等d分钟再开始喝咖啡,可以一开始就喝一次。

用贪心算法,把所有的咖啡时间都排序,然后从头到尾遍历,每天开始,先拿队列里第一个没有喝过咖啡的时间a,然后把这个时间加d,找到第一个大于a+d的喝咖啡时间,直到所有时间遍历完。

注意,由于测试用例为10e5级别,所以不能做成O(n^2)的时间复杂度,必须在O(nlogn)时间复杂度内解决战斗。所以,我们在取时间的时候需要有技巧

1,使用一个map来存喝咖啡的时间和他对应的索引i,以喝咖啡时间为key,索引i为value,就可以实现根据时间排序的效果了。

2,每次使用了某个时间,直接将该时间从map里面移除。

3,已经定好了本次喝咖啡的时间,查找下次喝咖啡的时间,不能用遍历法,必须用二分查找来寻找。遍历法会在最坏情况下降到O(n^2)时间复杂度。

#include <bits/stdc++.h>
using namespace std;

typedef unsigned long long ull;
typedef long long ll;

#define TESTINGx

#ifdef TESTING
#define DEBUG(S, args...) {printf(S, args);printf("\n");}
#else
#define DEBUG(S, args...) 
#endif

int cmp(const void* a,const void* b)
{
    int a1 = *(int*)a;
    int b1 = *(int*)b;
    return a1 - b1;
}

int main()
{
    int n,m,d;
    cin >> n >> m >> d;
    map<int, int> index;
    int x;
    set<int> times;
    int ans[200005];

    for (int i = 0; i < n; ++i) {
        scanf("%d", &x);
        index[x] = i;
    }
    int day = 1;
   
    while(!index.empty())
    {
        //begin of a day, drink one coffe :)
        auto it = index.begin();
        ans[it->second] = day;

        int last = it->first;
        DEBUG("last is :%d", *it);
        index.erase(it);
        while((it = index.upper_bound(last + d)) !=index.end() ) {
            ans[it->second] = day;
            last = it->first;
            index.erase(it);
        }
        day++;
    }
    printf("%d\n", day - 1);
    for (int i = 0; i < n; ++i) {
        printf("%d ", ans[i]);
    }
    printf("\n");

    return 0;
}

D - Glider

题目给出n个风区,你可以从任何高为h的地方开始滑翔,没有风区的时候,按每秒下降1个单位下降,在风区内,则保持水平滑翔。问,从哪里飞出能飞最远的水平距离。

第一眼就觉得像动态规划。于是仔细分析,还真是。

我们首先直到,最优的解法,肯定是从某个风区的开始处出发。这个简单思考就能得到。在风区中间,肯定是浪费的。在非风区中间,即使能飞最远距离,也肯定是等效于从风区出发的,因为非风区飞行的距离是固定的,总是h。

为了避免重复计算,我们需要记录三个数据。

dp[i][0],从第i个风区出发,可以飞行多少距离

dp[i][1],从第i个风区出发,到达最后一个风区的时候,会从什么高度出来。

dp[i][2],从第i个风区出发,最后经过的风区是哪个。

记录了这些内容之后,我们就可以从最后一个风区开始,往前计算每个风区飞行的距离了。

1,初始化dp[n][0] = h+distance[n], dp[n][1] = h, dp[n][2]  = n

2, 初始化dp[i][0] = distance[i+1] + distance[i],dp[i][1] = dp[i+1][1]- distance_between[i][i+1],dp[i][2] = dp[i+1][2]

3,如果dp[i][1] 小于等于0,说明最后一个风区不能到达(已经撞到地上去了),需要往前移动。从dp[i][2]开始,不断往前,每次递减dp[i][2],然后dp[i][2]递减风区distance_between[  dp[i][2] ] [dp[i][2] - 1 ],dp[i][0]则递减 distance[dp[i][2]

每次都尝试用dp[i][0]的值更新答案即可

#include <bits/stdc++.h>
using namespace std;

typedef unsigned long long ull;
typedef long long ll;

#define TESTINGx

#ifdef TESTING
#define DEBUG(S, args...) {printf(S, args);printf("\n");}
#else
#define DEBUG(S, args...) 
#endif


int main()
{
    int n, h;
    int x[200005][2];
    ll dp[200005][3];
    cin >> n >> h;
    for (int i = 1; i <= n; ++i) {
        scanf("%d %d", &x[i][0], &x[i][1]);
    }

    dp[n][0] = h + (x[n][1] - x[n][0]);
    dp[n][1] = h;
    dp[n][2] = n;
    DEBUG("dp[%d][0]:%lld, dp[%d][1]:%lld, dp[%d][2]:%lld", n,dp[n][0],n,dp[n][1],n,dp[n][2]);
    ll ans = dp[n][0];
    for(int i = n - 1; i > 0; --i)
    {
        //delta
        int delta = x[i+1][0] - x[i][1];

        DEBUG("i:%d, delta %d", i, delta);
        dp[i][0] = dp[i+1][0] + (x[i][1] - x[i][0]);
        dp[i][1] = dp[i+1][1] - delta;
        dp[i][2] = dp[i+1][2];
        while(dp[i][1] <= 0 && dp[i][2] >= i)
        {
            DEBUG("moving dp[%d][0]:%lld, dp[%d][1]:%lld, dp[%d][2]:%lld", i,dp[i][0],i,dp[i][1],i,dp[i][2]);
            int curWind = dp[i][2];
            dp[i][0] -= x[curWind][1] - x[curWind][0];
            dp[i][1] += x[curWind][0] - x[curWind - 1][1];
            dp[i][2]--;
        }
        ans = std::max(ans, dp[i][0]);
        DEBUG("dp[%d][0]:%lld, dp[%d][1]:%lld, dp[%d][2]:%lld", i,dp[i][0],i,dp[i][1],i,dp[i][2]);
    }
    printf("%lld", ans);

    return 0;
}

E - Tree Reconstruction

有一棵树,其节点从1到n顺序标记。输入是将每一条边分别去除后,分成两个子图,每个子图里面的点的最大标记数。求问是否可以根据输入构造一个树。然后求这个树。

图论表示不熟,看答案的。

需要构造成树很简单,

1,由于输入规定每次输入的 a<=b总是成立,所以我们需要判定,b总是为n,否则就是不成立的。试想,不管去掉哪个边,总有一个子图里面有第n个节点嘛。这个好说。

2,对于所有a,按从小到大排列。然后,对于任意一个ai, 总有 i > ai。这是因为,对于任意索引k,你最多只能构造k个子树,使得k总是最大节点。这个推理需要递归地往下推,这就得到了i > ai的条件了。

然后,关于怎么构造的问题。事实上,由于没有附带条件,你可以直接构造一个所有节点都只有一个儿子节点的bamboo。然后根节点是n,叶子节点是a1。

构造过程如下:

1,先把a排序,从小到大排

2,对于任意一个i,如果ai < ai+1成立,我们把ai+1用上。

3,否则,我们从没有用过的节点里面找一个,最小的。

最后,把n接上,就ok了。

#include <bits/stdc++.h>
using namespace std;

typedef unsigned long long ull;
typedef long long ll;

#define MAXN 1005//2e5

#define TESTINGx

#ifdef TESTING
#define DEBUG(S, args...) {printf(S, args);printf("\n");}
#else
#define DEBUG(S, args...) 
#endif

int cmp(const void*a, const void* b)
{
    int ia = *(int*)a;
    int ib = *(int*)b;
    return ia - ib;
}

int main()
{
    int n,a,b;
    int input[MAXN];
    cin >> n;

    for (int i = 1; i < n; ++i) {
        scanf("%d %d", &a, &b);
        input[i] = a;
        if(b != n)
        {
            printf("NO\n");
            return 0;
        }
    }

    qsort(input, n, sizeof(int), cmp);
    for(int i = 1; i < n; ++i)
    {
        DEBUG("input[%d] %d", i,input[i])
        if(i > input[i])
        {
            printf("NO\n");
            return 0;
        }
    }

    vector<int> ans;
    set<int> unused;
    for (int i = 1; i < n; ++i) {
        unused.insert(i);
    }
    ans.push_back(input[1]);
    unused.erase(input[1]);
    for(int i = 2; i <n; ++i)
    {
        if(input[i] <= input[i-1])
        {
            auto first_unused = unused.begin();
            ans.push_back(*first_unused);
            unused.erase(first_unused);
        }
        else
        {
            ans.push_back(input[i]);
            unused.erase(input[i]);
        }
    }
    ans.push_back(n);
    printf("YES\n");
    for (int i = 1; i < ans.size(); ++i) {
        printf("%d %d\n", ans[i-1], ans[i]);
    }
    return 0;
}

F - Ray in the tube

有一个地铁的管道,管道上下两边装了很多感应器。我们的任务是,从管道的上下分别选一个点,作为激光的入射路径,使得激光能尽可能多的射在感应器上面。激光遇到管道的墙壁总是做镜面反射,可以把管道内壁认为是一面光滑的镜子。感应器不影响激光的反射和前进。

首先,管道有多高,是没有用的。我们只需要关心x的距离,选择合适的dx,然后计算能照射到多少感应器即可。

其次,dx的选择,实际上最优选择总是2的指数次(包括0次)或者0。嗯。。。这里感觉就被大神们智商压制了,想了好久才明白这个是为什么。

我们先看怎么计算照射到的感应器。

对于任意dx,以及启动位置Ax,我们可以知道,从发射的那面管道,我们能覆盖的点是Ax + 2dx, Ax + 4x, Ax + 6x ..... Ax + 2k dx

然后对于另一面,我们能覆盖到的点是Ax + 3dx, Ax + 5dx, Ax+7dx ......Ax + (2k + 1)dx

ok,可以看出,对于第一个序列,如果dx为大于1的奇数,设dx=pd'x,则第一面的队列就变成了Ax + 2pd'x , Ax + 4pd'x ... Ax + 2kpd'x,这个队列明显是Ax + 2dx, Ax + 4x, Ax + 6x ..... Ax + 2k dx的子队列。所以没有必要额外枚举一次。同时,另一边,则有 Ax+3pd'x, Ax + 5pd'x, Ax + 7pd'x ... Ax + (2k +1)pd'x,由于p为奇数,所以这个队列肯定也是Ax+3d'x, Ax + 5d'x, Ax + 7d'x ... Ax + (2k +1)d'x的子队列。故而也没有必要枚举一次。(如果想不明白,可以随便找个数字带进去,然后用dx=1比较一下)

至此,我们可以看到,奇数的dx(除了1)以外,都是不需要枚举的。那么什么是2的指数次呢?实际上,针对Ax + 2dx, Ax + 4x, Ax + 6x ..... Ax + 2k dx这个数列,枚举0,1,2就足够覆盖所有case了。但是对于Ax + 3dx, Ax + 5dx, Ax+7dx ......Ax + (2k + 1)dx这个队列,则不然。这是由于他们的起始dx为奇数导致的。

可以看到,如果dx取2,得到的队列是

6 10 14 18 。。。。

如果dx取4,得到的队列就是

12 20 28 。。。。。

2不能覆盖4的情况。

所以只需要枚举log(10^9)种dx即可。每种dx,都要照对应的Ax,这个只需要针对每隔位置的坐标x进行(激光出发行)X mod (2dx)或者(非激光出发行) X + d mod (2dx),得到的结果,就是需要取多少Ax才能照射到。遍历所有的镭射,用这个模运算的结果做key,累加答案,然后遍历所有模结果,更新答案即可。

#include <bits/stdc++.h>
using namespace std;

typedef unsigned long long ull;
typedef long long ll;

#define MAXN 100005  //2e5

#define TESTING

#ifdef TESTING
#define DEBUG(S, args...) {printf(S, args);printf("\n");}
#else
#define DEBUG(S, args...) 
#endif

int n[2];
ll input[2][MAXN];
int ans = 1;

void solve()
{
    //check zero
    int k = 0;
    for(int i = 0;i < n[0]; i++)
    {
        while(input[1][k] < input[0][i])
        {
            k++;
        }
        if(input[1][k] == input[0][i])
        {
            DEBUG("hit:%d, %d", k, i)
            ans = 2;
            break;
        }
    }
    //check for 1 and 2
    for(ll i = 1;i <= 10e9; i *= 2 )
    {
        ull mod = i * 2;
        map<int,int> res;
        for(int j = 0; j < 2; j++)
        {
            for(int l = 0; l < n[j]; l++)
            {
                res[(input[j][l] + j * i) % mod]++;
            }
        }
        for(auto it:res)
        {
            DEBUG("i %d, first %d,second %d", i, it.first, it.second)
            ans = max(ans,it.second);
        }
    }
}

int main()
{
    int y;
    cin >> n[0] >> y;
    for (int i = 0; i < n[0]; ++i) {
        scanf("%lld", &input[0][i]);
    }

    cin >> n[1] >> y;
    for (int i = 0; i < n[1]; ++i) {
        scanf("%lld", &input[1][i]);
    }

    solve();
    printf("%d", ans);

    
    return 0;
}

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值