Опять зима-зима-зима-зи|

JosephusWang

园龄:2年7个月粉丝:3关注:4

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;
}

本文作者:JosephusWang

本文链接:https://www.cnblogs.com/JosephusWang/p/17857384.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   JosephusWang  阅读(30)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起