NOIP2017模拟赛(9) 总结

前言:这次真的送命了,第一题脑抽了,第二题最小割建图又错了,第三题树形背包没调试出来,还把程序注释了。题目简单,5个人AK了,我该怎么办?这要到了真正比赛时我又会怎样?


a 星星

题目描述

天空中有N (1 ≤ N ≤ 400)颗星,每颗星有一个唯一的坐标(x, y), (1 ≤ x, y ≤ N)。请计算可以覆盖至少K(1 ≤ K ≤ N)颗星的矩形的最小面积。矩形的边必须平行于X轴或Y轴,长度必须为正整数。星如果在矩形的边上,也认为它是属于矩形内的。


输入格式

样例数据的第一行是两个整数N,K,代表星的数目和至少应该覆盖的数目。
接下来有N行,每行有两个整数x, y,代表星的坐标。


输出格式

输出一个整数,代表矩形的最小面积。


这里写图片描述


解题思路(尺取法+前缀和)

这题是签到题,但也是送命题。我考试时脑一抽,居然想到矩形框的左上角是星星(这明显是错的,假设一个菱形),这种题以前还做过的,我也不知道我怎么了。最近情绪低落,脑子也有毛病了。
考试时我想了个n^2logn的二分方法,甚至认为n^3的是暴力(数据太魔性),结果正解就是枚举两重,然后单调一重的n^3的解法。
直接讲正解吧,我都不好意思扯下去了。

枚举矩形的左边的边的y坐标和右边的边的y坐标,然后用尺取法枚举上下两条边的x坐标,检验框内有几个就用二维的前缀和就行了。注意:这里的每颗星星占1个面积,不是点而是格子。

尺取法的方法就是先枚举一个L,然后R必然是单调不减的,如果区间不满足R就加,然后L在外面一直加,如果R过头了就break。
这样 O(n3) 是可以过的。考试时我这个sb却在想n^2logn的错误方法。对拍还是万分重要的。
ps:LZZ说还有 O(n2log2n) 的算法。%%%


代码

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cmath>
#include <algorithm>
#include <cstring>
#define N 405
#define oo 0x7fffffff
using namespace std;

int n, K, sum[N][N], exist[N][N], Ans = oo;

int main(){

    freopen("a.in", "r", stdin);
    freopen("a.out", "w", stdout);

    scanf("%d%d", &n, &K);

    int x, y;
    for(int i = 1; i <= n; i++){
      scanf("%d%d", &x, &y);
      exist[x][y] = 1;
    }  

    for(int i = 1; i <= n; i++)
     for(int j = 1; j <= n; j++)  sum[i][j] = sum[i-1][j] + sum[i][j-1] - sum[i-1][j-1] + exist[i][j];

    for(int i = 1; i <= n; i++)
     for(int j = i; j <= n; j++){
       int R = 1;
       for(int L = 1; L <= n; L++){
         while(R <= n && sum[R][j] - sum[R][i-1] - sum[L-1][j] + sum[L-1][i-1] < K)  R ++;
         if(R > n)  break;
         Ans = min(Ans, (R-L+1)*(j-i+1));
       }
     }

    printf("%d\n", Ans);

    return 0;
}

b 战争

题目描述

X国和Y国是死对头,X国有N个炮台, Y国有M个基地和K个发电站,炮台、基地、发电站所在的位置的坐标都是整数。Y国的每个发电站能对距离其L以内(包括L)的基地供电。X国的每个炮台都可以发射无限次,每次发射可以击毁一个基地或者一个发电站,消耗的能量为两者距离的平方,这里的距离是欧几里德距离。X国决定要摧毁Y国的所有基地,我们说Y国的某个基地被摧毁的条件是:基地本身被直接摧毁或者对其供电的所有发电站被击。
问X国摧毁Y国所有基地需要消耗的总能量最小是多少。
提示:点(X1,Y1)和点(X2,Y2)的欧几里德距离是:
dis = sqrt( (X2-X1)* (X2-X1) + (Y2-Y1)* (Y2-Y1)).


