NOIP2018提高组省一冲奖班模测训练(三)

NOIP2018提高组省一冲奖班模测训练(三)

自己按照noip的方式考,只在最后一两分钟交了一次

第一题过了,对拍拍到尾。

第二题不会。考试时往组合计数的方向想,推公式,推了一个多小时,大脑爆炸,还是一直推不出来(然而正解是dp??)

第三题打了暴力

如果是正式比赛的话,就在省一分数线徘徊。

第二题如果能拿一点部分分的话,那省一就比较稳了

正式比赛的话应该部分分会有,这套题的第二题部分分貌似拿不了

讲解讲的非常细,非常清晰。比雅礼集训听的舒服太多太多了。

Anan的派对 

Anan想举办一个派对。
Anan的朋友总共有 n 人。第i个人如果参加派对会得到 ci 的快乐值,除他自己外每多一个人参加他会减少 di 的快乐值。
Anan想让这个派对的总快乐值尽可能大,在这个基础上,能来的人越多越好。
Anan想知道最佳的邀请方案的快乐值与参加人数。
对于 50% 的数据, n20 
对于 100% 的数据, n1000 
Input
第一行一个正整数n 。
第二行n个整数表示ci。
第三行n个整数表示di。
Output
第一行一个整数最大快乐值。
第二行一个整数表示最佳方案的参加人数。
Input示例
6
10 10 10 10 10 9
2 2 2 2 2 3
Output示例
18
3



1000的数据范围一般是n方,最多加两个log
这道题第一反应二分参加人数不就非常好算了吗
但是貌似参加人数不满足单调性
然后这个思路就pass掉了
然后开始绕弯子,想到搜索,想到动规……
一直绕了40多分钟……
然后突然想到,貌似直接枚举人数复杂度是n方logn的
…………
相信自己的第一感觉……
然后不要凭感觉看会不会超时,要算复杂度……
最后1个小时才做完了,排行榜上有人17分钟AC了……


枚举出人数之后,有两个小优化(不加也能过)
(1)排序的复杂度是nlogn,而我们关心的只是前几个人的值,所以可以用nth_element
复杂度是O(n)的。
可以把n方logn 优化到n方
(2)如果有人的值是负数可以直接break,这个人不选还更优
#include<bits/stdc++.h>
#define REP(i, a, b) for(register int i = (a); i < (b); i++)
#define _for(i, a, b) for(register int i = (a); i <= (b); i++)
using namespace std;

const int MAXN = 1e3 + 10;
int c[MAXN], d[MAXN], t[MAXN], n;

