作者: KSkun

[ZJOI2009]取石子游戏 题解

[ZJOI2009]取石子游戏 题解

题目地址:洛谷:【P2599】[ZJOI2009]取石子游戏 – 洛谷、BZOJ:Problem 1413. — [ZJOI2009]取石子游戏

题目描述

在研究过Nim游戏及各种变种之后,Orez又发现了一种全新的取石子游戏,这个游戏是这样的: 有n堆石子,将这n堆石子摆成一排。游戏由两个人进行,两人轮流操作,每次操作者都可以从最左或最右的一堆中取出若干颗石子,可以将那一堆全部取掉,但不能不取,不能操作的人就输了。 Orez问:对于任意给出一个初始一个局面,是否存在先手必胜策略。

输入输出格式

输入格式:
文件的第一行为一个整数T,表示有 T组测试数据。对于每组测试数据,第一行为一个整数n,表示有n堆石子;第二行为n个整数ai,依次表示每堆石子的数目。

输出格式:
对于每组测试数据仅输出一个整数0或1。其中1表示有先手必胜策略,0表示没有。

输入输出样例

输入样例#1:

1
4
3 1 9 4

输出样例#1:

0

说明

数据范围
对于30%的数据 n≤5 ai≤10^5
对于100%的数据 T≤10 n≤1000 每堆的石子数目≤10^9

题解

我并不能保证底下的题解完全正确,欢迎各位同学阅读的时候自己想一想找找错,毕竟我在想这个题的时候也被绕进去了。包括底下的这个参考资料,我觉得也都有点问题。
参考资料:题解 P2599 【[ZJOI2009]取石子游戏】 – Jason_Yvan 的博客 – 洛谷博客
这里有一种做法,定义lft[i][j]表示(i, j)区间的左边加一堆该数量的石子先手必败,rgt[i][j]表示(i, j)区间的右边加一堆该数量的石子先手必败,我们利用这两个数组的递推关系进行区间DP,来判定每个局面的状况。这是由于局面之间是可以相互转移的。
设计这个定义的合法性基于对于一个区间,lft和rgt的值是唯一确定的。下面给出证明:

  1. 若存在不止一个lft值,那么一步可以把一种值拿成另一种值,会发现必败方发生变化,这与我们的定义不符;
  2. 若不存在lft值,那么必胜态只可能通过拿右边的石子转移到某个必败态,左边的石子对该状态没有影响。若固定右边的石子数,该状态对应了多种必败态,且左边的石子数都不相同,这相当于存在不止一种lft值,与1矛盾。

首先,类似Nim游戏,我们会发现当只有两堆石子且数量相等的时候,先手必败。因此初始值即lft[i][i] = rgt[i][i] = a[i]。
剩下的是一个区间DP的思路,我们枚举每个区间,进行转移。转移可以对状况分类讨论。
先分析求lft[i][j],令L = lft[i][j – 1],R = rgt[i][j – 1],X = a[j]:

  1. R == X:此时区间[i, j]已是必败态,因此lft[i][j] = 0即可;
  2. X < L && X < R:lft[i][j] = X。对于左右两堆石子,后手可以按照先手的操作在另外一堆进行同样的操作,直至两堆都没有了。此时会转移至区间[i, j – 1]的状态,该状态又会是分类讨论中的某一种情况,从而走向必败态;
  3. X < L && X > R:lft[i][j] = X – 1。如果先手在左边拿到Y,Y < R时,后手右边也可以拿到Y,则变为情况2;Y ≥ R时,后手在右边拿到Y + 1,则又是情况3。如果先手在右边拿到Y,Y < R时,也可到情况2;Y = R时,后手将左边一堆拿完,进入情况1;Y > R时,后手在左边拿到Y – 1,又是情况3;
  4. X ≥ L && X < R:lft[i][j] = X + 1。类似情况3的分析即可。
  5. X > L && X > R:lft[i][j] = X。先手若将某一堆拿到Y,Y > max(L, R)时,后手跟着先手拿又是情况5;若左侧拿到L或右侧拿到R,则把另一堆拿完,到情况1;否则我们可以设法转移到情况3和4。

