NOIP 历年真题

2016

愤怒的小鸟

假设我们要去打坐标为 \((x_1,y_1)\)\((x_2,y_2)\) 的两只小猪,那么使用待定系数法即可求得抛物线。具体如下:

\[ax_1^2+bx_1=y_1\\ ax_2^2+bx_2=y_2 \]

变形得:

\[ax_1+b=y_1/x_1\\ ax_2+b=y_2/x_2 \]

两式相减得:

\[a(x_1-x_2)=y_1/x_1-y_2/x_2 \]

因此,

\[a=\frac{y_1/x_1-y_2/x_2}{x_1-x_2} \]

代入即可求得 \(b=y_1/x_1-ax_1\),然后状压表示出这条抛物线能够打掉的所有小猪。

这是一个经典的重复覆盖问题。此处使用状压 DP 解决,为了降低复杂度,每次选择一条抛物线,使得其一定覆盖了一只没有被覆盖的小猪。

另外的一些细节详见代码。

// Title:  愤怒的小鸟
// Source: NOIP2016提高组
// Author: WZR
#include <bits/stdc++.h>
#define rep(i, s, t) for(int i=s; i<t; ++i)
#define F first
#define S second
#define pii pair<int, int>
#define ll long long
#define debug(x) cout<<#x<<":"<<x<<endl;
const int N=20;
using namespace std;

int n, type;
double x[N], y[N], eps=1e-10;
int line[N][N], f[1<<N];

int cmp(double x, double y)
{
    if(fabs(x-y)<eps) return 0;
    if(x<y) return -1;
    return 1;
}

void Solve()
{
    scanf("%d%d", &n, &type);
    rep(i, 0, n) scanf("%lf%lf", x+i, y+i);
    memset(line, 0, sizeof line);
    rep(i, 0, n) rep(j, 0, n)
    {
        line[i][i]=1<<i; // 一定可以一鸟打一猪
        double x1=x[i], y1=y[i];
        double x2=x[j], y2=y[j];
        if(cmp(x1, x2)==0) continue; // 分母不能为0
        double a=(y1/x1-y2/x2)/(x1-x2);
        if(cmp(a, 0)>=0) continue; // 抛物线必须开口朝下
        double b=y1/x1-a*x1;
        int st=0;
        rep(k, 0, n)
        {
            double x1=x[k], y1=y[k];
            if(cmp(a*x1*x1+b*x1, y1)==0) // 在抛物线上
                st|=1<<k;
        }
        line[i][j]=st;
    }
    memset(f, 0x3f, sizeof f);
    f[0]=0;
    rep(s, 0, (1<<n)-1)
    {
        int k=0;
        rep(i, 0, n) if(!(s>>i&1))
            k=i; // k目前没有被覆盖
        rep(i, 0, n)
        {
            int t=s|line[k][i]; // line[k][i]一定覆盖了k
            f[t]=min(f[t], f[s]+1);
        }
    }
    printf("%d\n", f[(1<<n)-1]);
}

int main()
{
    int T; scanf("%d", &T);
    while(T--) Solve();

    return 0;
}

2019

划分

数据范围较小时,可以考虑 dp。设 \(f(i,j)\) 表示当前段末尾为 \(i\),上一段末尾为 \(j\) 的最小代价。转移为:

\[f(i,j)= \min _{s_i-s_j \ge s_j-s_k}f(j,k)+(s_i-s_j)^2 \]

时间复杂度 \(O(n^3)\)

不难想到一个性质:要使得 \(f(i,j)\) 最小,上一段末尾 \(j\) 要尽可能靠后。这样就能保证 \((s_i-s_j)^2\) 每次都比较小。

有了这个贪心结论,重新定义 dp 状态,省去第二维。假如现在有决策点 \(j\),要求划分合法,则需要 \(s_i-s_j\ge pre(j)\)\(pre(j)\) 表示上一段末尾为 \(j\) 的总和。移项,\(s_j+pre(j) \le s_i\)

