您的位置:首页 > 其它

[题解]NOIP2016提高组の题解集合 - by xyz32768

2017-11-04 18:11 288 查看
NOIP2016提高组的难度参差不齐……按照难度从小到大排序讲吧……

Top 6:D1T1 玩具谜题(toy) - 模拟

模拟题。根据小人的朝向和向左/右数来判断走向。注意边界。

#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
inline int read() {
int res = 0; bool bo = 0; char c;
while (((c = getchar()) < '0' || c > '9') && c != '-');
if (c == '-') bo = 1; else res = c - 48;
while ((c = getchar()) >= '0' && c <= '9')
res = (res << 3) + (res << 1) + (c - 48);
return bo ? ~res + 1 : res;
}
const int N = 1e5 + 5;
int n, m, v
; string s
;
int main() {
//freopen("toy.in", "r", stdin);
//freopen("toy.out", "w", stdout);
int i, now = 0; n = read(); m = read();
for (i = 1; i <= n; i++) v[i] = read(), cin >> s[i];
for (i = 1; i <= m; i++)
if (v[now + 1] ^ read()) now = (now + read()) % n;
else now = (now - read() + n) % n;
cout << s[now + 1] << endl;
return 0;
}


Top 5:D2T1 组合数问题(problem) - 组合数+前缀和

首先使用公式Cmn=Cmn−1+Cm−1n−1预处理2000以内的组合数取模k的值,判断一下每个值是否为0后再求一下前缀和,就能O(1)一组询问了。

代码:

#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
inline int read() {
int res = 0; bool bo = 0; char c;
while (((c = getchar()) < '0' || c > '9') && c != '-');
if (c == '-') bo = 1; else res = c - 48;
while ((c = getchar()) >= '0' && c <= '9')
res = (res << 3) + (res << 1) + (c - 48);
return bo ? ~res + 1 : res;
}
const int N = 2000;
int m, n, PYZ, c[N + 5][N + 5], col[N + 5][N + 5], cnt[N + 5][N + 5];
int main() {
//freopen("problem.in", "r", stdin);
//freopen("problem.out", "w", stdout);
int i, j, T; T = read(); PYZ = read();
for (i = 0; i <= N; i++) c[i][0] = 1;
for (i = 1; i <= N; i++) for (j = 1; j <= i; j++)
c[i][j] = (c[i - 1][j - 1] + c[i - 1][j]) % PYZ;
for (j = 1; j <= N; j++) for (i = 1; i <= (N - j + 1); i++)
col[j][i] = col[j][i - 1] + (c[i + j - 1][j] == 0);
for (i = 1; i <= N; i++) for (j = 1; j <= i; j++)
cnt[i][j] = cnt[i][j - 1] + col[j][i - j + 1];
while (T--) {
m = read(); n = read();
printf("%d\n", cnt[m][min(m, n)]);
}
return 0;
}


Top 4:D2T3 愤怒的小鸟(angrybirds) - 状压DP

首先知道,三个不在一条直线上的点可以确定一条抛物线。而(0,0)是已经固定的点,所以只要枚举两个点,就可以得到一条抛物线。

考虑状态压缩。先预处理出con[i][j],表示第i只猪和第j只猪与(0,0)组成的抛物线,储存该抛物线能消灭猪的集合(用2进制表示,一位为1表示不能消灭这个猪,为0则可以消灭这个猪)。

预处理较简单,注意两个猪(x1,y1)(x2,y2)组成的抛物线的解析式为:

y=x1y2−x2y1x1x2(x2−x1)x2+x22y1−x21y2x1x2(x2−x1)x

然后设f[S]为当前猪的状态为二进制S(为1则未消灭,为0则已消灭)。边界即f[0]=0。

转移有两种:

1、f[S]=min(f[S],f[S&con[i][j]]+1),i,j∈S,i≠j

2、f[S]=min(f[S],f[S−2i−1]+1),i∈S

这样就能O(2nn2)解决了。

优化:每一个猪是肯定要消灭的,所以可以在第一个转移中,用S集合中的第一个数来代替i,将复杂度优化到O(2nn)。

代码:

#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
inline int read() {
int res = 0; bool bo = 0; char c;
while (((c = getchar()) < '0' || c > '9') && c != '-');
if (c == '-') bo = 1; else res = c - 48;
while ((c = getchar()) >= '0' && c <= '9')
res = (res << 3) + (res << 1) + (c - 48);
return bo ? ~res + 1 : res;
}
const int N = 20; const double eps = 1e-9;
int n, f[1 << N], ef