rgt[i][j]仍可以用类似的方法来求。令L = lft[i + 1][j],R = rgt[i + 1][j],X = a[i]:

  1. L == X:此时区间[i, j]已是必败态,因此rgt[i][j] = 0即可;
  2. X < R && X < L:rgt[i][j] = X。对于左右两堆石子,后手可以按照先手的操作在另外一堆进行同样的操作,直至两堆都没有了。此时会转移至区间[i + 1, j]的状态,该状态又会是分类讨论中的某一种情况,从而走向必败态;
  3. X < R && X > L:rgt[i][j] = X + 1。如果先手在右边拿到Y,Y < L时,后手左边也可以拿到Y,则变为情况2;Y ≥ L时,后手在左边拿到Y – 1,则又是情况3。如果先手在左边拿到Y,Y < L时,也可到情况2;Y = L时,后手将右边一堆拿完,进入情况1;Y > L时,后手在右边拿到Y + 1,又是情况3;
  4. X ≥ R && X < L:rgt[i][j] = X – 1。类似情况3的分析即可。
  5. X > R && X > L:rgt[i][j] = X。先手若将某一堆拿到Y,Y > max(L, R)时,后手跟着先手拿又是情况5;若左侧拿到L或右侧拿到R,则把另一堆拿完,到情况1;否则我们可以设法转移到情况3和4。

最后验证a[1]是否与lft[2][n]相等即可。复杂度O(Tn^2)

代码

// Code by KSkun, 2018/6
#include <cstdio>
#include <cctype>

#include <algorithm>

typedef long long LL;

inline char fgc() {
    static char buf[100000], *p1 = buf, *p2 = buf;
    return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2)
        ? EOF : *p1++;
}

inline LL readint() {
    register LL res = 0, neg = 1; register char c = fgc();
    for(; !isdigit(c); c = fgc()) if(c == '-') neg = -1;
    for(; isdigit(c); c = fgc()) res = (res << 1) + (res << 3) + c - '0';
    return res * neg;
}

const int MAXN = 1005;

int T, n, a[MAXN], lft[MAXN][MAXN], rgt[MAXN][MAXN];

int main() {
    T = readint();
    while(T--) {
        n = readint();
        for(int i = 1; i <= n; i++) {
            a[i] = lft[i][i] = rgt[i][i] = readint();
        }
        for(int k = 1; k <= n; k++) {
            for(int i = 1; i + k <= n; i++) {
                int j = i + k, L = lft[i][j - 1], R = rgt[i][j - 1], X = a[j];
                if(R == X) lft[i][j] = 0;
                else if(L > X && R > X) lft[i][j] = X;
                else if(L <= X && R > X) lft[i][j] = X + 1;
                else if(L > X && R < X) lft[i][j] = X - 1;
                else lft[i][j] = X;
                L = lft[i + 1][j]; R = rgt[i + 1][j]; X = a[i];
                if(L == X) rgt[i][j] = 0;
                else if(R > X && L > X) rgt[i][j] = X;
                else if(R <= X && L > X) rgt[i][j] = X - 1;
                else if(R > X && L < X) rgt[i][j] = X + 1;
                else rgt[i][j] = X;
            }
        }
        puts(a[1] == lft[2][n] ? "0" : "1");
    }
    return 0;
}
[BZOJ3551][ONTAK2010]Peaks加强版 题解

[BZOJ3551][ONTAK2010]Peaks加强版 题解

题目地址:BZOJ:Problem 3551. — [ONTAK2010]Peaks加强版

题目描述

在Bytemountains有N座山峰,每座山峰有他的高度h_i。有些山峰之间有双向道路相连,共M条路径,每条路径有一个困难值,这个值越大表示越难走,现在有Q组询问,每组询问询问从点v开始只经过困难值小于等于x的路径所能到达的山峰中第k高的山峰,如果无解输出-1。

题意简述

有一个图,点和边都有权值。回答若干询问,每个询问表示只保留图中边权不大于x的边,v所在的连通块中,点权k大。强制在线。

输入输出格式

输入格式:
第一行三个数N,M,Q。
第二行N个数,第i个数为h_i
接下来M行,每行3个数a b c,表示从a到b有一条困难值为c的双向路径。
接下来Q行,每行三个数v x k,表示一组询问。v=v xor lastans,x=x xor lastans,k=k xor lastans。如果lastans=-1则不变。

输出格式:
对于每组询问,输出一个整数表示答案。

输入输出样例

输入样例#1:

10 11 4
1 2 3 4 5 6 7 8 9 10
1 4 4
2 5 3
9 8 2
7 8 10
7 1 4
6 7 1
6 4 8
2 1 5
10 8 10
3 4 7
3 4 6
1 5 2
7 3 0
0 4 9
9 8 3

输出样例#1:

6
1
-1
8

说明

【数据范围】
N<=10^5, M,Q<=5*10^5,h_i,c,x<=10^9。

题解

