2019 ICPC Asia Nanchang Regional
A.9102(待补)
可以参考这篇博客戳我~
B.A Funny Bipartite Graph(状压dp)
题意:
给定一个二分图,左右均有个点,左边的点有个贡献
。左边的每个点度数至少为
至多为
,且左边每个点只会连向右边编号大于等于它的点。现在你要选择一些边,限制如下:
- 右边的每一个点都要被覆盖到;
- 有一个
矩阵
,若
则表示左边第
个点和第
个点不能同时被覆盖到;
对于左边的每一个点,如果它没被覆盖到则代价为,否则代价为
,其中
表示被它覆盖了多少次。满足上面两条的情况下要使得代价最小。求最小代价,或输出无解。
其中
题解:
观察到的数据范围,那么可以考虑用状压dp。又发现题目中给出左边每个点只会连向右边编号大于等于它的点。那么从左边第一个节点开始依次选择连边,当前选择点
的时候,右边编号为
的点肯定都被覆盖了,而左边编号为
的点肯定都还没选择。那么这两个部分的二进制长度加起来刚好是
。
所以从左边第一个节点开始依次选择连边进行状态转移,为选择连边时,
的
位代表前面选择的左边的点,
位表示已经选择了的右边的点。然后枚举点
连边情况进行转移即可。
因为下一个的节点状态只需要从上一次的节点状态转移,所以可以用滚动数组进行优化
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int, int> pii;
const int maxn = (1 << 18) + 5;
const int INF = 0x3f3f3f3f;
const int mod = 998244353;
int dp[2][maxn];
int val[20], ban[20];
vector<int> G[20];
char s[20];
int n;
void solve()
{
//初始化
scanf("%d", &n);
for (int i = 0; i < n; i++)
{
G[i].clear();
scanf("%s", s);
for (int j = 0; j < n; j++)
if (s[j] == '1')
G[i].push_back(j);
}
for (int i = 0; i < n; i++)
{
scanf("%s", s);
ban[i] = 0;
for (int j = 0; j < i; j++)
if (s[j] == '1')
ban[i] |= (1 << j);
}
for (int i = 0; i < n; i++)
scanf("%d", &val[i]);
//状态转移
int cur = 0, nxt = 1; //用于滚动数组
memset(dp, INF, sizeof(dp));
dp[cur][0] = 0;
for (int i = 0; i < n; i++)
{
for (int mask = 0; mask < (1 << n); mask++)
{
int lstate = mask & ((1 << i) - 1); //代表选择的左边节点
int rstate = mask & ((1 << n) - (1 << i)); //代表已选的右边节点
if (dp[cur][mask] == INF) //如果右边节点的[1,i-1]未被覆盖着直接跳过
continue;
if ((rstate) >> i & 1) //如果右边节点i已选则考虑是否不用加入左边节点i
dp[nxt][mask ^ (1 << i)] = min(dp[nxt][mask ^ (1 << i)], dp[cur][mask]);
if (ban[i] & lstate) //如果i节点与已选的左边节点冲突则跳过
continue;
for (int t = 1; t < (1 << G[i].size()); t++) //依次枚举与左边节点相连的右边节点
{
int cost = 1;
int tmp = 0;
for (int j = 0; j < G[i].size(); j++)
{
int v = G[i][j];
if ((t >> j) & 1)
{
cost *= val[i];
tmp |= (1 << v);
}
}
int nowstate = rstate | tmp;
if (!((nowstate >> i) & 1)) //如果没有覆盖到右边节点i则这个选取不行
continue;
dp[nxt][lstate | nowstate] = min(dp[nxt][lstate | nowstate], dp[cur][mask] + cost);
}
}
swap(cur, nxt);
memset(dp[nxt], INF, sizeof(dp[nxt]));
}
int ans = INF;
for (int i = 0; i < (1 << n); i++)
ans = min(ans, dp[cur][i]);
if (ans == INF)
puts("-1");
else
printf("%d\n", ans);
}
int main()
{
int t;
for (scanf("%d", &t); t >= 1; t--)
solve();
} C.And and Pair(组合数学)
题意:
给定一个二进制表示的n,让你找满足如下要求的数对的个数
题解:
遍历一遍,设从低位到最高位的
之间有
个
,可选择的
就是
种(
为
的位置
可以选
,
为
的位置
为
),从低位向高位枚举每个数字,当遇到
时,让他做最高位的
,设这个
的低位中有
个
和
个
,简单推导得到有
种方案。
要注意这种方法只算了不为
的情况,当
时也成立,所以结果要加上
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int mod = 1e9 + 7;
const int maxn = 1e5 + 10;
ll num2[maxn];
ll num3[maxn];
char a[maxn];
int main()
{
ll t;
scanf("%lld", &t);
getchar();
num2[0] = num3[0] = 1;
for (int i = 1; i < maxn; i++)
{
num2[i] = (num2[i - 1] * 2) % mod;
num3[i] = (num3[i - 1] * 3) % mod;
}
while (t--)
{
scanf("%s", a);
ll len = strlen(a);
ll y, x;
y = x = 0;
ll num = 0;
for (ll i = len - 1; i >= 0; i--)
{
if (a[i] == '1')
{
num = (num + (num2[x] * num3[y]) % mod) % mod;
y++;
}
else
x++;
}
num = (num + 1) % mod;
printf("%lld\n", num);
}
return 0;
} E.Bob's Problem(最大生成树)
题意:
一张包含个点和
条边的图,边分为黑边和白边(
为黑边,
为白边),每条边有各自的权值
,其中黑边任意选,白边只能选
条,在保持整张图连通的情况下使得所选变的权值和最大,求最大的权值和
题解:
将所有的黑边都加入,白边按权值排序做一颗最大生成树即可,判断连完后是否是一整个连通块,如果连完仍有剩余则贪心加入权值较大的白边即可
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 5e5 + 5;
int n, m, k, cnt, fa[maxn];
ll ans;
bool vis[maxn];
struct node
{
int u, v, w;
node(int u0 = 0, int v0 = 0, int w0 = 0) : u(u0), v(v0), w(w0) {}
bool operator<(const node &C) const
{
return w > C.w;
}
};
vector<node> v;
int find(int x) { return fa[x] == x ? x : fa[x] = find(fa[x]); }
void merge(int x, int y)
{
int a = find(x), b = find(y);
if (a != b)
fa[a] = b, cnt++;
}
void init()
{
v.clear();
ans = 0, cnt = 0;
for (int i = 1; i <= n; i++)
fa[i] = i;
memset(vis, 0, sizeof(vis));
}
int main()
{
int t;
scanf("%d", &t);
while (t--)
{
scanf("%d%d%d", &n, &m, &k);
init();
for (int i = 1; i <= m; i++)
{
int x, y, w, c;
scanf("%d%d%d%d", &x, &y, &w, &c);
if (!c)
ans += w, merge(x, y);
else
v.push_back(node(x, y, w));
}
priority_queue<int> q;
sort(v.begin(), v.end());
for (int i = 0; i < v.size() && k; i++)
{
int a = find(v[i].u), b = find(v[i].v);
if (a != b)
{
vis[i] = 1;
cnt++, k--;
ans += v[i].w;
fa[a] = b;
}
}
if (cnt != n - 1)
{
printf("%d\n", -1);
continue;
}
for (int i = 0; i < v.size() && k; i++)
if (!vis[i])
ans += v[i].w, k--;
printf("%lld\n", ans);
}
return 0;
} G.Eating Plan(思维)
题意:
给出个数
和
个查询,求最小的区间长度,使得
题解:
要求对每个数的阶乘取模,所以打表可以发现当后,阶乘就为
。
那么我们只要求出前个数的阶乘即可,然后统计出每个区间长度所得的最大值与询问比较即可
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int, int> pii;
const int maxn = 1e5 + 5;
const int INF = 0x3f3f3f3f;
const int mod = 998857459;
ll jc[3005];
int len[maxn];
struct node
{
int v, pos;
} a[maxn];
int main()
{
int n, m;
scanf("%d%d", &n, &m);
jc[0] = jc[1] = 1;
for (int i = 2; i <= 2802; i++)
jc[i] = jc[i - 1] * i % mod;
int cnt = 0;
for (int i = 0, x; i < n; i++)
{
scanf("%d", &x);
if (x <= 2802)
{
a[cnt].v = jc[x];
a[cnt++].pos = i;
}
}
for (int i = 0; i < cnt; i++)
{
int t = 0;
for (int j = i; j < cnt; j++)
{
t = (t + a[j].v) % mod;
len[a[j].pos - a[i].pos + 1] = max(len[a[j].pos - a[i].pos + 1], t);
}
}
while (m--)
{
int x;
scanf("%d", &x);
int ans = -1;
for (int i = 1; i < maxn; i++)
if (len[i] >= x)
{
ans = i;
break;
}
printf("%d\n", ans);
}
return 0;
} J.Summon(polay定理+矩阵快速幂)
题意:
长度为的环,填四种颜色,有
个要求
,每个要求限制一个长度为
的序列XXXX不许在环中出现,问有多少合法方案。(通过旋转可以变相同的算同一种方案)
题解:
首先根据polay定理:,
为方案数,
为置换数,
为当前置换的循环数量。可以求出无限制的总方案数,但是这道题还有配色限制,所以单单用polay定理还无法解决
考虑置换群。我们知道对于一个置换
,循环节数量为
,而且第一个元素在第一个循环中,第二个元素在第二个循环中,...,第
个元素在第
个循环中,而且每个循环中的元素颜色相同。
我们可以用一个矩阵表示
两色可以相邻,否则不行。
然后我们可以写一个dp方程,设表示处理到前
位最后一位颜色是
的答案,转移方程:
因为是一个环,所以还要保证最后一位的颜色和第一位的颜色满足条件,同时每个循环节内的颜色相同,所以我们可以取:
现在考虑四种颜色,对于一个四元组,如果第一位是
且后面跟着
,那么第二位就不能是
且后面跟着
。可以原先将
中的
都拓展为
表示一种前三个颜色组成的
和后三个颜色组成的
方案能否可行。
可知,如果用矩阵表示无向图连通情况的话,
次方代表的就是一个点经过
次转移,后
点到
点的方案数
又由于我们要起始元素颜色和终止元素颜色相同,就是求次转移后矩阵对角线为
的总和。
方案限制的问题解决以后,因为置换数有种,每个算复杂度太高,考虑到最终所求为
,所以可以等价于求
的所有约数
,对于每个
的
个数就是
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int mod = 998244353;
const int maxn = 1e5 + 5;
int primes[maxn], cnt;
int phi[maxn];
bool v[maxn];
void init(int n)
{
phi[1] = 1;
for (int i = 2; i <= n; i++)
{
if (!v[i])
{
primes[++cnt] = i;
phi[i] = i - 1;
}
for (int j = 1; primes[j] <= n / i; j++)
{
v[primes[j] * i] = 1;
if (i % primes[j] == 0)
{
phi[primes[j] * i] = phi[i] * primes[j];
break;
}
else
phi[primes[j] * i] = phi[i] * (primes[j] - 1);
}
}
}
struct mat
{
ll a[67][67];
} base, pbase[25];
mat mul(mat a, mat b)
{
mat res;
memset(res.a, 0, sizeof res.a);
for (int i = 0; i < 64; i++)
for (int j = 0; j < 64; j++)
for (int k = 0; k < 64; k++)
res.a[i][j] = (res.a[i][j] % mod + a.a[i][k] % mod * b.a[k][j] % mod) % mod;
return res;
}
ll ksm(ll a, ll b)
{
ll res = 1;
while (b)
{
if (b & 1)
res = (res * a) % mod;
a = a * a % mod;
b >>= 1;
}
return res % mod;
}
mat qmi(int b)
{
mat res;
memset(res.a, 0, sizeof res.a);
for (int i = 0; i < 64; i++)
res.a[i][i] = 1;
int t = 0;
while (b)
{
if (b & 1)
res = mul(res, pbase[t]);
b >>= 1;
t++;
}
return res;
}
int main()
{
init(maxn - 3);
int n, m;
scanf("%d%d", &n, &m);
for (int i = 0; i < 64; i++)
for (int k = 0; k < 4; k++)
{
int nxt = (i * 4 + k) % 64;
base.a[i][nxt] = 1;
}
while (m--)
{
int x = 0, t;
for (int i = 0; i < 4; i++)
{
scanf("%d", &t);
x = x * 4 + t;
}
base.a[x / 4][x % 64] = 0;
}
pbase[0] = base;
for (int i = 1; i < 20; i++)
pbase[i] = mul(pbase[i - 1], pbase[i - 1]);
ll ans = 0;
for (int i = 1; i <= n; i++)
{
if (n % i)
continue;
mat tt = qmi(i);
ll t = 0;
for (int j = 0; j < 64; j++)
t = (t + tt.a[j][j]) % mod;
t = t * phi[n / i] % mod;
ans = (ans + t) % mod;
}
ans = ans % mod * ksm(n, mod - 2) % mod;
cout << ans << endl;
return 0;
} K.Tree(dsu on tree + 权值线段树)
题意:
求有个点且以
为根的有根树上,其中每个节点有一个权值
,求满足下面条件的有序点对
数目
- 一个点不为另一个点的祖先
到
的路径长度小于等于给定的值
其中和
算两种方案
题解:
因为要满足一个点不为另一个点的祖先,那么可以想到如果将作为根节点的话,那么
和
恰好位于两棵不同的子树,且满足
和
。可以考虑用dsu on tree来实现,其中可以用一棵权值线段树来维护不同的
,这样可以实现
,
到
的路径长度小于等于给定的值
就是找权值为
那棵线段树中深度为
的值,其中
。
dsu on tree部分就是套板子,对dsu on tree不是很了解可以参考我这篇博客的E题,算是这题的弱化版。戳我~
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 1e5 + 5;
ll ans;
int n, k, a[maxn];
struct E
{
int to, next;
} Edge[maxn << 1];
int tot, head[maxn];
void init()
{
memset(head, -1, sizeof(head));
tot = ans = 0;
}
void addedge(int u, int v)
{
Edge[tot].to = v;
Edge[tot].next = head[u];
head[u] = tot++;
}
int siz[maxn], son[maxn], dep[maxn];
void dfs(int u)
{
siz[u] = 1;
son[u] = 0;
for (int i = head[u]; ~i; i = Edge[i].next)
{
int v = Edge[i].to;
dep[v] = dep[u] + 1;
dfs(v);
siz[u] += siz[v];
if (siz[v] > siz[son[u]])
son[u] = v;
}
}
int T[maxn], lson[maxn * 200], rson[maxn * 200], idx = 0, sum[maxn * 200];
void update(int &rt, int l, int r, int pos, int val)
{
if (!rt) rt = ++idx;
sum[rt] += val;
if (l == r) return;
int mid = l + r >> 1;
if (pos <= mid) update(lson[rt], l, mid, pos, val);
else update(rson[rt], mid + 1, r, pos, val);
}
int query(int rt, int l, int r, int L, int R)
{
if (!rt) return 0;
if (L <= l && r <= R) return sum[rt];
int res = 0;
int mid = l + r >> 1;
if (L <= mid) res += query(lson[rt], l, mid, L, R);
if (R > mid) res += query(rson[rt], mid + 1, r, L, R);
return res;
}
int flag; //flag用于标记重儿子
void cal(int u, int val)
{
update(T[a[u]], 1, n, dep[u], val);
for (int i = head[u]; ~i; i = Edge[i].next)
{
int v = Edge[i].to;
cal(v, val);
}
}
void query(int u, int lca)
{
int d = k + dep[lca] * 2 - dep[u];
d = min(d, n);
int t = 2 * a[lca] - a[u];
if (d >= 1 && t >= 0 && t <= n) ans += 2ll * query(T[t], 1, n, 1, d);
for (int i = head[u]; ~i; i = Edge[i].next)
{
int v = Edge[i].to;
query(v, lca);
}
}
//dsu on tree板子
void dfs(int u, bool keep)
{
//第一步,搞轻儿子及其子树算答案删贡献
for (int i = head[u]; ~i; i = Edge[i].next)
{
int v = Edge[i].to;
if (v == son[u]) continue;
dfs(v, false);
}
//第二步,搞重儿子及其子树算答案不删贡献
if (son[u])
{
dfs(son[u], true);
flag = son[u];
}
//第三步,暴力统计u及其所有轻儿子的贡献合并道刚算出的重儿子信息里
for (int i = head[u]; ~i; i = Edge[i].next)
{
int v = Edge[i].to;
if (v == flag) continue;
query(v, u);
cal(v, 1);
}
update(T[a[u]], 1, n, dep[u], 1);
flag = 0;
//把需要删除贡献的删一删
if (!keep) cal(u, -1);
}
int main()
{
init();
scanf("%d%d", &n, &k);
for (int i = 1; i <= n; i++)
scanf("%d", &a[i]);
for (int i = 2, u; i <= n; i++)
{
scanf("%d", &u);
addedge(u, i);
}
dep[1] = 1;
dfs(1);
dfs(1, 0);
printf("%lld\n", ans);
return 0;
} L.Who is the Champion(签到)
题意:
有个球队,给出
的矩阵第
行第
列表示第
个球队和第
个球队踢球进球的个数。两个球队pk,赢的球队加
分,输的球队不加分。平局各加
分。规定分数最多的球队为冠军,如果有多个分数最多的,按每个队赢球数-失球数最高的,如果仍有多个,输出
。否则输出第几个球队是冠军
题解:
按题意模拟即可
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int, int> pii;
typedef pair<ll, ll> pll;
const int maxn = 105;
const int INF = 0x3f3f3f3f;
const ll mod = 998244353;
int n, a[maxn][maxn];
struct node
{
int s, g, id;
bool operator<(const node &C) const
{
if (s == C.s)
return g > C.g;
return s > C.s;
}
} p[maxn];
int main()
{
int n;
scanf("%d", &n);
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
scanf("%d", &a[i][j]);
if (n == 1)
{
printf("%d\n", 1);
return 0;
}
memset(p, 0, sizeof(p));
for (int i = 1; i <= n; i++)
{
p[i].id = i;
for (int j = 1; j <= n; j++)
{
if (i == j)
continue;
p[i].g += a[i][j];
if (a[i][j] > a[j][i])
p[i].s += 3;
else if (a[i][j] == a[j][i])
p[i].s += 1;
p[i].g -= a[j][i];
}
}
sort(p + 1, p + n + 1);
if (p[1].s == p[2].s && p[1].g == p[2].g)
puts("play-offs");
else
printf("%d\n", p[1].id);
return 0;
} 
查看26道真题和解析