NOIP2016 复盘
D1T1 P1563 玩具谜题
我一开始学OI的时候以为可以直接跳链表,用膝盖想一想就知道会T。
所以做法就是判断顺时针转还是逆时针转,转完把超出范围的下标弄回来即可。
代码:
#include
using std::cin;
using std::cout;
using std::endl;
using std::string;
const int maxn = 100005;
int side[maxn];
string name[maxn];
int n, m;
int main() {
std::ios::sync_with_stdio(false);
cin >> n >> m;
for(int i = 1; i <= n; i++) cin >> side[i] >> name[i];
int now = 1;
while(m--) {
int a, s; cin >> a >> s;
if(side[now] ^ a) {
now = now + s;
now = 1 + (now - 1) % n;
} else {
now = now - s;
if(now < 1) {
now = 1 - now;
now = 1 + (now - 1) % n;
now = n - now + 1;
}
}
}
cout << name[now] << endl;
return 0;
}
D1T2 P1600 天天爱跑步
由于我太菜了,只能带来80pts的部分分,正解这辈子写不出来了吧555
直接给数据表:
1 2这两个点代表所有人的起点都等于终点。
当且仅当这些人所在的\(w=0\)时,能被观察到。
3 4这两个点代表\(w_j=0\),也就是观察者只在一开始观察。
直接遍历,找到不重复的所有的起点个数就完事了。
5这一个点只是数据小,没有其他特殊条件。
我们直接使用暴力做法,按题意模拟,找到所有路径的LCA,左右一起爬上去,计算路径上每一个点对应的时间,如果刚好被观察就计入答案。
其实前5个点都可以直接用这个方法解决。
6 7 8这三个点是退化成链的部分分。
我们认为从左到右分别是1到\(n\)。那么他们的路径就会有从左往右和从右往左两种。
我们对这些点来考虑贡献。显然对于一个点,如果左边\(w_i\)处有人要往右出发并且终点在这个点及其右边,就可以计入贡献。另一边也一样。
所以我们用一个vector记下从某个点出发的所有终止点,分上述两种思路去想,分别记录两种情况产生的贡献。
9 10 11 12这4个点是所有起点都为1的部分分。
我们直接可以以1为根来遍历整棵树,其中每一层的点要想观察到的话就必须要有\(w[i]=dep[i]-1\)。
我们进行树上差分,把路径上所有的点都标记起来,然后依次判断即可。
13 14 15 16这4个点是所有终点都为1的部分分。
显然对于一个观察点\(i\),当且仅当深度为\(w[i]+dep[i]\)的起点才会产生贡献。
参考了下题解的方法,用一个桶表示对应深度的起点的个数,先保存dfs前的桶里面的值,然后进行一次dfs,dfs后再查询值,前后之差就是这个深度对应的所有起点。
剩下的4个点我是真的不知道,但是考场上又不可能写出来,只能拿部分分维持生活的这个样子……
#include
#include
#include
const int maxn = 300005;
// 邻接表
struct Edges
{
int next, to;
} e[maxn << 1];
int head[maxn], tot;
// 需要的参数
int W[maxn], ans[maxn];
int S[maxn], T[maxn], LCA[maxn];// 单位为m
int isstart[maxn], isend[maxn];
// 树链剖分
int dep[maxn], size[maxn], fa[maxn], wson[maxn], top[maxn];
// 树上差分
int diff[maxn];
int bucket[maxn];
std::vector starter[maxn];
int n, m;
int read()
{
int ans = 0, s = 1;
char ch = getchar();
while(ch > '9' || ch < '0'){ if(ch == '-') s = -1; ch = getchar(); }
while(ch >= '0' && ch <= '9') ans = ans * 10 + ch - '0', ch = getchar();
return s * ans;
}
void link(int u, int v)
{
e[++tot] = (Edges){head[u], v};
head[u] = tot;
}
void dfs1(int u, int f)
{
dep[u] = dep[f] + 1; fa[u] = f; size[u] = 1;
for(int i = head[u]; i; i = e[i].next)
{
int v = e[i].to;
if(v == f) continue;
dfs1(v, u);
size[u] += size[v];
if(size[v] > size[wson[u]]) wson[u] = v;
}
}
void dfs2(int u, int topf)
{
top[u] = topf;
if(wson[u]) dfs2(wson[u], topf);
for(int i = head[u]; i; i = e[i].next)
{
int v = e[i].to;
if(v == fa[u] || v == wson[u]) continue;
dfs2(v, v);
}
}
int getLCA(int u, int v)
{
while(top[u] != top[v])
{
if(dep[top[u]] < dep[top[v]]) std::swap(u, v);
u = fa[top[u]];
}
if(dep[u] > dep[v]) std::swap(u, v);
return u;
}
void dfs3(int u, int f)// 树上前缀和
{
for(int i = head[u]; i; i = e[i].next)
{
int v = e[i].to;
if(v == f) continue;
dfs3(v, u);
diff[u] += diff[v];
}
}
void dfs4(int u, int f)
{
int history = bucket[dep[u] + W[u]];
bucket[dep[u]] += isstart[u];
for(int i = head[u]; i; i = e[i].next)
{
int v = e[i].to;
if(v == f) continue;
dfs4(v, u);
}
ans[u] = bucket[dep[u] + W[u]] - history;
}
// 部分分程序
void bf()// 暴力
{
for(int i = 1; i <= m; i++)
{
LCA[i] = getLCA(S[i], T[i]);
int tim = dep[S[i]] - dep[LCA[i]] + dep[T[i]] - dep[LCA[i]];
int temp = S[i], now = 0;
while(temp != LCA[i])
{
if(W[temp] == now) ans[temp]++;
temp = fa[temp]; now++;
}
temp = T[i], now = tim;
while(temp != LCA[i])
{
if(W[temp] == now) ans[temp]++;
temp = fa[temp]; now--;
}
if(W[LCA[i]] == dep[S[i]] - dep[LCA[i]]) ans[LCA[i]]++;
}
}
void skr()// 起点为1
{
for(int i = 1; i <= m; i++)
{
diff[T[i]]++;
}
dfs3(1, 0);
for(int i = 1; i <= n; i++)
{
if(W[i] == dep[i] - 1) ans[i] += diff[i];
}
}
void giao()// 终点为1
{
dfs4(1, 0);
}
void clearlove()// 退化成链
{
for(int i = 1; i <= m; i++)
{
starter[S[i]].push_back(T[i]);
}
for(int i = 1; i <= n; i++)
{
if(i - W[i] >= 1)
{
int siz = starter[i - W[i]].size();
for(int j = 0; j < siz; j++)
{
if(i <= starter[i - W[i]][j]) ans[i]++;
}
}
if(i + W[i] <= n)
{
int siz = starter[i + W[i]].size();
for(int j = 0; j < siz; j++)
{
if(starter[i + W[i]][j] <= i) ans[i]++;
}
}
}
}
int main()
{
bool start = true, end = true, chain = true;
n = read(), m = read();
for(int i = 1; i < n; i++)
{
int u = read(), v = read();
if(abs(u - v) != 1) chain = false;
link(u, v); link(v, u);
}
dfs1(1, 0); dfs2(1, 1);
for(int i = 1; i <= n; i++)
{
W[i] = read();
}
for(int i = 1; i <= m; i++)
{
S[i] = read(), T[i] = read();
isstart[S[i]]++;
isend[T[i]]++;
if(S[i] != 1) start = false;
if(T[i] != 1) end = false;
}
/*
if(chain) clearlove();
for(int i = 1; i <= n; i++) printf("%d ", ans[i]);
printf("\n");
return 0;
*/
if(n <= 1000) bf();
else if(start) skr();
else if(end) giao();
else if(chain) clearlove();
for(int i = 1; i <= n; i++) printf("%d ", ans[i]);
printf("\n");
return 0;
}
D1T3 P1850 换教室
期望dp第一次在NOIP考,难度很大!
最开始一定要知道什么是期望:期望就是答案乘以概率 的和。
我们先口胡看一下部分分:
特殊性质1:图上任意两点\(a_i,b_i,a_i\not=b_i\)间,存在一条耗费体力最少的路径只包含一条道路。
特殊性质2:对于所有的\(1\leq i\leq n,k_i=1\)。
2 5 8 12 16 20这5个点是\(m=0\)的部分分。
意思就是一次都换不了,做法就是跑下最短路然后模拟就是了。
1 3 6 9 13 17 21这6个点是\(m=1\)的部分分。
意思就是可以换一次,做法依然要跑最短路,然后枚举要换哪节课的教室,如果换了答案更优就换,因为答案可能换完更劣。用前缀和可以优化。
5 8 11 15 18 19这6个点是满足特殊性质2的部分分。
意思就是每次要换就能换成功,不需要考虑概率问题了,但是仍然有次数限制。
直接爆搜肯定不可行了,我们
终于来考虑dp。经过设想,我们可以设
dp[i][j][0/1]
来表示当前上了\(i\)节课,已经(在这里可以认为成功地)尝试换了\(j\)节课,0表示最后一次没试,1表示最后一次试了。更新dp状态的时候,按照这次换不换去更新即可。
5 6 8 9 12 14 18这7个点是满足特殊性质1的部分分。
不知道有什么用,可能就是在启示你记得求最短路。
用前三个部分分就已经可以过18个点,也就是拿72分了。
其实正解经过在特殊性质2的时候就已经给你思路了。
按照那种dp状态,我们必须明白里面的\(j\)一定代表尝试过的次数,不是成功的次数,而这次是否有尝试换可以通过看是0还是1来判断。
注意:如果是0,则一定在原班,如果是1,有对应的概率在原版和在新班。
接下来就是疯狂的分类讨论:
若这次在原教室,而上次可能试了,但是不知道成功不成功,不过我们知道有\(k[i-1]\)的概率成功,在新教室上课,而有\(1-k[i-1]\)的概率失败,在原班级上课。两种情况一起求期望再一起加入原先答案即可。
若这次在新教室,那就麻烦了那么这次和上次都不知道成功与否,不过也都知道对应的概率,那么我们同样分上次试了跟上次没试两种情况讨论。
- 上次没试。那么增加的期望答案就综合2个因素:这次成功了还是这次失败了。
- 上次试了。那么增加的期望答案就必须综合4个因素:两次都成功了,两次都失败了,这次成功上次失败,上次成功这次失败。4个答案按概率加权平均后计入答案就可以完成更新。
#include
#include
#include
const int maxn = 2005, maxv = 305;
const double INF = 999999999;
int c[maxn][2];
double p[maxn], dp[maxn][maxn][2];
int G[maxn][maxn];
int n, m, v, e;
int main()
{
scanf("%d%d%d%d", &n, &m, &v, &e);
for(int i = 1; i <= n; i++) scanf("%d", &c[i][0]);
for(int i = 1; i <= n; i++) scanf("%d", &c[i][1]);
for(int i = 1; i <= n; i++) scanf("%lf", &p[i]);
memset(G, 0x3f, sizeof G);
for(int i = 1; i <= v; i++) G[i][i] = 0;
while(e--)
{
int u, v, w; scanf("%d%d%d", &u, &v, &w);
G[u][v] = G[v][u] = std::min(G[u][v], w);
}
for(int k = 1; k <= v; k++)
for(int i = 1; i <= v; i++)
for(int j = 1; j <= v; j++)
G[i][j] = std::min(G[i][j], G[i][k] + G[k][j]);
for(int i = 1; i <= n; i++)
for(int j = 0; j <= m; j++)
dp[i][j][0] = dp[i][j][1] = INF;
dp[1][0][0] = dp[1][1][1] = dp[1][1][0] = 0;
for(int i = 2; i <= n; i++) dp[i][0][0] = dp[i - 1][0][0] + G[c[i - 1][0]][c[i][0]];
for(int i = 2; i <= n; i++)
{
int s0 = c[i - 1][0], s1 = c[i - 1][1];
int t0 = c[i][0], t1 = c[i][1];
for(int j = 1; j <= std::min(i, m); j++)
{
dp[i][j][0] = std::min(dp[i][j][0], std::min(dp[i - 1][j][0] + G[s0][t0], dp[i - 1][j][1] + p[i - 1] * G[s1][t0] + (1 - p[i - 1]) * G[s0][t0]));
dp[i][j][1] = std::min(dp[i][j][1], std::min(dp[i - 1][j - 1][0] + p[i] * G[s0][t1] + (1 - p[i]) * G[s0][t0], dp[i - 1][j - 1][1] + p[i - 1] * p[i] * G[s1][t1] + p[i - 1] * (1 - p[i]) * G[s1][t0] + (1 - p[i - 1]) * p[i] * G[s0][t1] + (1 - p[i - 1]) * (1 - p[i]) * G[s0][t0]));
}
}
double ans = INF;
for(int i = 0; i <= m; i++) ans = std::min(ans, std::min(dp[n][i][0], dp[n][i][1]));
printf("%.2lf\n", ans);
return 0;
}
D2T1 P2822 组合数问题
显然的送分题,但是拿满分并不容易。
\(n,m \leq 2000\)启示我们直接把整个组合数打表,然后再处理询问,打表方式按照杨辉三角去递推即可。
暴力去写会得到90pts,满足条件的加起来膜了之后等于0就满足。
为什么不能满分?因为询问太多了。
显然这是一个查询时与\(k\)无关的二维区间查询。
一维区间查询我们可以用前缀和优化,那这里我们就能用二维前缀和优化了!
递推公式是\(sum[i][j] = sum[i-1][j] + sum[i][j-1]-sum[i-1][j-1]\)。
最后就可以\(O(1)\)回答询问了。整个复杂度\(O(n^2)\)。
#include
using namespace std;
const int maxn = 2005;
int t, k, n, m;
int c[maxn][maxn], ans[maxn][maxn];
void init()
{
c[1][1] = 1;
for(int i = 1; i <= 2000; i++) c[i][0] = 1;
for(int i = 2; i <= 2000; i++)
{
for(int j = 1; j <= i; j++)
{
c[i][j] = (c[i - 1][j - 1] + c[i - 1][j]) % k;
}
}
for(int i = 1; i <= 2000; i++)
{
for(int j = 1; j <= 2000; j++)
{
ans[i][j] = ans[i - 1][j] + ans[i][j - 1] - ans[i - 1][j - 1];
if(c[i][j] == 0 && j <= i) ans[i][j]++;
}
}
}
int main()
{
scanf("%d%d", &t, &k);
init();
while(t--)
{
scanf("%d%d", &n, &m);
printf("%d\n", ans[n][m]);
}
return 0;
}
D2T2 P2827 蚯蚓
显然我们可以用优先队列找出最长的,把它删了再加切完的两条新的加进去。询问也就暴力解决即可。
上面的无脑做法其实也挺优秀,只要\(m\)不要太大就行,最终得了85pts。
正解不那么显然但非常神奇:维护三个队列,一个是原蚯蚓,另一个是切后的长蚯蚓,最后一个是切完后的短蚯蚓。三个队列都是单调递减的。
所以每次模拟的时候就直接在三个队列队头拿到最大的,切完放到第二个和第三个队列中。
代码:
#include
#include
#include
#include
#define ll long long
const int maxn = 100005;
const ll INF = 0x3f3f3f3f3f;
ll a[maxn];
std::queue qq[3];
ll delta = 0;
ll n, m, q, u, v, t;
double p;
bool cmp(ll a, ll b)
{
return a > b;
}
ll read()
{
ll ans = 0, s = 1;
char ch = getchar();
while(ch > '9' || ch < '0'){ if(ch == '-') s = -1; ch = getchar(); }
while(ch >= '0' && ch <= '9') ans = (ans << 3) + (ans << 1) + ch - '0', ch = getchar();
return s * ans;
}
int main()
{
n = read(), m = read(), q = read(), u = read(), v = read(), t = read();
p = (double)(u) / (double)(v);
for(int i = 1; i <= n; i++) a[i] = read();
std::sort(a + 1, a + n + 1, cmp);
for(int i = 1; i <= n; i++) qq[0].push(a[i]);
bool first = true;
for(int i = 1; i <= m; i++)
{
ll maxv = -INF, where = -1;
for(int j = 0; j < 3; j++)
{
if(!qq[j].empty() && qq[j].front() > maxv) where = j, maxv = qq[j].front();
}
ll x = qq[where].front() + delta; qq[where].pop();
if(i % t == 0)
{
if(first) printf("%lld", x), first = false;
else printf(" %lld", x);
}
ll one = floor(p * x);
ll other = x - one;
//if(one > other) std::swap(one, other);
delta += q;
qq[1].push(one - delta);
qq[2].push(other - delta);
}
printf("\n");
first = true;
for(int i = 1; i <= n + m; i++)
{
ll maxv = -INF, where = -1;
for(int j = 0; j < 3; j++)
{
if(!qq[j].empty() && qq[j].front() > maxv) where = j, maxv = qq[j].front();
}
ll x = qq[where].front() + delta; qq[where].pop();
if(i % t == 0)
{
if(first) printf("%lld", x), first = false;
else printf(" %lld", x);
}
}
printf("\n");
return 0;
}
D2T3 P2831 愤怒的小鸟
我去题解里面学习到爆搜的做法。状压做法似乎有点乱于是没有学。
首先是一个数学结论:我们可以\(O(1)\)计算出经过两只猪之间的抛物线方程(如果存在的话)。
这是我的式子:
要求b的话直接代一个进去就行了。
小猪的最终结局分成两种:一种被经过多头猪的抛物线打下来,一种被专属于自己的抛物线打下来。
而我们在爆搜的时候,对前面的猪,也分两种情况:一种被当前已确定的抛物线打下来,一种等待被打下来。
先考虑这只猪能否被已经被已有的抛物线打下来,有就直接考虑下一只。
如果不能的话,再考虑能否与前面还没打下来的猪配对。
但是并不是当前配对了就一定是最优的,反例太多了。
于是这只猪当然要考虑暂时落单,被后面的猪配对。
爆搜到最后可能会有一些猪依然没打下来,这就没有办法了,只能一只猪一条抛物线。
大体思路就是这样。
代码:
#include
const int maxn = 19;
const double eps = 1e-6;
double x[maxn], y[maxn];
double A[maxn], B[maxn];
int n, m, ans;
int lonely[maxn];
bool equal(double a, double b) {
return fabs(a - b) < eps;
}
int cala(int i, int j) {
if(equal(x[i], x[j])) return 0;
else return (x[j] * y[i] - x[i] * y[j]) / x[i] / x[j] / (x[i] - x[j]);
}
void dfs(int t, int u, int v) {
if(u + v > ans) return;
if(t > n) {
ans = u + v; return;
}
bool flag = false;
for(int i = 1; i <= u; i++) {
if(equal(A[i] * x[t] * x[t] + B[i] * x[t], y[t])) {
dfs(t + 1, u, v);
flag = true;
break;
}
}
if(!flag) {
for(int i = 1; i <= v; i++) {
if(equal(x[lonely[i]], x[t])) continue;
double a = (y[t] * x[lonely[i]] - y[lonely[i]] * x[t]) / (x[t] * x[t] * x[lonely[i]] - x[lonely[i]] * x[lonely[i]] * x[t]);
if(a < 0) {
double b = (y[t] - a * x[t] * x[t]) / x[t];
A[u + 1] = a, B[u + 1] = b;
int idx = lonely[i];
for(int j = i; j < v; j++) lonely[j] = lonely[j + 1];
dfs(t + 1, u + 1, v - 1);
for(int j = v; j > i; j--) lonely[j] = lonely[j - 1];
lonely[i] = idx;
}
}
lonely[v + 1] = t;
dfs(t + 1, u, v + 1);
}
}
int main() {
int T; scanf("%d", &T);
while(T--) {
scanf("%d %d", &n, &m);
for(int i = 1; i <= n; i++) scanf("%lf %lf", &x[i], &y[i]);
ans = 100;
dfs(1, 0, 0);
printf("%d\n", ans);
}
return 0;
}