参考资料:BZOJ 3551: [ONTAK2010]Peaks加强版 [Kruskal重构树 dfs序 主席树] – Candy? – 博客园
强制在线之前的离线做法就不行了。这里要用新的科技:Kruskal重构树。
首先,连通块的边肯定在最小生成树上比较优,因此我们可以跑一波Kruskal处理。在Kruskal选中一条边的时候,对这条边开一个点,把边权放在点上,再从点引出两条边,指向边的端点的并查集集合代表元,将这条边的点设为这两个集合的并集代表元。
这样建出来的树有优秀的性质:

  1. 二叉树,因为边有两个端点
  2. 越浅的边点边权越大
  3. 叶子(无出边的点)是原图中的点,其他的都是边点
  4. 两点间路径上边权最大的边点是LCA
  5. 一个边点对应的子树代表一个边权不大于该边点的边组成的连通块
  6. 边权最大的边点是有根树的根

这里我们会用到第5条。一个查询就是在找v的边权不大于询问的最浅边点祖先,在该祖先的子树内找k大,这个显然可以DFS序建主席树做。
注意本地开够栈,DFS的规模会相当大。复杂度是O(n \log n)

代码

// Code by KSkun, 2018/6
#include <cstdio>
#include <cctype>

#include <algorithm>
#include <vector>

typedef long long LL;

inline char fgc() {
    static char buf[100000], *p1 = buf, *p2 = buf;
    return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2)
        ? EOF : *p1++;
}

inline LL readint() {
    register LL res = 0, neg = 1; register char c = fgc();
    for(; !isdigit(c); c = fgc()) if(c == '-') neg = -1;
    for(; isdigit(c); c = fgc()) res = (res << 1) + (res << 3) + c - '0';
    return res * neg;
}

const int MAXN = 200005, MAXM = 500005;

int n, m, q, N;

int fa[MAXN];

inline int find(int x) {
    return x == fa[x] ? x : fa[x] = find(fa[x]);
}

struct Node {
    int lch, rch, val;
} tr[MAXN << 4];
int rt[MAXN], tot;

inline void insert(int &o, int l, int r, int x) {
    tr[++tot] = tr[o]; o = tot;
    tr[o].val++;
    if(l == r) return;
    int mid = (l + r) >> 1;
    if(x <= mid) insert(tr[o].lch, l, mid, x);
    else insert(tr[o].rch, mid + 1, r, x);
}

inline int query(int o1, int o2, int l, int r, int k) {
    if(l == r) return l;
    int mid = (l + r) >> 1, rsiz = tr[tr[o2].rch].val - tr[tr[o1].rch].val;
    if(k <= rsiz) return query(tr[o1].rch, tr[o2].rch, mid + 1, r, k);
    else return query(tr[o1].lch, tr[o2].lch, l, mid, k - rsiz);
}

int w[MAXN], anc[MAXN][20];
std::vector<int> gra[MAXN];

struct Edge {
    int u, v, w;
} edges[MAXM];

inline bool cmp(Edge a, Edge b) {
    return a.w < b.w;
}

inline void kruskal() {
    int cnt = 0;
    for(int i = 1; i <= m; i++) {
        int u = edges[i].u, v = edges[i].v, fu = find(u), fv = find(v);
        if(fu == fv) continue;
        w[++N] = edges[i].w;
        gra[N].push_back(fu); gra[N].push_back(fv);
        fa[fu] = fa[fv] = fa[N] = N;
        if(++cnt == n - 1) break;
    }
}

int vn[MAXN], dep[MAXN], dfl[MAXN], dfr[MAXN], clk;

inline void dfs(int u) {
    dfl[u] = clk;
    if(u <= n) vn[++clk] = u;
    for(int i = 1; (1 << i) <= dep[u]; i++) {
        anc[u][i] = anc[anc[u][i - 1]][i - 1];
    }
    for(int i = 0; i < gra[u].size(); i++) {
        int v = gra[u][i];
        if(v == anc[u][0]) continue;
        dep[v] = dep[u] + 1;
        anc[v][0] = u;
        dfs(v);
    }
    dfr[u] = clk;
}

inline int findrt(int u, int x) {
    for(int i = 19; i >= 0; i--) {
        if(anc[u][i] && w[anc[u][i]] <= x) u = anc[u][i];
    }
    return u;
}

std::vector<int> tmp;

