Codeforces Global Round 14 部分问题题解
A - Phoenix and Gold
- 如果初始给出的序列满足条件直接输出。
- 否则必然存在一个位置 (k),使得 (sum_{i=1}^{k}w_i=x)。
- 如果 (k e n),我们可以交换 (w_{k}) 与 (w_{k+1})。因为 (w_k e w_{k+1}),所以这样操作后一定就满足条件了。
- 否则 (k=n),此时无论如何操作都不满足条件。
说人话:找到前缀和等于 (x) 的位置,将这个位置与后一个位置上的数字交换。
复杂度 (O(n))。
int n ,x ,a[MX];
void solve(){
n = read() ,x = read();
for(int i = 1 ; i <= n ; ++i)
a[i] = read();
int sum = 0;
int aim = 0;
for(int i = 1 ; i <= n ; ++i){
sum += a[i];
if(sum == x){
aim = i;
}
}
if(aim){
if(aim != n) std::swap(a[aim] ,a[aim + 1]);
else{
puts("NO");
return;
}
}
puts("YES");
for(int i = 1 ; i <= n ; ++i)
printf("%d%c" ,a[i] ,"
"[i == n]);
}
int main(){
int T = read();
for(int i = 1 ; i <= T ; ++i){
solve();
}
return 0;
}
B - Phoenix and Puzzle
样例给出了两种构成正方形的状况。
我们于是可以用这两个正方形凑成更大的正方形。
复杂度 (O(1))。
int check(int x){
int d = sqrt(x) + 0.5;
if(d * d == x) return true;
return false;
}
void solve(){
int n = read();
if((n % 2 == 0 && check(n / 2))
|| (n % 4 == 0 && check(n / 4)))
puts("YES");
else puts("NO");
}
int main(){
int T = read();
for(int i = 1 ; i <= T ; ++i){
solve();
}
return 0;
}
C - Phoenix and Towers
初始随便选 (m) 个塔做塔基,然后对于剩下的 (n-m) 个塔考虑往谁上面垫。
因为 (1le h_i le x),不难发现每次往最小值垫就可以满足题目要求。
复杂度 (O(n log n)),实现精细可达 (O(n))。
int n ,m ,x;
struct CHOOSE{
int val ,id;
bool operator <(const CHOOSE& B)const{
return val > B.val;
}
};
int h[MX] ,id[MX];
void solve(){
n = read() ,m = read() ,x = read();
for(int i = 1 ; i <= n ; ++i)
h[i] = read();
std::priority_queue<CHOOSE> q;
for(int i = 1 ; i <= m ; ++i){
q.push((CHOOSE){h[i] ,i});
id[i] = i;
}
for(int i = m + 1 ; i <= n ; ++i){
CHOOSE cur = q.top(); q.pop();
id[i] = cur.id;
cur.val += h[i];
q.push(cur);
}
puts("YES");
for(int i = 1 ; i <= n ; ++i)
printf("%d%c" ,id[i] ,"
"[i == n]);
}
int main(){
int T = read();
for(int i = 1 ; i <= T ; ++i){
solve();
}
return 0;
}
D - Phoenix and Towers
差评啊,这个 D 怎么比 E 还难。
答案上界是 (n),当所有的都是左袜子,所有颜色都不同时取到。
一开始肯定有能配对的就配对。
然后考虑袜子数量多的一侧,如果有两个相同的,我们可以花费 (1) 的代价把它们配对。
我们贪心配对直到袜子数量多的一侧的再减少就变成数量少的了或者没有可配对的了。
接下来只能把袜子左右先配对,然后改颜色了。
复杂度 (O(n log n)),实现精细可 (O(n))。
int n ,L ,R ,c[MX] ,cnt[MX];
void solve(){
std::multiset<int> S[2];
n = read() ,L = read() ,R = read();
for(int i = 1 ; i <= n ; ++i){
cnt[i] = 0;
c[i] = read();
S[i > L].insert(c[i]);
}
int ans = 0;
for(int i = 1 ; i <= L ; ++i){
auto kksk = S[1].find(c[i]);
if(kksk == S[1].end()) continue;
S[0].erase(S[0].find(c[i]));
S[1].erase(kksk);
}
if(S[0].size() < S[1].size()) std::swap(S[0] ,S[1]);
for(auto i : S[0]) cnt[i]++;
for(int i = 1 ; i <= n ; ++i){
while(cnt[i] >= 2 && S[0].size() - 2u >= S[1].size()){
cnt[i] -= 2;
S[0].erase(S[0].find(i));
S[0].erase(S[0].find(i));
++ans;
}
}
int cnt1 = 0 ,cnt2 = 0;
cnt1 = S[0].size();
cnt2 = S[1].size();
ans += std::abs(cnt1 - cnt2) / 2;
ans += std::abs(cnt1 + cnt2) / 2;
printf("%d
" ,ans);
}
int main(){
int T = read();
for(int i = 1 ; i <= T ; ++i){
solve();
}
return 0;
}
E - Phoenix and Computers
有个非常直接的做法:设 (dp_{i,j,0/1,0/1}) 表示对于一个长度为 (i) 的序列,用 (j) 次点亮所有电脑,左右是点亮的电脑还是空的。
转移枚举当前点亮哪台电脑,以及分给左右多少次点亮机会,乘以一个组合数转移即可。
复杂度 (O(n^4)),完美被卡。
考虑手动点亮的电脑集合长啥样?肯定是一段一段的区间,并且两段区间之间有且仅有一个空格。
并且区间与区间之间的点亮顺序没有限制,单独一个长度为 (i) 的区间的点亮方案是 (sum_{j=1}^{i}inom{i-1}{j-1}=2^{i-1})。
于是我们重新设一个 dp 方程为 (dp_{i,j})。
表示考虑前 (i) 台电脑,用了 (j) 次手动点亮,并且所有电脑最终都被点亮的方案数。
转移枚举最后一个区间的长度即可。
复杂度 (O(n^3))。
int n;
LL dp[MX][MX] ,C[MX][MX] ,way[MX];
void init(){
for(int i = 0 ; i < MX ; ++i) C[i][0] = 1;
for(int i = 1 ; i < MX ; ++i)
for(int j = 1 ; j < MX ; ++j)
C[i][j] = (C[i - 1][j] + C[i - 1][j - 1]) % MOD;
for(int i = 1 ; i < MX ; ++i){
for(int fir = 1 ; fir <= i ; ++fir){
way[i] = (way[i] + C[i - 1][fir - 1]) % MOD;
}
}
}
int main(){
n = read() ,MOD = read();
init();
for(int i = 1 ; i <= n ; ++i){
dp[i][i] = way[i];
for(int j = 1 ; j <= i && i - j - 1 >= 0 ; ++j){
for(int las = 0 ; las + j <= i ; ++las){
dp[i][las + j] = (dp[i][las + j] + dp[i - j - 1][las] * way[j]
% MOD * C[las + j][j]) % MOD;
}
}
}
LL ans = 0;
for(int i = 0 ; i <= n ; ++i)
ans = (ans + dp[n][i]) % MOD;
printf("%lld
" ,ans);
return 0;
}
F - Phoenix and Earthquake
首先确定一个结论,只要 (sum_{i=1}^{n}a_i ge (n-1)x),就必然有解,所以我们只需要保留随意一棵生成树。
因为这题就是个传递纸牌的模型,或者你还可以把它看成 NOI2020 制作菜品的 (m=n-1) 部分分。
这样做一定有解:每次选择 (a_u+a_v) 最大的一条边 ((u,v)) 执行操作。但是维护这样的最大的边是很难的。
换一种思路,按传递纸牌的模型思考,把每个点的权值先减去 (x),现在只要保证每个联通块权值永远不小于 (-x) 就行了。
对于每一棵子树计算它的子树和是否大于等于 (-x)。
true
,则这棵子树可以先直接操作完,然后把它多余的牌传给它的祖先,这样它的祖先就有更多牌了。false
,仅仅凭这棵子树无法操作完,必须先从祖先处传牌过来。
于是就可以给每条边确定一个方向。
按深度从大到小枚举,对于每一个结点 (x),假设其直接祖先为 (f)。
- 若边 ((x,f)) 的方向是 (f o x),啥也不做。
- 否则方向是 (x o f),此时我们必须先做完 (x) 的子树,然后立刻连接 ((x,f))。
"做完 (x) 的子树"可以直接拓扑排序连接每条边。
略麻烦。
复杂度可以做到 (O(n alpha(n)))。
int fa[MX];
int n ,m ,C;
LL a[MX] ,wi[MX];
void init(){for(int i = 1 ; i < MX ; ++i) fa[i] = i ,wi[i] = a[i];}
int find(int x){return fa[x] == x ? x : fa[x] = find(fa[x]);}
void link(int x ,int y){
x = find(x) ,y = find(y);
fa[x] = y; wi[y] += wi[x] - C;
}
int head[MX] ,tot;
struct edge{
int node ,next ,w;
}h[MX << 1];
void addedge(int u ,int v ,int w ,int flg = 1){
h[++tot] = (edge){v ,head[u] ,w} ,head[u] = tot;
if(flg) addedge(v ,u ,w ,false);
}
std::vector<int> ans ,sp;
std::vector<std::pair<int ,int> > e[MX];
int ok ,indeg[MX] ,pa[MX] ,back[MX] ,dep[MX] ,vis[MX];
LL sz[MX];
void dapai(int x ,int f ,int dp){
dep[x] = dp;
pa[x] = f;
sz[x] = a[x];
for(int i = head[x] ,d ; i ; i = h[i].next){
if((d = h[i].node) == f) continue;
dapai(d ,x ,dp + 1);
sz[x] += sz[d] - C;
if(sz[d] - C >= 0){
e[d].push_back({x ,h[i].w});
indeg[x]++;
back[d] = 1;
sp.push_back(d);
}
else{
e[x].push_back({d ,h[i].w});
indeg[d]++;
}
}
}
bool cmp(int a ,int b){return dep[a] > dep[b];}
void topo(){
init();
back[1] = 1;
sp.push_back(1);
std::sort(sp.begin() ,sp.end() ,cmp);
for(auto r : sp){
std::queue<int> tmp ,q;
tmp.push(r);
while(!tmp.empty()){
int x = tmp.front(); tmp.pop();
if(!indeg[x]) q.push(x);
for(int i = head[x] ,d ; i ; i = h[i].next){
d = h[i].node;
if(x == r && d == pa[r]) continue;
if(!back[d] && !vis[d]){
vis[d] = 1;
tmp.push(d);
}
}
}
while(!q.empty()){
int x = q.front(); q.pop();
for(auto i : e[x]){
if(x == r && i.first == pa[x]) continue;
ans.push_back(i.second);
--indeg[i.first];
link(i.first ,x);
if(wi[find(x)] < 0){
ok = 0;
return;
}
if(!indeg[i.first]) q.push(i.first);
}
}
if(r != 1){
for(auto i : e[r]){
if(i.first == pa[r]){
ans.push_back(i.second);
--indeg[i.first];
link(i.first ,r);
if(wi[find(r)] < 0){
ok = 0;
return;
}
}
}
}
}
}
int main(){
init();
n = read() ,m = read() ,C = read();
for(int i = 1 ; i <= n ; ++i){
a[i] = read();
}
for(int i = 1 ,u ,v ; i <= m ; ++i){
u = read() ,v = read();
if(find(u) != find(v)){
link(u ,v);
addedge(u ,v ,i);
}
}
ok = 1;
dapai(1 ,0 ,1);
topo();
if(ok){
puts("YES");
for(auto i : ans)
printf("%d
" ,i);
}
else puts("NO");
return 0;
}
G - Phoenix and Odometers
为什么路径要回到原点?是不是跟环有什么关系?
对于询问点所在的强连通分量,设其所有环的长度的 (gcd) 为 (x)。
如果存在自然数 (k) 满足 (kx equiv -s pmod t),那么答案就是 YES
,否则为 NO
。
为什么?你只要走 (t) 遍某个环,它的作用就会消失。
还因为这是个强连通分量,你就可以任意选择经过某个环的次数。
设所有环的长度分别为 (w_1,w_2,cdots)
根据裴蜀定理,(sum_{i}k_iw_i= W) 有解的充要条件是 (gcd({w_i})|W),所以说得证。
现在问题是在这张图上,环的数量是指数级的,你根本不可能找全所有的 (w)。
但你发现你只要走 (t) 遍某个环,它的作用就会消失,据此我们可以保留一定数量的环,通过这些环的线性叠加形成所有的环的作用。
保留环是个常用套路,从某个点建外向树,对于后向边和横插边都像前向边那样就行了。
复杂度 (O(n log w))。
int head[MX] ,tot;
struct edge{
int node ,next ,w;
}h[MX << 1];
void addedge(int u ,int v ,int w){
h[++tot] = (edge){v ,head[u] ,w} ,head[u] = tot;
}
int n ,m;
int DFN[MX] ,low[MX] ,stk[MX] ,flg[MX] ,top ,cnt;
int repre[MX];
int col[MX] ,sz[MX] ,ccnt;
void tarjan(int x){
DFN[x] = low[x] = ++cnt;
stk[++top ] = x ,flg[x] = 1;
for(int i = head[x] ,d ; i ; i = h[i].next){
if(!DFN[d = h[i].node]){
tarjan(d);
low[x] = std::min(low[x] ,low[d]);
}else if(flg[d] && DFN[d] < low[x]) low[x] = DFN[d];
}
if(DFN[x] == low[x]){int j = 0;
repre[++ccnt] = stk[top];
while(j != x){
j = stk[top--];
col[j] = ccnt;
flg[j] = false;
sz[ccnt]++;
}
}
}
LL gcd(LL a ,LL b){
return b ? gcd(b ,a % b) : a;
}
LL cgcd[MX] ,w[MX];
LL getgcd(int x){
flg[x] = 1;
LL ans = 0;
for(int i = head[x] ,d ; i ; i = h[i].next){
if(col[d = h[i].node] != col[x]) continue;
if(flg[d]){
ans = std::abs(gcd(ans ,w[x] + h[i].w - w[d]));
}
else{
w[d] = w[x] + h[i].w;
ans = gcd(ans ,getgcd(d));
}
}
return ans;
}
int main(){
n = read() ,m = read();
for(int i = 1 ,u ,v ,w ; i <= m ; ++i){
u = read() ,v = read() ,w = read();
addedge(u ,v ,w);
}
for(int i = 1 ; i <= n ; ++i)
if(!DFN[i]) tarjan(i);
for(int i = 1 ; i <= ccnt ; ++i)
cgcd[i] = getgcd(repre[i]);
int q = read();
LL v ,s ,t;
for(int i = 1 ; i <= q ; ++i){
v = read() ,s = read() ,t = read();
if(cgcd[col[v]] == 0){
// A single point
puts(s ? "NO" : "YES");
}
else{
LL x = cgcd[col[v]];
s = (t - s) % t;
LL GCD = gcd(x ,s);
x /= GCD ,s /= GCD;
t /= gcd(GCD ,t);
if(gcd(x ,t) == 1) puts("YES");
else puts("NO");
}
}
return 0;
}