int main()
{
    scanf("%d", &n);
    _for(i, 1, n) scanf("%d", &c[i]);
    _for(i, 1, n) scanf("%d", &d[i]);
    
    int ans1 = 0, ans2 = 0;
    _for(num, 1, n)
    {
        _for(i, 1, n) t[i] = c[i] - d[i] * (num - 1);
        nth_element(t + 1, t + num, t + n + 1, greater<int>());
        
        int sum = 0, ok = 1;
        _for(i, 1, num) 
        {
            sum += t[i];
            if(t[i] < 0) { ok = 0; break; }
        }
        if(!ok) break;
        
        if(ans1 < sum) { ans1 = sum; ans2 = num;}
        else if(ans1 == sum && ans2 < num) ans2 = num;
    } 
    
    printf("%d
%d
", ans1, ans2);
    
    return 0;
}

 

XYG的蛋糕

XYG要过生日了,他准备了一个 n×m 的矩形蛋糕请大家吃。
切蛋糕的方法如下:每次选出已经分出的某一个矩形蛋糕,一刀将其切成两个小矩形蛋糕。当然,这一刀可以选择横切或者竖切。最终XYG要把蛋糕切成 n×m 块 1×1 的小蛋糕。
XYG希望你告诉他总共有多少种不同的切法。两个切法不同当且仅当其中一个切法中存在一条刀痕,在另一个切法中不存在。
对于 40% 的数据, n,m15 
对于 100% 的数据, n,m300 
Input
一行两个正整数n,m。
Output
一行一个正整数表示方案数998244353取模。
Input示例
3 2
Output示例
4



我第一反应数学问题,应该要用到什么组合数之类的。

然后就推啊推啊推啊推啊
还是没有推出来
中间发现有子问题结构,但没有深入去想

看到300的数据范围的时候应该马上反应到正解应该是一个n三方的复杂度
正解是dp
其实我发现有子问题结构就应该深入去想dp的
我们可以发现当矩形切了之后,矩形变小了,就可以利用之前的值
可以设dp[i][j]表示i * j的矩形的方案数
显然那么我们可以枚举切在哪个位置
先考虑横着切,切在k
那么方案数是
∑dp[k][j] * dp[i-k][j] 1 <= k < i
这里用到了乘法原理,两边的矩形是分步的,所以是乘法
同理竖着切
∑dp[i][k] * dp[i][j-k] 1 <= k < k
那么根据加法原理,dp[i][j]就是把两个加起来了。

但是这么做有一个问题,会重复计算
比如第一刀切第2行,第二刀切第3行
和第一刀切第3行,第二刀切第2行
刀痕是一样的,但是算了两次
这么办呢,我推数学公式的时候也在这卡住了,想不到好的解决方案

这个时候我们想想两层for循环枚举两两之间元素的时候,我们第二层循环j通常是从i+1开始,因为这样不会重复
枚举相同的i,j。那么我们可以把这个思想迁移到这道题。
如果我们当前是横着切,那么切的这一行以前一定没有横着切过。这样可以保证不重复,竖着切同理
怎么实现呢

我们可以设dp[i][j][0~2]
dp[i][j][1]表示只是横着切的时候的方案

dp[i][j][2]表示只是竖着切的时候的方案
dp[i][j][0]表示横竖都可以切的时候的方案

那么转移方程就是
dp[i][j][1] = dp[k][j][2] * dp[i-k][j][0]
dp[i][j][2] = dp[i][k][1] * dp[i][j-k][0]
dp[i][j][0] = dp[i][j][1] + dp[i][j][2]
答案为 dp[n][m][0]

#include<bits/stdc++.h>
#define REP(i, a, b) for(register int i = (a); i < (b); i++)
#define _for(i, a, b) for(register int i = (a); i <= (b); i++)
using namespace std;

typedef long long ll;
const int MAXN = 300 + 10;
const int mod = 998244353;
ll dp[MAXN][MAXN][3];
int n, m;

int main()
{
    scanf("%d%d", &n, &m);
    REP(i, 0, 3) dp[1][1][i] = 1;
    
    _for(i, 1, n)
        _for(j, 1, m)
        {
            if(i == 1 && j == 1) continue;
            REP(k, 1, i)
                dp[i][j][1] = (dp[i][j][1] + dp[k][j][2] * dp[i-k][j][0] % mod) % mod;
            REP(k, 1, j)
                dp[i][j][2] = (dp[i][j][2] + dp[i][k][1] * dp[i][j-k][0] % mod) % mod;
            dp[i][j][0] = (dp[i][j][1] + dp[i][j][2]) % mod;
        }
        
    printf("%lld
", dp[n][m][0]);

    return 0;
}

WZD的洞洞

WZD有一个形状为直角三角形的洞洞。
平面上有n个物品,每个物品有坐标xi,yi和价值wi,WZD希望用洞洞包含住价值尽可能高的物品。
由于洞洞的构造,其两条边必须平行于坐标轴,且直角在左下方(也就是说,洞洞只能平行移动),并且要求直角必须恰好落在一个物品上。
求洞洞能包含住最大价值为多少的物品。
40%的数据保证n≤5000;
100%的数据保证n≤100000;
100%的数据保证0<xi,yi≤10000,-10000<wi<10000。
Input
第一行一个整数n,表示有n个礼品。
第二行两个整数a,b,表示这个直角三角形的两个直角边的长度(a为平行于x轴的直角边的长度,b为平行y轴的直角边的长度)。
接下来n行,每行三个整数xi,yi,wi。
Output
仅一个数,表示最大价值。
Input示例
4
5 5
1 7 100
1 1 2
2 2 99
7 1 100
Output示例
101

这一题我判断点是不是在三角形内的方式中斜边是用数学中线性规划的方式判断的
完美的避开了正解
这道题的关键是一个三维偏序问题,但实际上是一个二维偏序问题
一个点被另外一个三角形包含有哪些条件?
三条边各一个条件
两条直角边就是对x和y坐标的限制
如果点j被点i的三角形覆盖
则有 xj >= xi, yj >= yi
这是很显然的
那么斜边呢
我开始是用线性规划的方法,推出斜边的方程,然后把点带进去如果小于0那就在斜边的下方
这么算没有错,但不能带来优化
我们要用一个比较麻烦却可以优化的方式来做
就是算出每个点到Bx + Ay = 0的距离(A,B如题目所述)

那么如果点j被点i的三角形覆盖
则有
dis[j] - dis[i] <= A * B / sqrt(A * A + B * B)
等号右边是三角形斜边上的高
这个式子可以拿笔画一下图,就懂了
那么我们可以用一个滑动窗口 O(n)的维护这个东西
先按照dis[i]从小到大排序
从大到小遍历,也就是逆序
每次把当前点的dis加入队列,这个时候队列里面每一个点的dis都是大于当前这个点的dis的
因为是按照从大到小加入的
那么最先加入的,也就是说现在处于队尾的,dis值就是最大的
那么可以判断拿队头(也就是新加入的点)和队尾做判断,看不满满足
dis[r] - dis[l] <= A * B / sqrt(A * A + B * B)
不满足就踢掉队尾,然后继续判断新的队尾,直到满足为止

dis的大小解决了,那么x和y呢??
我们可以观察出来,按照上述方式遍历,对于当前点i,和之前加入的点j
不可能同时满足xi > xj yi > yj
反证法。如果满足的话,那么有dis[i] > dis[j]
那么j这个点肯定还没有加入。
所以对于最坏只满足xi > xj yi > yj 其中一个
或者两个都不满足

那么我们把满足其中一个的点的权值去掉。
由于两个条件不会有交集
用两个树状数组维护就好了

然后有一些细节要注意,见注释
#include<bits/stdc++.h>
#define REP(i, a, b) for(register int i = (a); i < (b); i++)
#define _for(i, a, b) for(register int i = (a); i <= (b); i++)
using namespace std;

typedef long long ll;
const int MAXN = 1e5 + 10;
const int MAXM = 1e4;
struct node
{
    int x, y, w;
    double dis;
    bool operator < (const node& rhs) const
    {
        return dis < rhs.dis;
    }
}s[MAXN];
int q[MAXN], n, A, B;

struct Binary_Index_Tree
{
    int f[(MAXN << 1) + 10];
    
    Disjoint_Set() { memset(f, 0, sizeof(f)); }
    
    inline int lowbit(int x) { return x & (-x); }
    
    void add(int x, int p)
    {
        x += MAXN + 1; //如果下标有负数,在这里改一下就好了,使得下标大于等于1,下标最大要改,数组空间要改
        for(; x <= (MAXN << 1) + 1; x += lowbit(x))  
            f[x] += p;
    }
    
    int sum(int x)
    {
        int res = 0;
        x += MAXN + 1;
        for(; x; x -= lowbit(x))
            res += f[x];
        return res;
    }
}X, Y;

int main()
{
    scanf("%d%d%d", &n, &A, &B);
    REP(i, 0, n) 
    {
        scanf("%d%d%d", &s[i].x, &s[i].y, &s[i].w);
        s[i].dis = (B * s[i].x + A * s[i].y) / sqrt(A * A + B * B);
    }
    sort(s, s + n);
    
    double dmax = A * B / sqrt(A * A + B * B); //用double,int有误差 
    int ans = 0, sum = 0, l = 1, r = 0; //注意这里l与r的取值 
    
    for(int i = n - 1; i >= 0; i--)
    {
        sum += s[i].w;
        q[++r] = i;
        while(l <= r && s[q[l]].dis - s[q[r]].dis > dmax) //滑动窗口 
        {
            sum -= s[q[l]].w;
            X.add(s[q[l]].x, -s[q[l]].w); 
            Y.add(s[q[l]].y, -s[q[l]].w); 
            l++;
        }
        ans = max(ans, sum - X.sum(s[i].x - 1) - Y.sum(s[i].y - 1)); //一定要区分小于和小于等于,小于记得要-1 
        X.add(s[i].x, s[i].w); Y.add(s[i].y, s[i].w);                //做完了之后再把当前点加进去 
    }                                                          
    
    printf("%d
", ans);
    
    return 0;
}

总结

(1)只要复杂度是对的,直接枚举答案,不一定要二分。

(2)有子问题结构想dp。不重复的思想

(3)三维偏序问题化为二维偏序问题,二维偏序用树状数组为维护




原文地址:https://www.cnblogs.com/sugewud/p/9863668.html