;
double X
, Y
;
struct cyx {
double a, b;
cyx() {}
cyx(double _a, double _b) :
a(_a), b(_b) {}
};
cyx quad(double x1, double y1, double x2, double y2) {
if (abs(x1 * x2 * (x2 - x1)) < eps) return cyx(1e20, 1e20);
double a = (x1 * y2 - x2 * y1) / (x1 * x2 * (x2 - x1)),
b = (y1 * x2 * x2 - y2 * x1 * x1) / (x1 * x2 * (x2 - x1));
return a < -eps ? cyx(a, b) : cyx(1e20, 1e20);
}
int Dp(int state) {
if (f[state] != -1) return f[state];
if (!state) return 0;
int i, fir, res = n; for (i = 1; i <= n; i++)
if ((state >> i - 1) & 1)
{fir = i; break;}
for (i = 1; i <= n; i++)
if (((state >> i - 1) & 1)) {
if (i != fir && (state & ef[fir][i]) != state)
res = min(res, Dp(state & ef[fir][i]) + 1);
res = min(res, Dp(state ^ (1 << i - 1)) + 1);
}
return f[state] = res;
}
void work() {
int i, j, k; n = read(); read(); memset(f, -1, sizeof(f));
for (i = 1; i <= n; i++) scanf("%lf%lf", &X[i], &Y[i]);
for (i = 1; i <= n; i++) for (j = i + 1; j <= n; j++)
ef[i][j] = (1 << n) - 1;
for (i = 1; i <= n; i++) for (j = i + 1; j <= n; j++) {
cyx q = quad(X[i], Y[i], X[j], Y[j]);
if (q.a > 1e19) continue;
for (k = 1; k <= n; k++)
if (abs(q.a * X[k] * X[k] + q.b * X[k] - Y[k]) < eps)
ef[i][j] ^= 1 << k - 1;
}
printf("%d\n", Dp((1 << n) - 1));
}
int main() {
//freopen("angrybirds.in", "r", stdin);
//freopen("angrybirds.out", "w", stdout);
int T = read();
while (T--) work();
return 0;
}


Top 3:D2T2 蚯蚓(earthworm) - 队列

容易想到用堆维护蚯蚓长度,对于蚯蚓的增长,就用一个delta标记。

但是O(nlogn)的复杂度无法承受n≤7∗106的数据范围。

当q=0的时候,可以很容易得到每次切蚯蚓得到的两段是单调不增的。

但q≠0时也满足这个性质。

证明:

如果说这一次切掉的蚯蚓实际长度为x+delta,下一次切掉的蚯蚓实际长度为y+delta+q,由题意容易得到x>y。

那么这一次把蚯蚓切成的两段中,第一段的相对长度为p(x+delta)−delta−q=p∗x+p∗delta−delta−q。

下一次把蚯蚓切成的两段中,第一段相对长度为p(y+delta+q)−delta−2q=p∗y+p∗delta+p∗q−delta−2q

由0<p<1,q>=0得pq−q<0

又由于x>y,所以p(x−y)>0,即px−py>pq−q

也就是px+p∗delta−delta−q>py+p∗delta+p∗q−delta−2q。第二段同理。即每次切掉的两个蚯蚓长度是单调不增的。

根据这个特性,可以维护3个队列,分别为未切的蚯蚓,第一段和第二段。每一次操作就在3个队列的队首中选最大值出队,切完后加入到第二个队列和第三个队列的队尾,同时维护delta。输出答案后,将三个队列中剩余的元素归并即可。

代码:

