NOIP 历年真题
2016
愤怒的小鸟
假设我们要去打坐标为 \((x_1,y_1)\) 和 \((x_2,y_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\) 的最小代价。转移为:
时间复杂度 \(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。
-
如果 A 吃完 C 后,仍然比 B 强,那么,A 一定会吃掉 C。这很显然:A 的老大地位不会动摇,不吃白不吃。
-
如果 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。
-
如果 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 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步