输入格式

第一行:四个整数:N、M、K、L。1 <= N、M、K <= 50. 1<=L<=2000.
第二行:N个整数,第i个整数表示第i个炮台的横坐标。
第三行:N个整数,第i个整数表示第i个炮台的纵坐标。
第四行:M个整数,第i个整数表示第i个基地的横坐标。
第五行:M个整数,第i个整数表示第i个基地的纵坐标。
第六行:K个整数,第i个整数表示第i个发电站的横坐标。
第七行:K个整数,第i个整数表示第i个发电站的纵坐标。
所有的坐标的范围:[-500,500]。所有的炮台、基地、发电站不会重复。
数据保证每个基地至少有一个发电站为其供电。


输出格式

问X国摧毁Y国所有基地需要消耗的总能量最小是多少。


这里写图片描述


解题思路(最小割)

这是一道最小割的果题。然而直接贪心(割掉s连出去的边和割掉连到t的边取min)能拿98分,ykm直接贪心100分,这数据,真是感人。

我在考场上很快想出了最小割,然后Dinic没写错,却犯了很严重的错误:构图错了。这智商,真是感人。

本来每个发电站直接被离之最近的炮台击毁就行了,就是s向每个发电站连边,发电站与它供电的基地连边,在基地向t连边。中间容量为oo,两边分别为摧毁其建筑的最小能量花费。就是说有一定的贪心
然而这个贪心是不能少的,因为每个建筑被摧毁一次就可以一条割掉s-t路径了。所以这样割和花费才是一一对应的。

然而naive的我,将炮台放进了图中,徒增麻烦,还错了。因为如果炮台与基地(或发电站)每个都连边的话,割掉一条路径(使一座建筑摧毁)就需要割掉炮台与之相连的所有边,相当于每个炮台都打它一遍,这破坏了这种一一对应关系,唉,我的最小割都白学了。
然而样例中只有一个炮台,我还能说什么呢,论检查代码的重要性,早知道就不去死磕第三题了QAQ。。


代码

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <algorithm>
#include <cmath>
#define N 60
#define tu 100000
#define tutu 1000000
#define oo 1e9

using namespace std;

int n, m, K, L;

struct Data{
    int x, y;
}at[N], bs[N], ele[N];

int head_p[tu], cur = -1, s, t, level[tu], iter[tu], q[tu];
struct Adj{int next, obj, cap;} Edg[tutu];

void Insert(int a, int b, int c){
    cur ++;
    Edg[cur].next = head_p[a];
    Edg[cur].obj = b;
    Edg[cur].cap = c;
    head_p[a] = cur;
}

int Dinic(int now, int f){
    if(now == t || !f)  return f;
    int ret = 0;
    for(int &i = iter[now]; ~ i; i = Edg[i].next){
      int v = Edg[i].obj, c = Edg[i].cap;
      if(level[v] > level[now] && c){
        int d = Dinic(v, min(f, c));
        f -= d;
        ret += d;
        Edg[i].cap -= d;
        Edg[i^1].cap += d;
        if(!f)  break;
      }
    }
    return ret;
}

bool bfs(){
    for(int i = s; i <= t; i++)  level[i] = -1;
    level[s] = 0;
    int head, tail;
    q[head = tail = 0] = s;
    while(head <= tail){
      int now = q[head++];
      for(int i = head_p[now]; ~ i; i = Edg[i].next){
        int v = Edg[i].obj, c = Edg[i].cap;
        if(level[v] == -1 && c){
          q[++tail] = v;
          level[v] = level[now] + 1;
        }
      }
    }
    return level[t] != -1;
}

int Min_cut(){
    int flow = 0;
    while(bfs()){  
      for(int i = s; i <= t; i++)  iter[i] = head_p[i];
      flow += Dinic(s, oo);
    }
    return flow;
}

