2013 Multi-University Training Contest 2
HDU-4611 Balls Rearrangement
题意:具体题意不大清楚,最后要处理一个这样的表达式:sum{ |i % a - i % b| },0 <= i < N 的取值很大,a、b均小于10^5。
分析:观察|i % a|和|i % b|可以发现其均为被模数的一个滚动剩余系,且中间的某些段的值是恒定的。再注意到其实处理到a和b的最小公倍数的时候又可以把最小公倍数循环的部分处理出来。我的做法就是维护好两个数,分别表示a和b两边谁出现最进出现 i % a 或者是 i % b 等于0的情况,因为此时两者的差值会发生改变,到达公倍数后则会出现两数同时到达该关键点,此时用总次数 N 除以公倍数加速。由于公倍数和枚举的间隔两者是此消彼长得关系,因此算法得以在较快时间内完成。
#include <cstdio> #include <cstring> #include <cstdlib> #include <queue> #include <iostream> #include <algorithm> using namespace std; priority_queue<int>q1, q2; int main() { int T; scanf("%d", &T); int a, b, n; while (T--) { scanf("%d %d %d", &n, &a, &b); while (!q1.empty()) q1.pop(); while (!q2.empty()) q2.pop(); q1.push(a), q2.push(b); int gap = 0, ti = -1; long long ret = 0; n -= 1; while (ti < n) { gap = min(q1.top(), q2.top()); if (ti + gap > n) { gap = n - ti; } ti += gap; ret += 1LL*gap*abs(ti % a - ti % b); q1.pop(), q2.pop(); int lst1 = q1.top()-gap; int lst2 = q2.top()-gap; if (!lst1 && !lst2) { ret += ((n+1)/(ti+1)-1) * ret; ti = (n+1)/(ti+1)*(ti+1)-1; } if (lst1 > 0) q1.push(lst1); else q1.push(a+lst1); if (lst2 > 0) q2.push(lst2); else q2.push(b+lst2); } printf("%I64d\n", ret); } return 0; }
HDU-4614 Vases and Flowers
题意:有N个花瓶编号为0 - N-1,现在每天可能要插一些花到花瓶或者清楚一些瓶中的花。现有两种操作,第一种操作是要求从第A号花瓶开始插入F朵花,从A到N-1依次遍历,有空花瓶则将花插入,如果没有查完的话,剩下的花舍弃,输出该次插入花的第一个和最后一个花瓶的编号;第二种操作是清理L-R花瓶的花,输出此次清理中有多少花瓶中是有花的。
分析:通过一颗线段树维护区间和值即可。对于第一种操作,如果从A到N-1区间剩余的容量为0的话,输出不能插入,否则修改插入的花数目为剩余容量。然后二分出[A, x]之间容量为 1 和 F 的点即可,再把二分出的两个值所形成的区间作一个 1 覆盖;对于第二种操作直接输出区间和然后进行一次区间 0 覆盖。
#include <cstdlib> #include <cstdio> #include <cstring> #include <iostream> #include <algorithm> #define lch p<<1 #define rch p<<1|1 using namespace std; const int N = 50005; int n, m; struct Node { int l, r, sum, add; }e[N<<2]; void push_up(int p) { e[p].sum = e[lch].sum + e[rch].sum; } void push_down(int p) { if (e[p].add != -1) { e[lch].add = e[rch].add = e[p].add; e[lch].sum = e[p].add * (e[lch].r - e[lch].l + 1); e[rch].sum = e[p].add * (e[rch].r - e[rch].l + 1); // 拥有延时标记的节点的属性要保证是正确的 e[p].add = -1; } } void build(int p, int l, int r) { e[p].l = l, e[p].r = r; e[p].sum = 0, e[p].add = -1; if (l < r) { int mid = (l + r) >> 1; build(lch, l, mid); build(rch, mid+1, r); push_up(p); } } void modify(int p, int l, int r, int val) { if (l == e[p].l && r == e[p].r) { e[p].add = val; e[p].sum = e[p].add * (e[p].r - e[p].l + 1); } else { push_down(p); int mid = (e[p].l + e[p].r) >> 1; if (r <= mid) modify(lch, l, r, val); else if (l > mid) modify(rch, l, r, val); else { modify(lch, l, mid, val); modify(rch, mid+1, r, val); } push_up(p); } } int query(int p, int l, int r) { if (l == e[p].l && r == e[p].r) { return e[p].sum; } else { push_down(p); int mid = (e[p].l + e[p].r) >> 1; if (r <= mid) return query(lch, l, r); else if (l > mid) return query(rch, l, r); else return query(lch, l, mid) + query(rch, mid+1, r); } } int solveleft(int A, int F) { int l = A, r = n-1, ret; while (l <= r) { int mid = (l + r) >> 1; if (mid-A+1-query(1, A, mid) >= 1) { ret = mid, r = mid - 1; } else { l = mid + 1; } } return ret; } int solveright(int A, int F) { int l = A, r = n-1, ret; while (l <= r) { int mid = (l + r) >> 1; if (mid-A+1-query(1, A, mid) >= F) { ret= mid, r = mid - 1; } else { l = mid + 1; } } return ret; } int main() { int T; scanf("%d", &T); while (T--) { scanf("%d %d", &n, &m); build(1, 0, n-1); int x, A, F, ll, rr, op1, op2; for (int i = 0; i < m; ++i) { scanf("%d %d %d", &x, &op1, &op2); if (x == 1) { A = op1, F = op2; int left = (n-A) - query(1, A, n-1); if (!left) { puts("Can not put any one."); } else { if (left < F) {// 说明不够地方插花 F = left; } printf("%d %d\n", ll = solveleft(A, F), rr = solveright(A, F)); modify(1, ll, rr, 1); } } else { ll = op1, rr = op2; printf("%d\n", query(1, ll, rr)); modify(1, ll, rr, 0); } } puts(""); } return 0; }
HDU-4616 Game
题意:给定N个点,代表N个房间,每个房间里面有一些礼物,每个礼物用一个价值来表示,一些房间里面还有一些陷阱,现在告诉你可以有多少次机会被陷阱抓到,最后一次被抓到后不能移动。N个点,N-1条边形成一棵树,现在允许你从任意一个点出发,不能够往回走,在满足要求的情况下能够获得的最大的礼物总价值。
分析:典型的树形dp题目。设状态dp[i][j][0]表示第 i 个顶点已经经过 j 次陷阱,并且从非陷阱开始的路径中最大的价值,dp[i][j][1]就是起点从陷阱开始的最大价值。定义好状态后在树中进行动态规划,主要的思想就是枚举组合子树形成解和向上递推出父亲节点的值。在转移的时候要注意节点的初始化以及一个状态必须由一个合法更新的状态推过来,因为给定礼物中没有价值为0的礼物,因此初始化所有状态为0,以非0来断定一个状态是否合法。
#include <cstdlib> #include <cstring> #include <cstdio> #include <vector> #include <iostream> #include <algorithm> using namespace std; const int N = 50005; int n, m, ans; int vl[N], bk[N]; int dp[N][4][2]; char vis[N]; vector<int>v[N]; // dp[i][j][0]表示到第i个节点的一条路径经过k次陷阱并且从非陷阱开始的最大价值 // dp[i][j][1]表示到第i个节点的一条路径经过k次陷阱并且从陷阱开始的最大价值 void dfs(int u) { // 树形dp vis[u] = 1; int gd[4][2] = {0}; vector<int>const&vt = v[u]; int LIM = m - bk[u]; // 由于合并子树必定要经过u节点,因此该节点是否为陷阱将影响到合并 for (int i = 0; i < (int)vt.size(); ++i) { if (vis[vt[i]]) continue; dfs(vt[i]); // 下面做一个合并子树形成最终方案的操作 for (int j = 0; j <= LIM; ++j) { // 一共使用少于LIM次逃离陷阱的机会将不关心其端点是否为陷阱 for (int k = 0; j+k <= LIM; ++k) { if (gd[j][0] && dp[vt[i]][k][1]) ans = max(ans, vl[u]+gd[j][0]+dp[vt[i]][k][1]); if (gd[j][1] && dp[vt[i]][k][0]) ans = max(ans, vl[u]+gd[j][1]+dp[vt[i]][k][0]); if (gd[j][1] && dp[vt[i]][k][1]) ans = max(ans, vl[u]+gd[j][1]+dp[vt[i]][k][1]); // 当合并的陷阱次数达到LIM时两边均从非陷阱出发将是非法的组合 if (j + k != LIM && gd[j][0] && dp[vt[i]][k][0]) ans = max(ans, vl[u]+gd[j][0]+dp[vt[i]][k][0]); } } for (int j = 0; j <= m; ++j) { gd[j][1] = max(gd[j][1], dp[vt[i]][j][1]); if (j != m) // 已经走了m次陷阱并且以非陷阱开始则该路线已经不能再加节点 gd[j][0] = max(gd[j][0], dp[vt[i]][j][0]); // 维护这个数组是为了往根节点进行一个信息的反馈 } } // 处理完所有的孩子节点,此时向上更新 if (bk[u]) { for (int i = 0; i < m; ++i) { if (gd[i][0]) dp[u][i+1][0] = vl[u] + gd[i][0]; if (gd[i][1] || i == 0) // i == 0是为了初始化而来的 dp[u][i+1][1] = vl[u] + gd[i][1]; ans = max(ans, max(dp[u][i+1][0], dp[u][i+1][1])); } } else { for (int i = 0; i <= m; ++i) { // 孩子节点已经经过了m次陷阱,但是如果该点不为陷阱的话还是可以接起来的 if (gd[i][0] || i == 0) dp[u][i][0] = vl[u] + gd[i][0]; if (gd[i][1]) dp[u][i][1] = vl[u] + gd[i][1]; ans = max(ans, max(dp[u][i][0], dp[u][i][1])); } } } int main() { int T; scanf("%d", &T); while (T--) { scanf("%d %d", &n, &m); memset(dp, 0, sizeof (dp)); memset(vis, 0, sizeof (vis)); ans = 0; for (int i = 0; i < n; ++i) { v[i].clear(); scanf("%d %d", &vl[i], &bk[i]); } int x, y; for (int i = 1; i < n; ++i) { scanf("%d %d", &x, &y); v[x].push_back(y); v[y].push_back(x); } dfs(0); printf("%d\n", ans); } return 0; }
HDU-4618 Palindrome Sub-Array
题意:给定一个N*M的矩阵,每个位置有一个大于0的整数,现在要求找出一个最大子方阵使得每一行每一列都是一个回文串,输出最大的方阵的边长。
分析:由于回文串会牵涉到奇偶性的问题,所以首先将整个串的长度扩充,在每个数字的左右都添加上-1,这样避免了回文串为偶数时的麻烦。接着预处理出每一行每一列的回文半径数组(这里我改造了一下manacher函数,增加一个相邻元素间隔参数,使得每一列能够像一行一样处理),然后从左上角开始O(N^2)的枚举每一个点作为方阵的中心并且一圈一圈逐渐扩大,这样使得每次只需要判定关键性的四个点即可,O(N)的时间也便能枚举完所有的边长。在处理长度的时候要分枚举点是否为-1进行一下讨论。在扩充的数组中难免会有一些行或者一些列全由-1组成,这些行和列是不用计算的,因此计算回文数组是跳着计算的,枚举边长的时候因为有全-1圈的存在,因此也是跳着枚举的,注意的是中心点的不同,初始化边长也不一样。
#include <cstdlib> #include <cstdio> #include <cstring> #include <iostream> #include <algorithm> using namespace std; const int N = 305; int n, m; int mp[N<<1][N<<1]; // 扩充一倍用来插入特殊字符统一处理奇偶问题 int col[N<<1][N<<1]; // 从每一列来观察每个元素,那么其在所在列能够扩展的最长长度为多少 int row[N<<1][N<<1]; // 从每一行来观察每个元素,那么其在所在行能够扩展的最长长度为多少 void debug() { for (int i = 0; i < n; ++i) { for (int j = 0; j < m; ++j) { printf("%3d", mp[i][j]); } puts(""); } } // manacher三个参数的意义分别是数组的起始地址,相邻元素间隔4个字节为一单位,数组的长度 void manacher(const int ary[], int delta, int length, int rad[]) { for (int i=1, j=0, k; i < length; ) { while (i-j-1 >= 0 && i+j+1 < length && ary[(i-j-1)*delta] == ary[(i+j+1)*delta]) { ++j; } rad[i] = j; for (k = 1; i+k < length && k <= j && rad[i-k] != rad[i]-k; ++k) { rad[i+k] = min(rad[i-k], rad[i]-k); } i += k, j -= k; } } inline bool check(int i, int j, int k) { if (row[i-k][j] < k) return false; if (row[i+k][j] < k) return false; if (col[j-k][i] < k) return false; if (col[j+k][i] < k) return false; return true; } int gao() { int u, d, l, r, range, ret = 1; for (int i = 0; i < n; ++i) { for (int j = 0; j < m; ++j) { u = i, d = n-1-i, l = j, r = m-1-j; range = min(min(u,d), min(l,r)); for (int k = (~mp[i][j]) ? 2:1; k <= range; k+=2) { if (!check(i, j, k)) break; else ret = max(ret, ((k+1)/2)*2 + (mp[i][j] != -1)); } } } return ret; } int main() { int T; scanf("%d", &T); while (T--) { scanf("%d %d", &n, &m); memset(mp, 0xff, sizeof (mp)); for (int i = 1, j = 0; j < n; i+=2, ++j) { for (int k = 1, h = 0; h < m; k+=2, ++h) { scanf("%d", &mp[i][k]); } } n = n<<1|1, m = m<<1|1; for (int i = 1; i < n; i+=2) { manacher(mp[i], 1, m, row[i]); } for (int i = 1; i < m; i+=2) { manacher(&mp[0][i], N<<1, n, col[i]); } printf("%d\n", gao()); } return 0; }
HDU-4619 Warm up 2
题意:给定一个网格,现在有两种1*2的骨牌放在网格上,一种是横着放的即放到(x,y)和(x+1,y),一种是竖着放的放到(x,y)和(x,y+1)。同一类型的骨牌不能够重叠的部分,不同类型的骨牌则没有这一要求,问在给定的放置方案下,如果取出最少的骨牌,使得剩下的骨牌能够没有重叠的部分。
分析:由于同一类型的骨牌没有不会产生关系,这非常符合二分图的定义,又因为边的数目很少(没有和点的平方成正比),通过在两种骨牌之间构边采用邻接表的形式保存边关系时间复杂度为O(N*M),完全可接受。最大独立子集=顶点数-最大匹配数。
#include <cstdlib> #include <cstring> #include <cstdio> #include <vector> #include <algorithm> #include <iostream> using namespace std; struct Node { int x, y; }; const int N = 1005; Node h[N], v[N]; int n, m; vector<int>vt[N]; int match[N]; char vis[N]; inline bool check(int a, int b) { int _x1 = h[a].x, _y1 = h[a].y; int _x2 = _x1+1, _y2 = _y1; int _x3 = v[b].x, _y3 = v[b].y; int _x4 = _x3, _y4 = _y3+1; if (_x1 == _x3 && _y1 == _y3) return true; if (_x1 == _x4 && _y1 == _y4) return true; if (_x2 == _x3 && _y2 == _y3) return true; if (_x2 == _x4 && _y2 == _y4) return true; return false; } void build() { for (int i = 1; i <= n; ++i) { for (int j = 1; j <= m; ++j) { if(check(i, j)) vt[i].push_back(j); } } } bool path(int u) { for (int i = 0; i < (int)vt[u].size(); ++i) { int j = vt[u][i]; if (vis[j]) continue; vis[j] = 1; if (!match[j] || path(match[j])) { match[j] = u; return true; } } return false; } int main() { while (scanf("%d %d", &n, &m), n|m) { for (int i = 1; i <= n; ++i) { vt[i].clear(); scanf("%d %d", &h[i].x, &h[i].y); } for (int i = 1; i <= m; ++i) { scanf("%d %d", &v[i].x, &v[i].y); } build(); memset(match, 0, sizeof (match)); int cnt = 0; for (int i = 1; i <= n; ++i) { memset(vis, 0, sizeof (vis)); if (path(i)) ++cnt; } printf("%d\n", n+m-cnt); } return 0; }
HDU-4620 Fruit Ninja Extreme
题意:给定了N组切水果的方案,现在定义如果一次切除中有三个或以上的水果被切除那么这一次切除就会带来红利。有最多200种水果,一种水果如果被切除过一次,那么在另外一次切除中将不存在。定义一个时间窗口W,当产生红利的切除连续出现,且两两之间的时间间隔不超过W,那么定义这样的切除为连续的红利切除。题目问给定的方案中如何安排会出现最多的连续红利切除。数据将给出每个切除所发生的时间以及水果的种类。
分析:直接搜索即可,注意先搜索切再搜索不切,这样会比较快的找的最优解来进行一个上界剪枝,下界通过相邻的时间间隔进行。
#include <cstdlib> #include <cstring> #include <cstdio> #include <iostream> #include <algorithm> #include <vector> using namespace std; const int N = 35; const int M = 205; int n, m, w, Max; char vis[M]; int rank[N], ti[N], path[N]; vector<int>ret, v[N]; inline bool cmp(int x, int y) { return ti[x] < ti[y]; } void dfs(int rk, int idx, int last) { if (rk > n) return; if (n-rk+idx <= Max) return; int u = rank[rk]; if (ti[u] - last > w && idx > 1) return; vector<int>vt; for (int i = 0; i < (int)v[u].size(); ++i) { if (!vis[v[u][i]]) vt.push_back(v[u][i]); } if (vt.size() >= 3) { for (int i = 0; i < vt.size(); ++i) { vis[vt[i]] = 1; } path[idx] = u; if (idx > Max) { Max = idx; ret.clear(); for (int i = 1; i <= idx; ++i) { ret.push_back(path[i]); } } dfs(rk+1, idx+1, ti[u]); for (int i = 0; i < vt.size(); ++i) { vis[vt[i]] = 0; } } dfs(rk+1, idx, last); } int main() { int T; scanf("%d", &T); while (T--) { scanf("%d %d %d", &n, &m, &w); int x, y; Max = 0; memset(vis, 0, sizeof (vis)); for (int i = 1; i <= n; ++i) { rank[i] = i; v[i].clear(); scanf("%d %d", &x, &ti[i]); for (int j = 0; j < x; ++j) { scanf("%d", &y); v[i].push_back(y); } } sort(rank+1, rank+1+n, cmp); dfs(1, 1, 0); sort(ret.begin(), ret.end()); printf("%d\n", ret.size()); for (int i = 0; i < (int)ret.size(); ++i) { printf(i == 0 ? "%d" : " %d", ret[i]); } puts(""); } return 0; }