29-30 考试总结
来源:\(COCI2011\)~\(2012\) \(test1\)-\(test2\)部分题目。
T1 ples
题目大意
\(N\)个男生和\(N\)个女生参加舞会,跳舞时只能是一个男生一个女生,我们知道每个人的身高,同时某个男生只想和比他高(或者矮)的女生跳(有人跟你跳还挑三拣四的?),女生也是一样的,身高一样的人都不想与对方跳。给出所有数据,输出满足人们希望的前提下组成的最多对数。
分析
开始拿到题看了看,哦?二分图最大匹配板子?
直到看到了浩瀚无边的数据范围。。。正当心中默念"哦豁"时,看到了很小的值域范围。。。
人生大起大落真刺激
恩,带权二分图最大匹配,因为值域很小,我们可以把每个值看做点,人数看做点权,按题目要求连边,男生女生分别连向源点和汇点,然后就是跑最大流了。
因为内存限制,所以要精确算开多少条边。
代码
#include<queue>
#include<cstdio>
#include<cstdlib>
#include<cstring>
const int BASE = 1500;
const int INF = 0x3f3f3f3f;
inline int read(){
int f = 1, x = 0; char ch;
do { ch = getchar(); if (ch == '-') f = -1; } while (ch < '0' || ch > '9');
do {x = (x << 3) + (x << 1) + ch - '0'; ch = getchar(); } while (ch >= '0' && ch <= '9');
return f * x;
}
inline void hand_in() {
freopen("ples.in", "r", stdin);
freopen("ples.out", "w", stdout);
}
int n, s, t, maxflow;
struct Node{ int l, r; }A[1005], B[1005];
inline int min(int a, int b) { return a < b ? a : b; }
struct Sakura { int to, nxt, w; }sak[2200005]; int head[4010], cnt = 1;
inline void add(int x, int y, int w) {
++cnt;
sak[cnt].to = y, sak[cnt].w = w, sak[cnt].nxt = head[x], head[x] = cnt;
++cnt;
sak[cnt].to = x, sak[cnt].w = 0, sak[cnt].nxt = head[y], head[y] = cnt;
// printf("%d -> %d : %d\n", x, y, w);
}
int dep[4010];
inline bool bfs() {
std :: queue<int> q;
memset(dep, 0, sizeof dep);
dep[s] = 1, q.push(s);
while(!q.empty()) {
int u = q.front();
q.pop();
for (int i = head[u];i;i =sak[i].nxt) {
int v = sak[i].to, w = sak[i].w;
if (!dep[v] && w) {
dep[v] = dep[u] + 1;
if (v == t) return 1;
q.push(v);
}
}
}
return 0;
}
inline int dfs(int u, int flow) {
if (u == t) return flow;
int rest = flow, rlow;
for (int i = head[u];i && rest;i = sak[i].nxt) {
int v = sak[i].to, w = sak[i].w;
if (w && dep[v] == dep[u] + 1) {
rlow = dfs(v, min(rest, w));
if (!rlow) dep[v] = 0;
sak[i].w -= rlow;
sak[i ^ 1].w += rlow;
rest -= rlow;
}
}
return flow - rest;
}
inline int Dinic() {
int lowflow;
while (bfs()) {
while(lowflow = dfs(s, INF)) maxflow += lowflow;
}
return maxflow;
}
int main(){
hand_in();
n = read();
s = 4004, t = 4005;
for (int i = 1, x;i <= n; ++i) {
x = read();
if (x < 0) A[- x - BASE].l ++;
else A[x - BASE].r ++;
}
for (int i = 1, x;i <= n; ++i) {
x = read();
if (x < 0) B[- x - BASE].l ++;
else B[x - BASE].r ++;
}
/* A.l 0~1000 A.r 1001~2001 B.l 2002~3002 B.r 3003~4003 */
/* S --- A.l / A.r 连源点 */
for (int i = 0;i <= 1000; ++i) {
if (A[i].l) add(s, i, A[i].l);
if (A[i].r) add(s, i + 1001, A[i].r);
}
/* A.l --- B.r 男高女矮 */
for (int i = 1;i <= 1000; ++i) {
for (int j = 0;j < i; ++j) {
if (A[i].l && B[j].r) add(i, j + 3003, INF);
}
}
/* A.r --- B.l 男矮女高 */
for (int i = 0;i < 1000; ++i) {
for (int j = i + 1;j <= 1000; ++j) {
if (A[i].r && B[j].l) add(i + 1001, j + 2002, INF);
}
}
/* B.l / B.r --- T 连汇点 */
for (int j = 0;j <= 1000; ++j) {
if (B[j].l) add(j + 2002, t, B[j].l);
if (B[j].r) add(j + 3003, t, B[j].r);
}
printf("%d", Dinic());
return 0;
}
排序+贪心是正解???
好吧,的确是的。
我们可以发现,排序后让正数与负数匹配,用指针一直向后移动,那么会让后面的情况达到最优。
T2 sort
题目大意
对一个序列使用\(reverse\)函数排成升序,每次\(reverse\)序列中所有连续单调下降子序列,保证在第一次排序后,所有的连续单调下降子序列的长度都是偶数。求\(reverse\)函数的使用次数。
分析
30pt做法
直接用\(reverse\)函数模拟,你甚至可以自己手写。
100pt做法
首先,有个结论,在第一次对序列执行这种方法的排序后,序列中所有的连续单调下降子序列的长度要么是\(3\)要么是\(2\)(\(1\)排除在外)。
证明:考虑相邻的两个连续单调下降子序列,有两种情况:它们中间没有间隔;它们中间有一个元素的间隔。对于第一种情况,\(reverse\)这两个子序列后,只会在它们俩的交界处形成一个长度为\(2\)的单调下降序列;对于第二种情况,同理,会形成一个长度为\(3\)的单调下降序列。
又因为题目保证都为偶数,所以在第一次排序后,所有的连续单调下降子序列的长度都是\(2\)。
这有什么性质?在之后我们每次\(reverse\),就会让一个数的前面比它大的数减\(1\),又因为递增序列所有数都比它前面的数大,所以联想到求逆序对。
所以这道题的解法就是先模拟排序一遍该序列,累加答案,然后计算当前序列的逆序对数,累加进答案里面。
代码
#include<cstdio>
#include<cstdlib>
#include<algorithm>
#define ll long long
const int N = 100000 + 5;
using std :: reverse;
inline int read(){
int f = 1, x = 0; char ch;
do { ch = getchar(); if (ch == '-') f = -1; } while (ch < '0' || ch > '9');
do {x = (x << 3) + (x << 1) + ch - '0'; ch = getchar(); } while (ch >= '0' && ch <= '9');
return f * x;
}
inline void hand_in() {
freopen("sort.in", "r", stdin);
freopen("sort.out", "w", stdout);
}
int n, a[N];
ll ans;
inline bool judge() {
for (int i = 2;i <= n; ++i) {
if (a[i] < a[i - 1]) return 1;
}
return 0;
}
int c[N];
inline int lowbit(int x) {
return x & (-x);
}
inline void add(int x) {
for (int i = x;i <= n; i += lowbit(i)) c[i] ++;
}
inline int ask(int x) {
int res = 0;
if (!x) return 0;
for (int i = x;i;i -= lowbit(i)) res += c[i];
return res;
}
inline void solve() {
int st = 1, ed = 1, len = 1;
for (int i = 2;i <= n; ++i) {
if (a[i] < a[i - 1]) {
ed = i, len ++;
}
else {
if (len > 1) {
reverse(a + st, a + ed + 1);
ans ++;
}
st = i, ed = i, len = 1;
}
}
if (len > 1) {
reverse(a + st, a + ed + 1);
ans ++;
}
for (int i = n;i >= 1; --i) {
ans += ask(a[i] - 1);
add(a[i]);
}
}
int main(){
hand_in();
n = read();
for (int i = 1;i <= n; ++i) a[i] = read();
solve();
printf("%lld", ans);
return 0;
}
T3 skakac(To be continue)
题目大意
略略略~
分析
20pt做法
也许直接\(dfs\)?
30~50pt做法
设\(f[i][j][k]\)表示当前时间为\(i\),棋盘位置为\((j,k)\)是否到达的状态,然后就可以枚举\(i-1\)的状态转移过来,用个滚动数组优化下,空间复杂度\(O(n^2)\),时间复杂度\(O(Tn^2)\)。
同时,也可以上\(BFS\)。
60~100pt做法
鬼知道数据有多恐怖,貌似正确的复杂度只有\(70pt\)???
考虑压缩每一行的状态,然后就可以\(O(Tn)\)转移。
难就难在处理图上的\(K_{i,j}\)。
为了保证时间复杂度和空间复杂度,对\(K_{i,j}\)分开处理。
如果\(K_{i,j} > 1000\)那么把在\(T\)以内的\(K_{i,j}\)的倍数记录下来,当\(T\)达到这个值的时候再处理。 如果\(K_{i,j}\le 1000\),可以把\(K_{i,j}\)分解质因数,设=\(K_{i,j}=p_1^{c_1}p_2^{c_2}...p_k^{c_k}\),那么当\(T\)能同时整除\(_1^{c_1}p_2^{c_2}...p_k^{c_k}\) 时当前\((i,j)\)就可到达。
令\(g[i][j][k]\)表示第k行能整除\(p_i^j\)时的情况,那么把\(T\)也分解质因数,棋盘的情况就可以表示为\(g[1][c_1] \& g[2][c_2] ... g[k][c_k]\)。
当然次数为\(0\)的也要考虑进去,为了节省时间可以令\(h[i][j]=g[i][0]\&g[i+1][0]...g[j][0]\)。
然后就可以预处理出所有状态下图对应的状态,也许写得有点问题,数据点过不全。
代码
#include<vector>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<algorithm>
#define ll long long
#define Re register
const int P = 1000000007;
const int BASE = 1e6;
const int N = 30 + 5;
using std :: vector;
using std :: sort;
inline void hand_in() {
freopen("skakac.in", "r", stdin);
freopen("skakac.out", "w", stdout);
}
inline int read(){
int f = 1, x = 0; char ch;
do { ch = getchar(); if (ch == '-') f = -1; } while (ch < '0' || ch > '9');
do {x = (x << 3) + (x << 1) + ch - '0'; ch = getchar(); } while (ch >= '0' && ch <= '9');
return f * x;
}
int n, t, st_x, st_y;
int mp[N][N];
struct Node {
int t, x, y;
Node (int a = 0, int b = 0, int c = 0) : t(a), x(b), y(c) {}
friend bool operator < (Node a, Node b) { return a.t < b.t; }
}ans[N * N], rate[BASE + 5]; int oo, pp;
/* 预处理质数 */
int prim[BASE + 5], vis[BASE + 5], tot, ps;
inline void init() {
for (Re int i = 2;i <= BASE; ++i) {
if (!vis[i]) {
prim[++tot] = i, vis[i] = tot;
if (i <= 1000) ps = tot; /* 小优化:记录下1000以内的质数到了哪里 */
}
for (Re int j = 1;j <= tot; ++j) {
if ((ll)i * (ll)prim[j] > BASE) break;
vis[i * prim[j]] = j;
if (i % prim[j] == 0) break;
}
}
}
/* 对(x,y)上的k值进行质因数分解 */
/*
h: h[i][j][k]表示
g: g[i][j][k]表示第k行能整除pi^j时的情况
*/
int h[250][250][35], g[250][25][35];
inline void divide(int k, int x, int y) {
for (Re int i = 1;i <= ps; ++i) {
int ret = 0;
while (k % prim[i] == 0) ret ++, k /= prim[i];
/* 既然x^ret能够到达,所以高于ret的次幂都行,最多达到2^20 */
for (;ret <= 20; ++ret) g[i][ret][x] |= (1 << y);
}
}
/* 质因数分解 */
struct Divide {
int pr, nm;
friend bool operator < (Divide a, Divide b) { return a.pr < b.pr; }
}dv[BASE + 5]; int w[35];
inline void work(int x) {
int ct = 0, pos = 1;
/* 会超时??? */
// for (int i = 1, ret;prim[i] <= x; ++i) {
// if (x % prim[i]) continue;
// ret = 0;
// while (x % prim[i] == 0) ret ++, x /= prim[i];
// if (ret) dv[++ct].pr = i, dv[ct].nm = ret;
// }
while (x ^ 1) {
int ret = 0, op = vis[x];
while (x % prim[op] == 0) ret ++, x /= prim[op];
dv[++ct].pr = op, dv[ct].nm = ret;
}
sort(dv + 1, dv + 1 + ct);
for (int i = 1;i <= ct && dv[i].pr <= ps; ++i) {
for (int j = 1;j <= n; ++j) {
w[j] &= g[dv[i].pr][dv[i].nm][j];
}
if (pos < dv[i].pr) {
for (int j = 1;j <= n; ++j) {
w[j] &= h[pos][dv[i].pr - 1][j];
}
}
pos = dv[i].pr + 1;
}
if (pos < ps) for (int i = 1;i <= n; ++i) w[i] &= h[pos][ps][i];
}
int f[2][35], now, pre;
int main(){
hand_in();
/* 读入 */
n = read(), t = read(), st_x = read(), st_y = read();
/* 预处理出质数 */
init();
/* 对地图分类处理:1000以上用倍数,反之分解它 */
for (Re int i = 1;i <= n; ++i) {
for (Re int j = 1;j <= n; ++j) {
mp[i][j] = read();
if (mp[i][j] >= 1500) {
for (Re int k = 1;k * mp[i][j] <= t; ++k) rate[++pp] = Node(k * mp[i][j], i, j);
}
else {
divide(mp[i][j], i, j);
}
}
}
/* 排序后可以依次考虑 */
sort(rate + 1, rate + 1 + pp);
for (Re int i = 1;i <= ps; ++i) {
for (Re int j = i;j <= ps; ++j) {
for (Re int s = 1;s <= n; ++s) {
/* 初始化全为1 */
h[i][j][s] = (1 << (n + 1)) - 1;
for (Re int k = i;k <= j; ++k) {
h[i][j][s] &= g[k][0][s];
}
}
}
}
f[0][st_x] |= (1 << st_y);
for (Re int i = 1, p = 1;i <= t; ++i) {
now ^= 1, pre = now ^ 1;
for (Re int j = 1;j <= n; ++j) f[now][j] = 0, w[j] = (1 << (n + 1)) - 1;
for (Re int j = 1;j <= n; ++j) {
if (j > 1) f[now][j] |= (f[pre][j - 1] >> 2) | (f[pre][j - 1] << 2);
if (j < n) f[now][j] |= (f[pre][j + 1] >> 2) | (f[pre][j + 1] << 2);
if (j > 2) f[now][j] |= (f[pre][j - 2] >> 1) | (f[pre][j - 2] << 1);
if (j < n - 1) f[now][j] |= (f[pre][j + 2] >> 1) | (f[pre][j + 2] << 1);
}
work(i);
while (p <= pp && rate[p].t == i) w[rate[p].x] |= (1 << rate[p].y), ++p;
for (Re int j = 1;j <= n; ++j) f[now][j] &= w[j];
}
for (Re int i = 1;i <= n; ++i) {
for (Re int j = 1;j <= n; ++j) {
if (f[now][i] & (1 << j)) ans[++oo].x = i, ans[oo].y = j;
}
}
printf("%d\n", oo);
for (Re int i = 1;i <= oo; ++i) {
printf("%d %d\n", ans[i].x, ans[i].y);
}
return 0;
}
T4 kom
题目大意
给出\(N\)个互不相同的正整数,统计共有多少对数,它们有公共的一个数字(不一定在同一位置上)。
分析
20pt做法
无脑暴力。
60pt做法
假的容斥。
考试时最先想到这个方法。
对每个数用二进制状态存下来\(O(n)\)扫描,然后对1024种状态容斥一下,就是\(O(n\times 1024)\)。
貌似这种方法用bitset优化下就能过?
100pt做法
我们不需要知道所有数,只需要知道某个数出现了哪些数字,思考压缩状态,即只用出现了哪些数字来表示一个数,又因为数字只有\(0\)~\(9\),所以整个状态总量就是\(2^{10}=1024\)。然后我们就可以枚举压缩后的状态啦,记得不能算自身,所以要减去自己与自己成对的情况。
代码
#include<cstdio>
#include<cstdlib>
#include<cstring>
#define ll long long
inline void hand_in() {
freopen("kom.in", "r", stdin);
freopen("kom.out", "w", stdout);
}
int n, len, v, pan[1025];
char ch[20]; ll ret;
int main(){
hand_in();
scanf("%d", &n), v = (1 << 10);
for (int i = 1, a;i <= n; ++i) {
scanf("%s", ch + 1);
len = strlen(ch + 1), a = 0;
for (int j = 1;j <= len; ++j) {
a |= (1 << (ch[j] - '0'));
}
pan[a] ++;
}
for (int i = 0;i < v; ++i) {
for (int j = 0;j < v; ++j) {
if (i & j) {
ret += (ll)pan[i] * (ll)pan[j];
if (i == j) ret -= (ll)pan[i];
}
}
}
printf("%lld\n", ret >> 1);
return 0;
}
T5 fun
题目大意
略略略~
分析
30pt做法
直接按照那个函数用\(dfs\)模拟即可。
100pt做法
我们可以发现,若一层循环中上下界为常数项,我们可以把它换在任何位置,对答案没有任何影响,只需要在统计答案时乘上这层循环的循环次数即可。
推广一下,当我们确定一个字母变量的值的时候,那么所有依赖于它的变量循环都可变为常数循环,我们任意交换是没有问题的。根据依赖关系,我们会发现是由一棵棵树组成的森林,每棵树相互之间是不会受到影响的,所以直接把每棵树相乘即为最终答案。
对于每棵树,我们从上往下遍历,就能依次确定各个变量的取值范围,然后将子树的值相乘,再上传就是答案。
用记忆化优化下复杂度。
代码
#include<cstdio>
#include<cstdlib>
#include<cstring>
#define ll long long
const int P = 1000000007;
const int N = 30;
inline void hand_in() {
freopen("fun.in", "r", stdin);
freopen("fun.out", "w", stdout);
}
inline bool is_num(char *s) { if (s[1] < '0' || s[1] > '9') return 0; return 1; }
int cnt, to[N << 1], nxt[N << 1], head[N]; inline void add(int x, int y) { ++cnt; to[cnt] = y, nxt[cnt] = head[x], head[x] = cnt; }
inline int change_num(char *s) { int l = strlen(s + 1); int res = 0; for (int i = 1;i <= l; ++i) { res = res * 10 + s[i] - '0'; } return res; }
ll dfs(int, int);
ll f[N][100005], ans = 1;
int n, l[N], r[N], rate[N];
char ls[10], rs[10];
inline ll find(int now, int lim) {
ll res = 1;
int p = head[now], nt;
while (p) {
nt = to[p];
res *= dfs(nt, lim) % P;
p = nxt[p];
}
return res;
}
inline ll dfs(int now, int lim) {
if (~f[now][lim]) return f[now][lim];
int last = lim;
if (rate[now] == 0) {
while (last <= r[now] && f[now][last] == -1) last ++;
if (last > r[now]) f[now][last] = 0;
last --;
while (last >= lim) {
f[now][last] = (f[now][last + 1] + find(now, last)) % P;
last --;
}
}
else if (rate[now] == 1) {
while (last >= l[now] && f[now][last] == -1) last --;
if (last < l[now]) f[now][last] = 0;
last ++;
while (last <= lim) {
f[now][last] = (f[now][last - 1] + find(now, last)) % P;
last ++;
}
}
else {
f[now][lim] = 0;
for (int i = l[now];i <= r[now]; ++i) {
f[now][i] = find(now, i);
f[now][lim] = (f[now][i] + f[now][lim]) % P;
}
}
return f[now][lim];
}
int main(){
// hand_in();
scanf("%d", &n);
memset(f, -1, sizeof f);
for (int i = 0;i < n; ++i) {
scanf("%s %s", ls + 1, rs + 1);
rate[i] = -1;
if (is_num(ls)) {
l[i] = change_num(ls);
}
else {
rate[i] = 0;
l[i] = ls[1] - 'a';
add(l[i], i);
}
if (is_num(rs)) {
r[i] = change_num(rs);
}
else {
rate[i] = 1;
r[i] = rs[1] - 'a';
add(r[i], i);
}
}
for (int i = 0;i < n; ++i) {
if (rate[i] == -1) {
ans = ans * dfs(i, 0) % P;
}
}
printf("%lld", ans);
return 0;
}
T6 ras
题目大意
略略略~
分析
30pt做法
把计算答案的式子列出来后会发现可以贪心。
即把序列按\(t\)的升序排序,然后直接计算答案。
对于每次修改,暴力修改,再重新排遍序,再计算答案。
100pt做法
思路大体没错,算法的瓶颈在于修改,也就是动态维护前缀和。
考虑使用数据结构权值线段树或权值树状数组。
可以把维护前缀和拆成两个操作,清除某位上的一个数,把一个数插入到某位上。
在操作的同时对\(ans\)进行维护。
注意需要开\(ll\)。
代码
#include<cstdio>
#include<cstdlib>
#include<algorithm>
#define ll long long
using std :: sort;
const int N = 100000 + 5;
inline int read(){
int f = 1, x = 0; char ch;
do { ch = getchar(); if (ch == '-') f = -1; } while (ch < '0' || ch > '9');
do {x = (x << 3) + (x << 1) + ch - '0'; ch = getchar(); } while (ch >= '0' && ch <= '9');
return f * x;
}
inline void hand_in() {
freopen("ras.in", "r", stdin);
freopen("ras.out", "w", stdout);
}
int n, c; ll ans, s;
struct Data { int l, t; }mk[200005], rsd[200005];
inline bool cmp(const Data &a, const Data &b) { return a.t < b.t; }
struct Segment_Tree {
struct Node {
int l, r;
ll x, t;
}tr[N << 2];
#define ls (p << 1)
#define rs ((p << 1) | 1)
inline void build(int p, int l, int r) {
tr[p].l = l, tr[p].r = r;
if (l == r) return;
int mid = (l + r) >> 1;
build(ls, l, mid), build(rs, mid + 1, r);
}
inline void change(int p, int x, int a, int b) {
tr[p].x += a, tr[p].t += b;
if (tr[p].l == tr[p].r) return;
int mid = (tr[p].l + tr[p].r) >> 1;
if (x <= mid) change(ls, x, a, b);
else change(rs, x, a, b);
}
inline ll ask_x(int p, int l, int r) {
if (l <= tr[p].l && tr[p].r <= r) {
return tr[p].x;
}
int mid = (tr[p].l + tr[p].r) >> 1;
ll res = 0;
if (l <= mid) res += ask_x(ls, l, r);
if (r > mid) res += ask_x(rs, l, r);
return res;
}
inline ll ask_t(int p, int l, int r) {
if (l <= tr[p].l && tr[p].r <= r) {
return tr[p].t;
}
int mid = (tr[p].l + tr[p].r) >> 1;
ll res = 0;
if (l <= mid) res += ask_t(ls, l, r);
if (r > mid) res += ask_t(rs, l, r);
return res;
}
}st;
int main(){
hand_in();
n = read(), c = read();
for (int i = 1;i <= n; ++i) {
mk[i].l = read(), mk[i].t = read();
rsd[i] = mk[i], ans += mk[i].l;
}
st.build(1, 1, 100001);
sort(mk + 1, mk + 1 + n, cmp);
for (int i = 1;i <= n; ++i) {
s += mk[i].t;
ans -= s;
st.change(1, mk[i].t, mk[i].t, 1);
}
printf("%lld\n", ans);
for (int i = 1, id, l, t;i <= c; ++i) {
id = read(), l = read(), t = read();
ans -= rsd[id].l - st.ask_x(1, 1, rsd[id].t) - st.ask_t(1, rsd[id].t + 1, 100001) * rsd[id].t;
st.change(1, rsd[id].t, -rsd[id].t, -1);
rsd[id].l = l, rsd[id].t = t;
st.change(1, rsd[id].t, rsd[id].t, 1);
ans += rsd[id].l - st.ask_x(1, 1, rsd[id].t) - st.ask_t(1, rsd[id].t + 1, 100001) * rsd[id].t;
printf("%lld\n", ans);
}
return 0;
}