不难发现,\(s_j+pre(j)\) 满足单调性,因此可以二分 \(j\),或者直接使用单调队列。

注意:本题内存紧张,需要尽可能地省去无用的数组,比如说,\(f(n)\) 其实可以倒推得到。同时注意 __int128

// Title:  划分
// Source: CSP-S2019
// Author: Jerrywang
#include <bits/stdc++.h>
#define rep(i, s, t) for(int i=s; i<=t; ++i)
#define F first
#define S second
#define pii pair<int, int>
#define ll long long
#define LL __int128
#define debug(x) cout<<#x<<":"<<x<<endl;
const int N=40000001;
using namespace std;

inline void write(LL x)
{
    if(x<10)
    {
        putchar(x+48); return;
    }
    write(x/10), putchar(x%10+48);
}

int n, type, b[N], q[N], pre[N], hh, tt;
ll a[N];
inline ll d(int i) {return a[i]-a[pre[i]];}

int main()
{
    scanf("%d%d", &n, &type);
    if(type==0)
    {
        rep(i, 1, n) scanf("%lld", a+i), a[i]+=a[i-1];
    }
    else
    {
        int x, y, z, m; scanf("%d%d%d", &x, &y, &z);
        scanf("%d%d%d", b+1, b+2, &m);
        rep(i, 3, n)
            b[i]=((ll)x*b[i-1]+(ll)y*b[i-2]+z)%(1<<30);
        int p1=0;
        while(m--)
        {
            int p, l, r; scanf("%d%d%d", &p, &l, &r);
            rep(i, p1+1, p) a[i]=b[i]%(r-l+1)+l, a[i]+=a[i-1];
            p1=p;
        }
    }
    rep(i, 1, n)
    {
        int j;
        // s[i]-s[q[hh]]>=d[q[hh]]
        while(hh<=tt && a[q[hh]]+d(q[hh])<=a[i])
            j=q[hh++];
        pre[i]=j;
        while(hh<=tt && a[q[tt]]+d(q[tt])>=a[i]+d(i)) tt--;
        q[++tt]=i;
    }
    LL res=0; int i=n;
    while(i) res+=(LL)d(i)*d(i), i=pre[i];
    write(res);

    return 0;
}

2020

贪吃蛇

假设现在实力最强的蛇叫做 A,次强的为 B,最弱的为 C,次弱的为 D。

  1. 如果 A 吃完 C 后,仍然比 B 强,那么,A 一定会吃掉 C。这很显然:A 的老大地位不会动摇,不吃白不吃。

  2. 如果 A 吃完 C 后,比 B 弱,但比 D 强,那么,A 一定会吃掉 C。这需要一点简单的证明。

    A 吃掉 C 后,B 也就成为了最强的蛇。根据结论 1,B 一定会吃掉 D。

    因为 \(A>B,C<D\),所以 \(A-C>B-D\)

    这样,现在的 B 比现在的 A 弱,B 会想方设法不死,A 也就死不了,所以可以放心吃 C。

  3. 如果 A 吃完 C 后,比 D 弱,成为了当前最弱的,A 有可能也会吃掉 C。这是一个博弈问题。

    博弈问题的根本思路就是:我预判了你的预判

    是这么考虑的:如果 A 选择吃,B 吃 A 不会陷入情况 3,那么,A 就不能吃。

    反之:如果 A 选择吃,B 吃 A 还会陷入情况 3,出现了一只 E,E 吃 B 不会陷入情况 3(E 可以放心吃 B),那么可以倒推得到 B 不敢吃 A,A 就可以吃。

    这是一种不断递归、取反状态的想法,详见代码 chk 函数。

综上,本题的大体思路是:先不考虑情况 3,直到最强蛇操作过后会变成最弱蛇,再考虑情况 3。不难发现,情况 3 最多发生一次,因为假如当前蛇选择吃,下一只蛇必定不吃。使用 set 模拟,在洛谷上可过。