#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
inline int read() {
int res = 0; bool bo = 0; char c;
while (((c = getchar()) < '0' || c > '9') && c != '-');
if (c == '-') bo = 1; else res = c - 48;
while ((c = getchar()) >= '0' && c <= '9')
res = (res << 3) + (res << 1) + (c - 48);
return bo ? ~res + 1 : res;
}
const int N = 1e5 + 5, M = 7e6 + 5, INF = 0x3f3f3f3f;
int n, m, q, u, v, t, a
, Q[4][M + N], H[4], T[4], add;
double p;
int pop() {
int i, id = 0, ans = -INF;
for (i = 1; i <= 3; i++)
if (H[i] < T[i] && Q[i][H[i] + 1] > ans)
id = i, ans = Q[id][H[id] + 1];
return Q[id][++H[id]];
}
int main() {
//freopen("earthworm.in", "r", stdin);
//freopen("earthworm.out", "w", stdout);
int i, cnt = 0; n = read(); m = read(); q = read();
u = read(); v = read(); t = read(); p = (1.0 * u) / (1.0 * v);
for (i = 1; i <= n; i++) a[i] = read();
sort(a + 1, a + n + 1); for (i = n; i >= 1; i--)
Q[1][++T[1]] = a[i];
for (i = 1; i <= m; i++) {
int x = pop() + add, y = (int) (1.0 * x * p), z = x - y;
add += q; y -= add; z -= add;
Q[2][++T[2]] = y; Q[3][++T[3]] = z;
if ((++cnt) == t) printf("%d ", x), cnt = 0;
}
printf("\n"); cnt = 0;
for (i = 1; i <= n + m; i++) {
int x = pop() + add;
if ((++cnt) == t) printf("%d ", x), cnt = 0;
}
printf("\n");
return 0;
}


Top 2:D1T3 换教室(classroom) - Floyd+期望DP

首先使用Floyd求出任意两点之间的最短路,设i到j的最短路为len[i][j]。

DP模型:设f[i][j][0/1]为到了第i个时间段,换了j次教室,第三维为0表示不换第i个教室,否则表示换第i个教室。

转移有4种:

1、第i−1和第i个教室都不换。即

f[i][j][0]=min(f[i][j][0],f[i−1][j][0]+len[ci−1][ci])。

2、换第i−1个教室,不换第i个教室。这时候将答案分成两部分,一部分是1到i−1,第二部分是i−1到i。

这时候第一部分的期望为f[i−1][j][1],第二部分的期望为len[di−1][ci]∗ki−1+len[ci−1][ci]∗(1−ki−1)。所以:

