USACO 2020 February Contest, Gold
图片懒得上传了,如果影响阅读可以看个人公开笔记
另外就是之前接近一年没登陆,所以消息都没看到,抱歉了。
Problem 1. Delegation
题面描述
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-10myv8p6-1611135874428)(https://i.loli.net/2021/01/20/Tt8W5LVDlkUjBEy.png)]
解题思路
本题是求每次挤奶的最早时间,由于某次挤奶时间可能有依赖,所以可以将依赖看作边,即 a 比 b 至少晚 z 天,就建一条 b --> a 的边,边权为 z ,接下来只要找到所有的入度为 0 的点,从他们出发更新其它所有点的最大路径即可。
根据题意可知我们建立的图中不会有环,但是一个点可能被重复访问多次,而一个点一旦被修改,那么从他出发所有的终点都需要更新,于是可以采用深搜或者广搜,采用深搜剪枝有些麻烦,于是我后来又改成广搜了,改成广搜需要一个标记数组记录待更新节点是否已经在队列中了,如果已经在就无需再次压入了。
但是需要注意的是,一个点第一次被访问时无论是否被更新都要压入队列的,否则无法更新后面的点,因此还需要一个标记数组 vis2 来记录某个点是否被第一次访问。
代码示例
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int n,m,c;
const int N = 1e5+10;
const int M = 10*N;
int head[N],ver[M],nex[M],edge[M],tot = 1;
int deg[N];
void addEdge(int x,int y,int z){
ver[++tot] = y; edge[tot] = z;
nex[tot] = head[x]; head[x] = tot;
deg[y]++;
}
int s[N];
queue<int> que;
int vis[N],vis2[N];
void solve(){
for(int i = 1;i <= n;i++) scanf("%d",s+i);
for(int i = 1,x,y,z;i <= c;i++){
scanf("%d%d%d",&x,&y,&z);
addEdge(x,y,z);
}
for(int i = 1;i <= n;i++)
if(!deg[i]) que.push(i),vis[i] = 1;
while(que.size()){
int x = que.front(); que.pop();
vis[x] = 0,vis2[x] = 1;
for(int i = head[x];i;i = nex[i]){
int y = ver[i], z = edge[i];
if(s[x]+z >= s[y]){
if(!vis[y]) que.push(y),vis[y] = 1;
s[y] = s[x]+z;
}
if(!vis2[y] && !vis[y]) que.push(y),vis[y] = 1;
}
}
for(int i = 1;i <= n;i++) printf("%d\n",s[i]);
}
int main(){
scanf("%d%d%d",&n,&m,&c);
solve();
return 0;
}
Problem 2. Help Yourself
题面描述
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KxTuvHPz-1611135874432)(https://i.loli.net/2021/01/20/Mkmo8xnR12twOC9.png)]
解题思路
假设对于第 i 个线段,在其前面(坐标轴意义的前) a 条线段与其相交,那么该线段对答案的贡献是 2 n − 1 − a 2^{n-1-a} 2n−1−a ,因为其它 n-1-a 条线段都与其不相交,所以这 a 条线段对这 n-1-a 条线段的所有子集的复杂度之和为 2 n − 1 − a 2^{n-1-a} 2n−1−a 。
这里求相交的线段数量采用差分+前缀和的思路,在求一维坐标轴上线段相交常用这种方法。
代码示例
#include<bits/stdc++.h>
using namespace std;
const int N = 2e5+10;
const int MOD = 1e9+7;
int n,ans = 0;
int sum[N],l[N],r[N],pw2[N];
void solve(){
for(int i = 1;i <= n;i++)
cin >> l[i] >> r[i];
pw2[0] = 1;
for(int i = 1;i < n;i++) pw2[i] = pw2[i-1]*2%MOD;//预处理出 2 的幂
for(int i = 1;i <= n;i++){
sum[l[i]]++, sum[r[i]]--; //进行差分
}
for(int i = 1;i <= 2*n;i++) sum[i] += sum[i-1]; //求前缀和
for(int i = 1;i <= n;i++) ans = (ans + pw2[n-1-sum[l[i]-1]])%MOD;
cout << ans << endl;
}
int main(){
scanf("%d",&n);
solve();
return 0;
}
Problem 3. Delegation
题面描述
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ctSpL4Yq-1611135874435)(https://i.loli.net/2021/01/20/hNJi51tCTWpq4BV.png)]
解题思路
这道题的难点在于如何判断一个 k 是否合法。
可以通过树上 dp 的做法来判断能否分为若干条长度为 k 的链。不应该将注意力放在边上,而是应该放在点上。无需关注每颗子树如何分割,只需关系到底能不能分割。如果一个子树有 a 个节点,那么这个子树内一定可以分出 a/k 条长度为 k 的链,同时剩下 a%k 条边。所以只需要考虑子树内分割后剩下多少条边,再考虑子树外剩下多少条边,然后看看能否配对即可。
显然树上 dp 可以通过 dfs 来实现,通过一次树上 dp 来求出所需的数据,而后只需 o(N) 一次判断即可。
解决了如何判断 k 是否合法,剩下的就简单了,因为我们很容易就能分析出来,若 k 不是 n-1 的因子,那就无论如何都不能均等分割的,所以实际上最多只需要执行 m 次 check(),其中 m 是 n-1 的因子数目。所以总的时间复杂度为 O(mn)。
代码示例
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+10;
const int MOD = 1e9+10;
const int M = 2*N;
int n;
int head[N],ver[M],nex[M],tot = 1;
void addEdge(int x,int y){
ver[++tot] = y, nex[tot] = head[x]; head[x] = tot;
}
vector<int> num[N];
int sub[N]; // sub[x] 子树 x 的节点个数
void dfs(int x,int fa){
sub[x] = 1;
for(int i = head[x];i ;i = nex[i]){
int y = ver[i];
if(y == fa) continue;
dfs(y,x);
sub[x] += sub[y];
num[x].push_back(sub[y]);
}
if(sub[x] != n) num[x].push_back(n-sub[x]);
}
int cur[N];
bool check(int k){
if((n-1)%k) return false;
for(int i = 0;i < k;i++) cur[i] = 0;
for(int i = 1;i <= n;i++){
int cnt = 0;
for(auto &t:num[i]){
int z = t%k;
if(z == 0) continue;
if(cur[k-z]) cur[k-z]--,cnt--;
else cur[z]++,cnt++;
}
if(cnt) return false;
}
return true;
}
void solve(){
for(int i = 1,x,y;i < n;i++){
scanf("%d%d",&x,&y);
addEdge(x,y); addEdge(y,x);
}
dfs(1,0);
for(int i = 1;i < n;i++) cout << check(i);
cout << endl;
}
int main(){
scanf("%d",&n);
solve();
return 0;
}