// Title:  贪吃蛇
// Source: CSP-S2020
// Author: Jerrywang
#include <bits/stdc++.h>
#define rep(i, s, t) for(int i=s; i<=t; ++i)
#define F first
#define S second
#define pii pair<int, int>
#define ll long long
#define debug(x) cout<<#x<<":"<<x<<endl;
const int N=1000010;
using namespace std;

inline int read()
{
    int x=0, f=1;
    char c=getchar();
    while(!isdigit(c)) {if(c=='-') f=-1; c=getchar();}
    while(isdigit(c)) {x=(x<<3)+(x<<1)+c-'0'; c=getchar();}
    return x*f;
}
inline void write(ll x)
{
    if(x<10)
    {
        putchar(x+48); return;
    }
    write(x/10), putchar(x%10+48);
}

int T, n;
struct snake
{
    int id, x;
    bool operator <(snake t) const
    {
        if(x!=t.x) return x<t.x;
        return id<t.id;
    }
} a[N];
set<snake> S;
bool chk()
{
    if(S.size()==2) return 1;
    auto i1=S.begin(), i2=next(i1), i3=--S.end();
    snake t=*i3; t.x-=i1->x;
    if(!(t<*i2)) return 1;
    S.erase(i1), S.erase(i3), S.insert(t);
    return !chk();
}

int solve()
{
    int res=n; S.clear();
    rep(i, 1, n) S.insert(a[i]);
    while(1)
    {
        auto i1=S.begin(), i2=next(i1), i3=--S.end();
        snake t=*i3; t.x-=i1->x;
        if(t<*i2) break;
        S.erase(i1), S.erase(i3), S.insert(t); res--;
    }
    if(chk()) res--;
    return res;
}

int main()
{
    T=read();
    rep(tt, 1, T)
    {
        if(tt==1)
        {
            n=read();
            rep(i, 1, n) a[i].x=read(), a[i].id=i;
        }
        else
        {
            int k=read();
            while(k--)
            {
                int i=read(), x=read();
                a[i].x=x;
            }
        }
        write(solve()); puts("");
    }

    return 0;
}

如果想加速本做法,可以考虑单调队列。维护两个单调队列 q1、q2,分别从大到小存储没吃过、吃过的蛇。细节很多。

// Title:  贪吃蛇
// Source: CSP-S2020
// Author: Jerrywang
#include <bits/stdc++.h>
#define rep(i, s, t) for(int i=s; i<=t; ++i)
#define F first
#define S second
#define pii pair<int, int>
#define ll long long
#define debug(x) cout<<#x<<":"<<x<<endl;
const int N=1000010, inf=2e9;
using namespace std;

inline int read()
{
    int x=0, f=1;
    char c=getchar();
    while(!isdigit(c)) {if(c=='-') f=-1; c=getchar();}
    while(isdigit(c)) {x=(x<<3)+(x<<1)+c-'0'; c=getchar();}
    return x*f;
}
inline void write(int x)
{
    if(x<10)
    {
        putchar(x+48); return;
    }
    write(x/10), putchar(x%10+48);
}

int T, n;
struct snake
{
    int id, x;
    bool operator <(snake t)
    {
        if(x!=t.x) return x<t.x;
        return id<t.id;
    }
    bool operator ==(snake t) {return x==t.x && id==t.id;}
} a[N];
snake q1[N], q2[N]; int h1, t1, h2, t2;

snake Max()
{
    snake res={0, 0};
    if(h1<=t1) res=q1[h1];
    if(h2<=t2 && res<q2[h2]) res=q2[h2];
    if(h1<=t1 && q1[h1]==res) h1++;
    else h2++;
    return res;
}
snake Min(bool del) // del:是否删除
{
    snake res={inf, inf};
    if(h1<=t1) res=q1[t1];
    if(h2<=t2 && q2[t2]<res) res=q2[t2];
    if(del)
    {
        if(h1<=t1 && q1[t1]==res) t1--;
        else t2--;
    }
    return res;
}