f[i][j][0]=min(f[i][j][0],f[i−1][j][1]+len[di−1][ci]∗ki−1+len[ci−1][ci]∗(1−ki−1)。

3、不换第i−1个教室,换第i个教室。这时候也是一样的思想,即

f[i][j][1]=min(f[i][j][1],f[i−1][j−1][0]+len[ci−1][di]∗ki+len[ci−1][ci]∗(1−ki))。

4、第i−1个教室和第i个教室都换。这时候是情况最多的,有4种。分别为:

(1)两个申请都通过。即len[di−1][di],发生的概率为ki−1∗ki。

(2)只通过第一个申请。即len[di−1][ci],发生的概率为ki−1∗(1−ki)。

(3)只通过第二个申请。即len[ci−1][di],发生的概率为(1−ki−1)∗ki。

(4)两个申请都不通过。即len[ci−1][ci],发生的概率为(1−ki−1)∗(1−ki)。

得出转移:

f[i][j][1]=min(f[i−1][j−1][1],len[di−1][di]∗ki−1∗ki

+len[di−1][ci]∗ki−1∗(1−ki)+len[ci−1][di]∗(1−ki−1)∗ki

+len[ci−1][ci]∗(1−ki−1)∗(1−ki))。

代码:

#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
inline int read() {
int res = 0; bool bo = 0; char c;
while (((c = getchar()) < '0' || c > '9') && c != '-');
if (c == '-') bo = 1; else res = c - 48;
while ((c = getchar()) >= '0' && c <= '9')
res = (res << 3) + (res << 1) + (c - 48);
return bo ? ~res + 1 : res;
}
void chkmin(double &a, double b) {
a = min(a, b);
}
const int N = 2005, M = 305, INF = 0x3f3f3f3f;
int n, m, v, e, c
, d
, ex[M][M];
double K
, f

[2];
void floyd() {
int i, j, k; for (k = 1; k <= v; k++)
for (i = 1; i <= v; i++) for (j = 1; j <= v; j++)
ex[i][j] = min(ex[i][j], ex[i][k] + ex[k][j]);
for (i = 1; i <= v; i++) ex[i][i] = 0;
}
double solve() {
floyd(); int i, j;
for (i = 1; i <= n; i++) for (j = 0; j <= m; j++)
f[i][j][0] = f[i][j][1] = 1e20;
f[1][0][0] = f[1][1][1] = 0;
for (i = 2; i <= n; i++) for (j = 0; j <= min(i, m); j++) {
chkmin(f[i][j][0], f[i - 1][j][0] +
ex[c[i - 1]][c[i]]);
if (j == 0) continue;
chkmin(f[i][j][0], f[i - 1][j][1] +
1.0 * ex[d[i - 1]][c[i]] * K[i - 1]
+ 1.0 * ex[c[i - 1]][c[i]] * (1.0 - K[i - 1]));
chkmin(f[i][j][1], f[i - 1][j - 1][0] +
1.0 * ex[c[i - 1]][d[i]] * K[i]
+ 1.0 * ex[c[i - 1]][c[i]] * (1.0 - K[i]));
chkmin(f[i][j][1], f[i - 1][j - 1][1] +
1.0 * ex[d[i - 1]][d[i]] * K[i - 1] * K[i] +
1.0 * ex[c[i - 1]][d[i]] * (1.0 - K[i - 1]) * K[i] +
1.0 * ex[d[i - 1]][c[i]] * K[i - 1] * (1.0 - K[i]) +
1.0 * ex[c[i - 1]][c[i]] * (1.0 - K[i - 1]) * (1.0 - K[i]));
}
double res = 1e20;
for (j = 0; j <= min(n, m); j++)
res = min(res, min(f
[j][0], f
[j][1]));
return res;
}
int main() {
//freopen("classroom.in", "r", stdin);
//freopen("classroom.out", "w", stdout);
int i, x, y, z; memset(ex, INF, sizeof(ex));
n = read(); m = read(); v = read(); e = read();
for (i = 1; i <= n; i++) c[i] = read();
for (i = 1; i <= n; i++) d[i] = read();
for (i = 1; i <= n; i++) scanf("%lf", &K[i]);
while (e--) {
x = read(); y = read(); z = read();
if (x != y) ex[x][y] = ex[y][x] = min(ex[x][y], z);
}
printf("%.2lf\n", solve());
return 0;
}


Top 1:D1T2 天天爱跑步(running) - dfs+统计

这题是2016中最难的。首先,还是把原图视为有根树,求出每个Si和Ti的lca即lcai。

可以发现,第u个观察员能观察到的玩家数目为:

在Si到lcai的路径上合法的i的个数+在lcai到Ti的路径上合法的i个数,如果lcai算了两遍则减1。

怎么求前两个内容呢?

先考虑第一个,设u的深度为dep[u],那么对于一个点v往上走,点v在恰好Wu的时间经过u的条件是dep[u]+Wu=dep[v]。但是可以发现,对于任意一个玩家i,Si一旦走到了lcai就不再往上走了。所以这时候的结果就是所有满足dep[u]+Wu=dep[Si]并且lcai不在u的子树内(不包括u),Si在u的子树内(包括u)的i个数。

第二个内容也是差不多的思想。对于一个玩家i,在部分路径lcai到Ti中恰好第Wu秒经过点u的条件是dep[Si]+dep[u]−2dep[lcai]=Wu,移项得dep[u]−Wu=2dep[lcai]−dep[Si]。同样,Si一旦走到了lcai就不再往上走。所以求的就是满足dep[u]−Wu=2dep[lcai]−dep[Si]并且lcai不在u的子树内(不包括u),Ti在u的子树内(包括u)的i个数。

考虑怎样实现。对于第一个内容,容易想到用一个计数器cnt[x]来维护到当前考虑过的所有Si中,满足dep[Si]=x的i的个数。但是现在只想统计当前子树内的Si,怎么办呢?可以发现,对于每一个节点,只需要去考虑深度为dep[u]+Wu的节点。所以,使用一次dfs来解决这个问题,搜到一个节点的时候u,先记下当前的cnt[dep[u]+Wu],然后再去搜u的子节点,求出cnt[dep[u]+Wu]相对于搜u的所有子节点之前的变化量(增量),就是u的子树内满足dep[u]+Wu=dep[Si]的i的个数。再考虑消除lcai的影响。因为如果lcai在u的子树内(不包括u),那么lcai一定也在u的父亲的子树内,所以对于这一部分的答案,直接从子节点传上来即可。但在子节点v的结果往父亲u上传时,如果v是某些lcai,那么传到u时就要消除因lca造成的影响,也就是说,对于所有dep[v]+Wv=dep[Si]并且lcai=v的i,在传到u时必须去掉。解决办法就是维护一个vector或邻接表,储存v作为哪些玩家路径的lca,这样就能方便地往上传了。

第二个内容的实现也是差不多,也是用dfs和增量维护子树内的信息,用vector或邻接表进行去重。但是注意细节:dep[u]−Wu有可能是负数,因此统计数组的下标要加上一个定值。

复杂度:如果用倍增求lca,那么复杂度为O(nlogn),如果用Tarjan离线求lca,那么复杂度为O(n)。

代码:

#include <cmath>
#include <cstdio>
#include <vector>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
inline int read() {
int res = 0; bool bo = 0; char c;
while (((c = getchar()) < '0' || c > '9') && c != '-');
if (c == '-') bo = 1; else res = c - 48;
while ((c = getchar()) >= '0' && c <= '9')
res = (res << 3) + (res << 1) + (c - 48);
return bo ? ~res + 1 : res;
}
const int N = 3e5 + 5, LogN = 22, M = 3e5;
int n, m, ecnt, nxt[N << 1], adj
, go[N << 1], tim
, S
, T
,
L
, dep
, fa
[LogN], up
, down
, res
, cnt[N << 1],
cnt_s
, cnt_t
, len
, md;
vector<int> lca_s
, lca_t
, s_t
;
void add_edge(int u, int v) {
nxt[++ecnt] = adj[u]; adj[u] = ecnt; go[ecnt] = v;
}
void dfs(int u, int fu) {
dep[u] = dep[fu] + 1; fa[u][0] = fu;
md = max(md, dep[u]);
int i; for (i = 1; i <= 20; i++)
fa[u][i] = fa[fa[u][i - 1]][i - 1];
for (int e = adj[u], v; e; e = nxt[e])
if ((v = go[e]) != fu) dfs(v, u);
}
int lca(int u, int v) {
if (dep[u] < dep[v]) swap(u, v);
int i; for (i = 20; i >= 0; i--) {
if (dep[fa[u][i]] >= dep[v]) u = fa[u][i];
if (u == v) return u;
}
for (i = 20; i >= 0; i--)
if (fa[u][i] != fa[v][i]) {
u = fa[u][i];
v = fa[v][i];
}
return fa[u][0];
}
void dfs1(int u, int fu) {
int i, cc = lca_s[u].size(), tot = cnt[up[u]];
for (int e = adj[u], v; e; e = nxt[e])
if ((v = go[e]) != fu)
dfs1(v, u);
cnt[dep[u]] += cnt_s[u];
if (up[u] <= md) res[u] += cnt[up[u]] - tot;
for (i = 0; i < cc; i++) cnt[dep[lca_s[u][i]]]--;
}
void dfs2(int u, int fu) {
int i, cc = lca_t[u].size(), ff = s_t[u].size(), tot = cnt[down[u] + M];
for (int e = adj[u], v; e; e = nxt[e])
if ((v = go[e]) != fu)
dfs2(v, u);
for (i = 0; i < ff; i++) cnt[s_t[u][i] + M]++;
res[u] += cnt[down[u] + M] - tot;
for (i = 0; i < cc; i++) cnt[lca_t[u][i] + M]--;
}
int main() {
//freopen("running.in", "r", stdin);
//freopen("running.out", "w", stdout);
int i, x, y; n = read(); m = read();
for (i = 1; i < n; i++) {
x = read(); y = read();
add_edge(x, y); add_edge(y, x);
}
dfs(1, 0); for (i = 1; i <= n; i++)
tim[i] = read(), up[i] = dep[i] + tim[i], down[i] = dep[i] - tim[i];
for (i = 1; i <= m; i++) S[i] = read(), T[i] = read(),
L[i] = lca(S[i], T[i]), cnt_s[S[i]]++, cnt_t[T[i]]++,
len[i] = dep[S[i]] + dep[T[i]] - (dep[L[i]] << 1),
lca_s[L[i]].push_back(S[i]), lca_t[L[i]].push_back(dep[T[i]] - len[i]),
s_t[T[i]].push_back(dep[T[i]] - len[i]);
dfs1(1, 0); memset(cnt, 0, sizeof(cnt)); dfs2(1, 0);
for (i = 1; i <= m; i++) if (dep[S[i]] - dep[L[i]] == tim[L[i]])
res[L[i]]--;
for (i = 1; i <= n; i++) printf("%d ", res[i]); printf("\n");
return 0;
}


总结

蒟蒻写完这份题解的之后的第7天,NOIP2017也要开始了。祝各位神犇大佬AK NOIP,AK 省选,AK NOI,AK CTSC,AK IOI,AK ACM系列各大比赛!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: