NOIP2016 复盘

NOIP2016 复盘

D1T1 P1563 玩具谜题

我一开始学OI的时候以为可以直接跳链表,用膝盖想一想就知道会T。

所以做法就是判断顺时针转还是逆时针转,转完把超出范围的下标弄回来即可。

代码:

#include<bits/stdc++.h>
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

直接给数据表:

3441.png

  1. 1 2这两个点代表所有人的起点都等于终点。

    当且仅当这些人所在的\(w=0\)时,能被观察到。

  2. 3 4这两个点代表\(w_j=0\),也就是观察者只在一开始观察。

    直接遍历,找到不重复的所有的起点个数就完事了。

  3. 5这一个点只是数据小,没有其他特殊条件。

    我们直接使用暴力做法,按题意模拟,找到所有路径的LCA,左右一起爬上去,计算路径上每一个点对应的时间,如果刚好被观察就计入答案。

    其实前5个点都可以直接用这个方法解决。

  4. 6 7 8这三个点是退化成链的部分分。

    我们认为从左到右分别是1到\(n\)。那么他们的路径就会有从左往右和从右往左两种。

    我们对这些点来考虑贡献。显然对于一个点,如果左边\(w_i\)处有人要往右出发并且终点在这个点及其右边,就可以计入贡献。另一边也一样。

    所以我们用一个vector记下从某个点出发的所有终止点,分上述两种思路去想,分别记录两种情况产生的贡献。

  5. 9 10 11 12这4个点是所有起点都为1的部分分。

    我们直接可以以1为根来遍历整棵树,其中每一层的点要想观察到的话就必须要有\(w[i]=dep[i]-1\)

    我们进行树上差分,把路径上所有的点都标记起来,然后依次判断即可。

  6. 13 14 15 16这4个点是所有终点都为1的部分分。

    显然对于一个观察点\(i\),当且仅当深度为\(w[i]+dep[i]\)的起点才会产生贡献。

    参考了下题解的方法,用一个桶表示对应深度的起点的个数,先保存dfs前的桶里面的值,然后进行一次dfs,dfs后再查询值,前后之差就是这个深度对应的所有起点。

剩下的4个点我是真的不知道,但是考场上又不可能写出来,只能拿部分分维持生活的这个样子……

#include<cstdio>
#include<algorithm>
#include<vector>
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<int> 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考,难度很大!

最开始一定要知道什么是期望:期望就是答案乘以概率 的和。

我们先口胡看一下部分分:

3443.png

特殊性质1:图上任意两点\(a_i,b_i,a_i\not=b_i\)间,存在一条耗费体力最少的路径只包含一条道路。

特殊性质2:对于所有的\(1\leq i\leq n,k_i=1\)

  1. 2 5 8 12 16 20这5个点是\(m=0\)的部分分。

    意思就是一次都换不了,做法就是跑下最短路然后模拟就是了。

  2. 1 3 6 9 13 17 21这6个点是\(m=1\)的部分分。

    意思就是可以换一次,做法依然要跑最短路,然后枚举要换哪节课的教室,如果换了答案更优就换,因为答案可能换完更劣。用前缀和可以优化。

  3. 5 8 11 15 18 19这6个点是满足特殊性质2的部分分。

    意思就是每次要换就能换成功,不需要考虑概率问题了,但是仍然有次数限制。

    直接爆搜肯定不可行了,我们终于来考虑dp。

    经过设想,我们可以设dp[i][j][0/1]来表示当前上了\(i\)节课,已经(在这里可以认为成功地)尝试换了\(j\)节课,0表示最后一次没试,1表示最后一次试了。

    更新dp状态的时候,按照这次换不换去更新即可。

  4. 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]\)的概率失败,在原班级上课。两种情况一起求期望再一起加入原先答案即可。

若这次在新教室,那就麻烦了那么这次和上次都不知道成功与否,不过也都知道对应的概率,那么我们同样分上次试了跟上次没试两种情况讨论。

  1. 上次没试。那么增加的期望答案就综合2个因素:这次成功了还是这次失败了。
  2. 上次试了。那么增加的期望答案就必须综合4个因素:两次都成功了,两次都失败了,这次成功上次失败,上次成功这次失败。4个答案按概率加权平均后计入答案就可以完成更新。
#include<cstdio>
#include<cstring>
#include<algorithm>
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<cstdio>
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<cstdio>
#include<cmath>
#include<algorithm>
#include<queue>
#define ll long long
const int maxn = 100005;
const ll INF = 0x3f3f3f3f3f;
ll a[maxn];

std::queue<ll> 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)\)计算出经过两只猪之间的抛物线方程(如果存在的话)。

这是我的式子:

1431596-20190825120448089-658420940.png

要求b的话直接代一个进去就行了。

小猪的最终结局分成两种:一种被经过多头猪的抛物线打下来,一种被专属于自己的抛物线打下来。

而我们在爆搜的时候,对前面的猪,也分两种情况:一种被当前已确定的抛物线打下来,一种等待被打下来。

先考虑这只猪能否被已经被已有的抛物线打下来,有就直接考虑下一只。

如果不能的话,再考虑能否与前面还没打下来的猪配对。

但是并不是当前配对了就一定是最优的,反例太多了。

于是这只猪当然要考虑暂时落单,被后面的猪配对。

爆搜到最后可能会有一些猪依然没打下来,这就没有办法了,只能一只猪一条抛物线。

大体思路就是这样。

代码:

#include<bits/stdc++.h>
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;
}

转载于:https://www.cnblogs.com/Garen-Wang/p/11334170.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值