bool chk(int remain)
{
    if(remain==1) return 0;
    if(remain==2) return 1; // 最后两条,一定能吃
    auto a=Min(1), b=Max(); b.x-=a.x;
    if(!(b<Min(0))) return 1; // 吃了不是最小,一定能吃
    q2[++t2]=b;
    return !chk(remain-1); // 下一条蛇能吃我,我就不能吃;反之亦然
}

int solve()
{
    int remain=n; h1=h2=1, t1=t2=0;
    rep(i, 1, n) q1[++t1]=a[n-i+1]; // 队头永远放最大的
    while(1)
    {
        auto a=Min(1), b=Max(); b.x-=a.x;
        q2[++t2]=b; remain--;
        if(b==Min(0)) break;
    }
    if(chk(remain)) remain++;
    return remain;
}

int main()
{
    T=read();
    rep(tt, 1, T)
    {
        if(tt==1)
        {
            n=read();
            rep(i, 1, n) a[i].x=read(), a[i].id=i;
        }
        else
        {
            int k=read();
            while(k--)
            {
                int i=read(), x=read();
                a[i].x=x;
            }
        }
        write(solve()); puts("");
    }

    return 0;
}

2022

星战

蛮神奇的一道题:题面很神奇,思路很神奇,代码很神奇,数据也很神奇。

怎么有赏析语文课文的感觉。

题面很神奇在于:如果已经“实现连续穿梭”,就必然“实现反击”!也就是说,“实现反击”这个条件是废的。每个点只有一条出边,必然构成一个内向基环树,那么每个点必然会绕进一个环内。

思路很神奇在于:你不会想到本题与哈希有关。

不方便直接考虑每个点是否只有一条出边,但可以换个角度思考:这等价于每条边的起点不重不漏地构成 \(1\sim n\)。将边 \(u\rightarrow v\) 的权值定义为 \(h[u]\)。维护所有边的权值和,如果其等于 \(\sum _{i=1}^n h[i]\),就满足要求,可以反攻。

对于操作 2,删除一个点上的所有入边,考虑维护每个点上所有入边的权值和,即可快速完成。

代码很神奇在于:代码是真的短!下面奉上代码:

// Title:  星战
// Source: CSP-S 2022
// Author: Jerrywang
#include <bits/stdc++.h>
#define ll unsigned long long
#define rep(i, s, t) for(int i=s; i<=t; ++i)
#define debug(x) cerr<<#x<<":"<<x<<endl;
const int N=500010;
using namespace std;

mt19937 rnd(time(0));
int n, m; ll h[N], sum[N], ori[N], cur, tot;

int main()
{
#ifdef Jerrywang
	freopen("E:/OI/in.txt", "r", stdin);
#endif
	scanf("%d%d", &n, &m);
	rep(i, 1, n) h[i]=rnd(), tot+=h[i];
	rep(i, 1, m)
	{
		int u, v; scanf("%d%d", &u, &v);
		cur+=h[u];
		sum[v]+=h[u];
	}
	rep(i, 1, n) ori[i]=sum[i];
	int T; scanf("%d", &T);
	while(T--)
	{
		int o, u, v; scanf("%d%d", &o, &u);
		if(o==1)
		{
			scanf("%d", &v);
			cur-=h[u];
			sum[v]-=h[u];
		}
		else if(o==2)
		{
			cur-=sum[u];
			sum[u]=0;
		}
		else if(o==3)
		{
			scanf("%d", &v);
			cur+=h[u];
			sum[v]+=h[u];
		}
		else
		{
			cur-=sum[u];
			sum[u]=ori[u];
			cur+=sum[u];
		}
		puts(cur==tot?"YES":"NO");
	}
	
	return 0;
}
posted @ 2023-11-26 15:58  JosephusWang  阅读(42)  评论(0)    收藏  举报