int Dis(Data A, Data B){
    return (A.x - B.x) * (A.x - B.x) + (A.y - B.y) * (A.y - B.y);
}

void Build(){
    s = 1;  t = s+K+m+1;
    for(int i = s; i <= t; i++)  head_p[i] = -1;

    for(int i = 1; i <= K; i++){
      int Min = oo;
      for(int j = 1; j <= n; j++)  Min = min(Min, Dis(ele[i], at[j]));
      Insert(s, s+i, Min);
      Insert(s+i, s, 0);
    }


    for(int i = 1; i <= m; i++){
      int Min = oo;
      for(int j = 1; j <= n; j++)  Min = min(Min, Dis(bs[i], at[j]));
      Insert(s+K+i, t, Min);
      Insert(t, s+K+i, 0);
    }


    for(int i = 1; i <= K; i++)
     for(int j = 1; j <= m; j++)
       if(Dis(ele[i], bs[j]) <= L * L){
         Insert(s+i, s+K+j, oo);
         Insert(s+K+j, s+i, 0);
       }
}

int main(){

    freopen("b.in", "r", stdin);
    freopen("b.out", "w", stdout);

    scanf("%d%d%d%d", &n, &m, &K, &L);

    for(int i = 1; i <= n; i++)  scanf("%d", &at[i].x);
    for(int i = 1; i <= n; i++)  scanf("%d", &at[i].y);
    for(int i = 1; i <= m; i++)  scanf("%d", &bs[i].x);
    for(int i = 1; i <= m; i++)  scanf("%d", &bs[i].y);
    for(int i = 1; i <= K; i++)  scanf("%d", &ele[i].x);
    for(int i = 1; i <= K; i++)  scanf("%d", &ele[i].y);

    Build();

    printf("%d\n", Min_cut());

    return 0;
}

c 染色树

题目描述

一棵共含有X个结点的树,结点编号1至X,根结点编号是1。有Y种不同的颜色,颜色编号从1至Y。
现在给每个结点都染上一种颜色,整颗树染色后满足:
1、对于编号是i的颜色,整颗树当中,至少有一个结点被染成了颜色i。
2、根结点必须被染成1号颜色,而且整颗树当中,恰好要有Z个结点被染成1号颜色。

染色过程结束后,现在要计算染色的总代价,总代价等于每一条边的代价之和,那么怎么计算一条边的代价呢?
不妨设结点a与结点b之间有一条边,该边的权重是W。
1、如果结点a和结点b最终被染成不同的颜色,那么a与b之间的那条边的代价等于0。
2、如果结点a和结点b最终被染成相同的颜色,那么a与b之间的那条边的代价等于W。

现在的问题是:在满足上述要求的前提下,染色树最小的总代价是多少?如果无法完成染色的任务,输出-1。


输入格式

第一行,三个整数:X、 Y 、Z。 1 <= X <= 300, 2 <= Y <= X, 1 <= Z <= X。
接下来有N-1行,第i行有三个整数:a,b,W。表示结点a与结点b之间有一条边,权重是W。 1 <= a, b <= X, 0 <= W <= 100000。


输出格式

一个整数。


输入样例

8 2 4
1 2 20
1 3 4
1 4 13
2 5 10
2 6 12
3 7 15
3 8 5


输出样例

4

【样例解释】
把结点1,3,5,6染成1号颜色,剩下的结点染成2号颜色。


解题思路(树形分组背包dp)

这是一道很典型的树形分组背包dp。
一开始我没想到这个背包模型,推了个错误的树形dp,后来改成了分组背包。
写完代码后,调了1h+,样例死活出不来。没有单步跟踪,调试很烦。结果我的背包犯了一些细节错误,最后实在改不出来,我竟然将其直接注释掉了,心想:反正我都知道怎么做了。结果第一、二题完全不在意料之中,后悔莫及。