int main() {
    n = readint(); m = readint(); q = readint(); N = n;
    for(int i = 1; i <= n; i++) {
        fa[i] = i; 
    }
    tmp.push_back(-1);
    for(int i = 1; i <= n; i++) {
        w[i] = readint();
        tmp.push_back(w[i]);
    }
    for(int i = 1; i <= m; i++) {
        edges[i].u = readint(); edges[i].v = readint(); edges[i].w = readint();
    }
    std::sort(edges + 1, edges + m + 1, cmp);
    kruskal();
    dfs(N);
    std::sort(tmp.begin(), tmp.end());
    tmp.erase(std::unique(tmp.begin(), tmp.end()), tmp.end());
    for(int i = 1; i <= n; i++) {
        w[i] = std::lower_bound(tmp.begin(), tmp.end(), w[i]) - tmp.begin();
    }
    for(int i = 1; i <= n; i++) {
        rt[i] = rt[i - 1];
        insert(rt[i], 1, n, w[vn[i]]);
    }
    int lastans = 0, v, x, k;
    while(q--) {
        v = readint() ^ lastans; x = readint() ^ lastans; k = readint() ^ lastans;
        v = findrt(v, x);
        if(tr[rt[dfr[v]]].val - tr[rt[dfl[v]]].val < k) puts("-1"), lastans = 0;
        else printf("%d\n", lastans = tmp[query(rt[dfl[v]], rt[dfr[v]], 1, n, k)]);
    }
    return 0;
}
[POI2003]Chocolate 题解

[POI2003]Chocolate 题解

题目地址:BZOJ:Problem 2430. — [Poi2003]Chocolate

题目描述

有一块n*m的矩形巧克力,准备将它切成n*m块。巧克力上共有n-1条横线和m-1条竖线,你每次可以沿着其中的一条横线或竖线将巧克力切开,无论切割的长短,沿着每条横线切一次的代价依次为y1,y2,…,yn-1,而沿竖线切割的代价依次为x1,x2,…,xm-1。例如,对于下图6*4的巧克力,
11(2) - [POI2003]Chocolate 题解
我们先沿着三条横线切割,需要3刀,得到4条巧克力,然后再将这4条巧克力沿竖线切割,每条都需要5刀,则最终所花费的代价为y1+y2+y3+4*(x1+x2+x3+x4+x5)。
当然,上述简单切法不见得是最优切法,那么怎样切割该块巧克力,花费的代价最少呢?

题意简述

要把一个大矩形横着切n-1刀竖着切m-1刀分成n*m个小矩形,从左到右从上到下每个位置切一刀都有一个代价,每一次对一整块(不论大小,无法一刀切断多块)切一次都会产生一次代价,求切割的最小代价。

输入输出格式

输入格式:
第一行为两个整数n和m。
接下来n-1行,每行一个整数,分别代表x1,x2,…,xn-1。
接下来m-1行,每行一个整数,分别代表y1,y2,…,ym-1。

输出格式:
输出一整数,为切割巧克力的最小代价。

输入输出样例

输入样例#1:

6 4
2
1
3
1
4
4
1
2

输出样例#1:

42

说明

30%的数据,n<=100,m<=100
100%的数据,n<=10000,m<=10000

题解

贪心。
一定是先切掉代价较大的位置。考虑证明它,对于平行的刀切位置,在一个序列中先切代价大的,由于每一刀要乘上与位置垂直的位置已经切了几刀这个数字,因此越靠前乘的数字越小,总和越小。对于垂直的两个刀切位置,先切代价较大的那个,若先切较小的,较大的乘的数字会+1,比先切较大的更劣。
由于需要排序,复杂度O(n \log n)

代码

// Code by KSkun, 2018/6
#include <cstdio>
#include <cctype>

#include <algorithm>
#include <vector>
#include <functional>

typedef long long LL;

inline char fgc() {
    static char buf[100000], *p1 = buf, *p2 = buf;
    return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2)
        ? EOF : *p1++;
}

inline LL readint() {
    register LL res = 0, neg = 1; register char c = fgc();
    for(; !isdigit(c); c = fgc()) if(c == '-') neg = -1;
    for(; isdigit(c); c = fgc()) res = (res << 1) + (res << 3) + c - '0';
    return res * neg;
}

const int MAXN = 20005;

struct Node {
    int w;
    bool type;
    inline bool operator>(const Node &rhs) const {
        return w > rhs.w;
    }
};

int n, m;
std::vector<Node> vec;

int main() {
    n = readint(); m = readint();
    for(int i = 1, t; i < n; i++) {
        t = readint(); vec.push_back(Node {t, 0});
    }
    for(int i = 1, t; i < m; i++) {
        t = readint(); vec.push_back(Node {t, 1});
    }
    std::sort(vec.begin(), vec.end(), std::greater<Node>());
    int ch = 1, cv = 1, ans = 0;
    for(int i = 0; i < vec.size(); i++) {
        ans += vec[i].w * (vec[i].type ? ch : cv);
        if(vec[i].type) cv++; else ch++;
    }
    printf("%d", ans);
    return 0;
}