2014 Nordic Collegiate Programming Contest
题目链接:https://codeforces.com/gym/100502
D - Dice Game
签到题1,和上次写的思路一样,先枚举每个人的两个骰子的结果,算出每个值的概率,然后再暴力枚举两次值,算出每个值的获胜概率和失败概率。每个事件的概率都是相等的,所以可以直接用整数。
int p1[205];
int p2[205];
void test_case() {
int l1, r1, l2, r2;
scanf("%d%d%d%d", &l1, &r1, &l2, &r2);
int sum1 = 0;
for(int i = l1; i <= r1; ++i) {
for(int j = l2; j <= r2; ++j) {
++p1[i + j];
++sum1;
}
}
scanf("%d%d%d%d", &l1, &r1, &l2, &r2);
int sum2 = 0;
for(int i = l1; i <= r1; ++i) {
for(int j = l2; j <= r2; ++j) {
++p2[i + j];
++sum2;
}
}
ll W = 0, T = 0, L = 0;
for(int i = 1; i <= 200; ++i) {
for(int j = 1; j < i; ++j)
W += p1[i] * p2[j];
T += p1[i] * p2[i];
for(int j = i + 1; j <= 200; ++j)
L += p1[i] * p2[j];
}
if(W > L)
puts("Gunnar");
else if(W < L)
puts("Emma");
else
puts("Tie");
}
K - Train Passengers
签到题2,直接模拟。
void test_case() {
int C, n;
scanf("%d%d", &C, &n);
ll cur = 0;
for(int i = 1; i <= n; ++i) {
int out, in, wait;
scanf("%d%d%d", &out, &in, &wait);
if(out > cur) {
puts("impossible");
return;
}
cur -= out;
cur += in;
if(cur > C) {
puts("impossible");
return;
}
if(cur < C && wait != 0) {
puts("impossible");
return;
}
}
if(cur != 0) {
puts("impossible");
return;
}
puts("possible");
return;
}
E - Opening Ceremony
签到题3。
题意:给一串数字,表示一些大楼的高度,每次操作:1:把一栋大楼清空,或者操作2:把每栋大楼(假设还有)都减少1。问最少的操作次数。
题解:贪心,排序,然后枚举中间点,中间点及其之前的全部用操作2清空,后面的全部用操作1清空。
int a[100005];
void test_case() {
int n;
scanf("%d", &n);
for(int i = 1; i <= n; ++i)
scanf("%d", &a[i]);
sort(a + 1, a + 1 + n);
int ans = n;
for(int i = 1; i <= n; ++i)
ans = min(ans, a[i] + n - i);
printf("%d\n", ans);
}
H - Clock Pictures
题意:有两个钟,都是有n根(2<=n<=200000)互不重叠的指针,他们分别位于一些刻度,刻度之间无法区分,所以不妨从0点开始标记刻度,刻度的范围是[0,360000)。问这两个钟是否有可能指示同一个时刻。
题解:也就是说指针之间的角度是钟的唯一特征,作一个差分,然后把第二个钟的差分序列延长一倍,问题变成在第二个钟的差分序列上是否存在第一个钟的差分序列的一个完全匹配。当时没想到用KMP做,用的多项式hash,然后用一发WA证明了多项式哈希假如BASE不取质数的话确实会WA。
看学弟居然还能随机选一些点验证的,比我还暴力,遇到出锅场就很强。
int a[200005];
int b[400005];
int d[400005];
void test_case() {
int n;
scanf("%d", &n);
for(int i = 1; i <= n; ++i)
scanf("%d", &a[i]);
for(int i = 1; i <= n; ++i)
scanf("%d", &b[i]);
sort(a + 1, a + 1 + n);
sort(b + 1, b + 1 + n);
for(int i = 1; i <= n; ++i)
b[i + n] = b[i];
for(int i = 1; i <= 2 * n; ++i) {
d[i] = b[i] - b[i - 1];
if(d[i] < 0)
d[i] += 360000;
}
ll BASE = 233;
ll ha = 0;
for(int i = 2; i <= n; ++i) {
ha = ha * BASE + (a[i] - a[i - 1]);
if(ha >= MOD)
ha %= MOD;
}
ll ha2 = 0;
for(int i = 2; i <= n; ++i) {
ha2 = ha2 * BASE + d[i];
if(ha2 >= MOD)
ha2 %= MOD;
}
if(ha == ha2) {
puts("possible");
return;
}
ll BASEK = qpow(BASE, n - 1);
for(int i = n + 1; i <= 2 * n; ++i) {
ha2 = ha2 * BASE + d[i];
if(ha2 >= MOD)
ha2 %= MOD;
ha2 = ha2 - d[i - n + 1] * BASEK % MOD;
if(ha2 < 0)
ha2 += MOD;
if(ha == ha2) {
puts("possible");
return;
}
}
puts("impossible");
return;
}
收获:
1、多项式哈希,确定要去掉的那一项被乘了多少次BASE,把这个项乘上同样的次数的BASE然后减掉,就可以从区间中去掉前面的项,同样的,假如存好每个位置的前缀哈希值,那么可以用后面的哈希减去前面的哈希乘上长度(多的BASE),就得到中间项的哈希。
2、可以取一系列为1的字符,然后混一个为2的字符,BASE取10,这样可以很容易看见哈希的过程。
3、在单文本串中找单模式串的完全匹配,整个KMP应该是最简单的。在单文本串中找多模式串的完全匹配,可以用AC自动机,那什么时候才用哈希呢?可能在文本串很长,然后模式串是强制在线输入的时候,KMP每次要跑一次文本串长度,而强制在线也不能够建AC自动机。但是哈希的话预处理文本串的哈希值放在map里面就行。有没有后缀数据结构可以解决这个问题呢?
写一下KMP和随机法搞过去的吧。
C - Catalan Square
题意:已知卡塔兰数的递推式为 \(H_1=1,H_{n+1}=\sum\limits_{i=0}^nH_iH_{n-i}\) 以及 \(H_n=\frac{1}{n+1}C_{2n}^{n}\) 求 \(S_n=\sum\limits_{i=0}^nH_iH_{n-i}\) 。
题解:其实就是要输出 \(S_n=\sum\limits_{i=0}^nH_iH_{n-i}=H_{n+1}\) ,用含有大数的语言直接用组合数求一波就可以了。注意不能真的计算所有的卡塔兰数然后相乘求和,因为乘法的复杂度过高了。
import java.math.BigInteger;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner=new Scanner(System.in);
int n= scanner.nextInt();
n++;
System.out.println(C(2*n,n).divide(BigInteger.valueOf(n+1)));
}
static BigInteger C(int n, int m){
BigInteger res1=BigInteger.ONE;
for(int i=n,k=m;k>0;--i,--k)
res1=res1.multiply(BigInteger.valueOf(i));
BigInteger res2=BigInteger.ONE;
for(int i=1,k=m;k>0;++i,--k)
res2=res2.multiply(BigInteger.valueOf(i));
return res1.divide(res2);
}
}
参考资料:https://zh.wikipedia.org/wiki/卡塔兰数
A - Amanda Lounges
看起来很像前天的“三人帮”的那道题,仔细想想是不是可以不用“扩展域”并查集来做。
题意:有个 \(n(1\leq n\leq 200000)\) 个点 \(m(1\leq m\leq 200000)\) 条边的无向图,边带权 \(\{0,1,2\}\) ,边的值表示边两端的点有多少个是黑点。求一种黑色点最少的染色方案,输出黑色点的数量,无解输出impossible。
题解:像昨天一样故技重施,用 \(A_0\) 表示点A染为白色,用 \(A_1\) 表示点A染为黑色,那么边带权为0或2的连一堆TRUE和FALSE,边带权为1的进行等价信息传递。假如不矛盾(指TRUE和FALSE不等价,且每个 \(A_0\) 和 \(A_1\) 之间都不等价),就有解。此时每个连通区域变成一个二分图,假如可以选,就选较少数量一侧的点(而不是一半的下整)染黑色。不能选的直接构造。具体实现时,连接有TRUE和FALSE的点所在的连通区域的颜色是固定的,从这些确定颜色的点开始染色并标记。经过一轮扫描之后,颜色确定的连通区域就已经完全被正确染色了,再扫描一次进行统计。最后剩下的就是可以自由染色的二分图,统计奇数深度和偶数深度的点的数量,取少的一侧即可。注意vis标记之间不能够有互相影响。
struct DisjointSetUnion {
static const int MAXN = 400005;
int n, fa[MAXN + 5], rnk[MAXN + 5];
void Init(int _n) {
n = _n;
for(int i = 1; i <= n; i++) {
fa[i] = i;
rnk[i] = 1;
}
}
int Find(int u) {
int r = fa[u];
while(fa[r] != r)
r = fa[r];
int t;
while(fa[u] != r) {
t = fa[u];
fa[u] = r;
u = t;
}
return r;
}
bool Merge(int u, int v) {
u = Find(u), v = Find(v);
if(u == v)
return false;
else {
if(rnk[u] < rnk[v])
swap(u, v);
fa[v] = u;
rnk[u] += rnk[v];
return true;
}
}
bool Query(int u, int v) {
u = Find(u), v = Find(v);
return u == v;
}
} dsu;
int vis[200005];
int cnt[200005];
vector<int> G[200005];
void dfs1(int u, int c) {
if(vis[u] == 2)
return;
vis[u] = 2;
cnt[u] = c;
for(auto &v : G[u])
dfs1(v, 1 - c);
}
int cnt1, cnt2;
void dfs2(int u, int c) {
if(vis[u] == 3)
return;
vis[u] = 3;
if(c == 1)
++cnt1;
else
++cnt2;
for(auto &v : G[u])
dfs2(v, 1 - c);
return;
}
void test_case() {
int n, m;
scanf("%d%d", &n, &m);
dsu.Init(2 * n + 2);
int TRUE = 2 * n + 1;
int FALSE = 2 * n + 2;
while(m--) {
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
if(w == 0) {
dsu.Merge(u, TRUE);
dsu.Merge(v, TRUE);
dsu.Merge(u + n, FALSE);
dsu.Merge(v + n, FALSE);
vis[u] = 1;
vis[v] = 1;
cnt[u] = 0;
cnt[v] = 0;
} else if(w == 2) {
dsu.Merge(u, FALSE);
dsu.Merge(v, FALSE);
dsu.Merge(u + n, TRUE);
dsu.Merge(v + n, TRUE);
vis[u] = 1;
vis[v] = 1;
cnt[u] = 1;
cnt[v] = 1;
} else {
dsu.Merge(u, v + n);
dsu.Merge(v, u + n);
G[u].push_back(v);
G[v].push_back(u);
}
}
if(dsu.Query(TRUE, FALSE)) {
puts("impossible");
return;
}
for(int i = 1; i <= n; ++i) {
if(dsu.Query(i, i + n)) {
puts("impossible");
return;
}
}
int sum = 0;
for(int i = 1; i <= n; ++i) {
if(vis[i] == 1)
dfs1(i, cnt[i]);
}
for(int i = 1; i <= n; ++i) {
if(vis[i] == 2)
sum += cnt[i];
}
for(int i = 1; i <= n; ++i) {
if(vis[i] == 0) {
cnt1 = 0;
cnt2 = 0;
dfs2(i, 1);
sum += min(cnt1, cnt2);
}
}
printf("%d\n", sum);
}
但是其实根本没有用到什么“扩展域”并查集啊,直接先对颜色确定的连通区域进行dfs染色,假如染色失败就是impossible的(就是进入点之后,发现这个点已经被染色的话,验证是否同色),然后再dfs统计是不是二分图,选小的一侧即可。
前天的那题“三人帮”,因为有第三个集合存在,所以不能够进行二分图染色,或者说,染色方法非常复杂。况且当时题目是需要判断是否存在一个方案,用“扩展域”并查集应该是没错。
G - Outing
题意:有 \(n(1\leq n \leq 1000)\) 个人,一辆 \(m(1\leq m \leq n)\) 个空位的巴士。每个人能被选到巴士上,仅当其指定的一个人也被选到巴士上。求最多选择多少人到巴士上。
题解:易知原图就是一片内向基环树森林,每个点要被选中的前提是其出边指向的点也被选中,所以说某棵基环树中若是有节点被选中,则必须选中整个环,然后可以在链上取一部分点。看起来就变成一个类似背包的东西,每棵基环树变成一个大小为环的大小,价值为树的大小的物品,然后求最大价值的物品集,最后和容量取个min。正好背包就是这个复杂度。
int n, m;
int G[1005];
vector<int> BG[1005];
int W[1005];
int V[1005];
int k;
int color[1005], cntcolor;
int iscircle[1005];
int incircle;
void dfs(int u, int c) {
if(color[u]) {
if(color[u] == c) {
incircle = u;
return;
}
return;
}
color[u] = c;
dfs(G[u], c);
if(incircle) {
iscircle[u] = 1;
if(u == incircle)
incircle = 0;
}
}
bool vis[1005];
int siz1, siz2;
void dfs2(int u) {
vis[u] = 1;
++siz2;
if(iscircle[u])
++siz1;
if(!vis[G[u]])
dfs2(G[u]);
for(auto &v : BG[u]) {
if(!vis[v])
dfs2(v);
}
}
void Build() {
int cntcolor = 0;
for(int i = 1; i <= n; ++i) {
if(!color[i]) {
++cntcolor;
dfs(i, cntcolor);
}
}
k = 0;
for(int i = 1; i <= n; ++i) {
if(!vis[i]) {
siz1 = 0;
siz2 = 0;
dfs2(i);
++k;
W[k] = siz1;
V[k] = siz2;
//printf("%d %d\n", W[k], V[k]);
}
}
}
int dp[1005][1005];
void test_case() {
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; ++i) {
scanf("%d", &G[i]);
BG[G[i]].push_back(i);
}
Build();
memset(dp, -INF, sizeof(dp));
dp[0][0] = 0;
for(int i = 1; i <= k; ++i) {
for(int j = 0; j < W[i]; ++j)
dp[i][j] = dp[i - 1][j];
for(int j = W[i]; j <= m; ++j)
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - W[i]] + V[i]);
}
int ans = 0;
for(int j = 0; j <= m; ++j)
ans = max(ans, dp[k][j]);
printf("%d\n", min(m, ans));
}
I - How many squares?
连续两次被几何卡了,真是服,虽然有其他办法,但是本质上是不懂这方面的技巧。