首先记 f[root][j] 为第 root 个点染1,子树中有 j 个1的答案,g[root][j]与之对应就是不染。
然后一开始清为无穷大。
f[root][1]=g[root][0]=0;
然后就做树形背包,按照模型,枚举 j 要倒序,因为不能将本轮的答案贡献出来,否则就不是分组背包了。然后枚举儿子v的子树有 k 个染了1。为了一定要取,所以一开始先将k=0丢进背包里。(不然一开始初值为0,里面取min时就取不到了,也可以开一个temp=oo,最后更新答案)。注意转移时不是+=而是=,因为背包已经算到了以前的那些状态。
到这里,还有问题,就是当 Y=2 时会有问题,因为我们只记两维因为转移(算不算边权)只跟是否取1有关,就是说如果取其他的话,就不算权值( root v 都不取1,那就随便去两个不同的非1数)。如果Y=2的话,这个就做不到了,所以要特判(+ w )。这是我考场上没想到的,样例就调不出来。
综上,方程如下

f[root][j]=min(f[root][j],min(f[root][jk]+f[v][k]+w,f[root][jk]+g[v][k]));

g[root][j]=min(g[root][j],min(g[root][jk]+f[v][k],g[root][jk]+g[v][k]+((m==2)?w:0)));

一开始将 k=0 丢进去,枚举 k 从1开始就行(j还要从0开始)。
输出-1的话,就是X < Y + Z - 1的情况。
时间复杂度 O(n3)
ps:三目表达式多加括号,写在加号后面不加就错。


代码

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <algorithm>
#include <cmath>
#define N 333
#define oo 1e9
using namespace std;

int n, m, K;
int cur = -1, head_p[N];
struct Tadj{int next, obj, w;} Edg[N<<1];

void Insert(int a, int b, int c){
    cur ++;
    Edg[cur].next = head_p[a];
    Edg[cur].obj = b;
    Edg[cur].w = c;
    head_p[a] = cur;
}

int f[N][N], g[N][N];

void dfs(int root, int fa){

    f[root][1] = g[root][0] = 0;
    for(int i = head_p[root]; ~ i; i = Edg[i].next){
      int v = Edg[i].obj, w = Edg[i].w;
      if(v == fa)  continue;
      dfs(v, root);
      for(int j = K; j >= 0; j--){
        f[root][j] += g[v][0];
        g[root][j] += g[v][0]+((m==2)?w:0);
        for(int k = 1; k <= j; k++){
          f[root][j] = min(f[root][j], min(f[root][j-k]+f[v][k]+w, f[root][j-k]+g[v][k]));
          g[root][j] = min(g[root][j], min(g[root][j-k]+f[v][k], g[root][j-k]+g[v][k]+((m==2)?w:0)));
        }
      }
    }
}
int main(){

    freopen("c.in", "r", stdin);
    freopen("c.out", "w", stdout);

    scanf("%d%d%d", &n, &m, &K);

    if(n < m + K - 1){  
      printf("-1\n");
      return 0;
    }

    cur = -1;
    for(int i = 1; i <= n; i++)  head_p[i] = -1;

    int a, b, c;
    for(int i = 1; i < n; i++){
      scanf("%d%d%d", &a, &b, &c);
      Insert(a, b, c);
      Insert(b, a, c);
    }

    for(int i = 1; i <= n; i++)
     for(int k = 0; k <= K; k++)  f[i][k] = g[i][k] = oo;

    dfs(1, 0);
    printf("%d\n", f[1][K]);
    return 0;
}

总结

这次题目十分简单,但我反而爆炸了。这足以说明我的实力实在不如于人,我要调整好心态,调整状况,不能被困难与失败所打倒。
以后写程序一定要注意细节啊,满分跟零分只是一点点的差别。


这里写图片描述

天高云阔 风语如歌

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值