2022 HDU多校7
Bowling(计算几何、凸包)
Problem
二维平面上给定一个\(n\)个点凸包和\(m\)个点,有\(q\)次询问,每次给定一个方向向量\(\vec{v}\),问这个凸包沿着\(\vec{v}\)移动或碰到几个点
\(1\le n,m,q\le 10^5\)
Solve
Code
Independent Feedback Vertex Set(图论、染色)
Problem
给定一种方式构造一个\(n\)个点的无向图图:一开始点\(1,2,3\)连成一个三元环,之后再依次加入\(n-3\)个点,每个点\(i\)都与之前已经直接连接的两个点\(u,v\)构成三元环。每个点有一个点权,现在要求从图中选出一个点集\(V_I\)使得\(V_I\)中任意两点没有边直接向量,并且使得\(G|V/V_I|\)是一个森林,问这样的\(V_I\)的最大点权和是多少
\(1\le n\le 5\times 10^5\)
Solve
要使得\(G|V/V_I|\)是一个森林,意味着\(G|V/V_I|\)不存在环,由于每次加点都形成一个三元环,所以\(V\)一定包含每个三元环中恰好一个,因为\(V\)中允许两个点直接连边,所以"恰好一个",那么对图做三染色,那么选的点一定同色,因为不同色就会由两个点在一个环中,然后对每种颜色取最大值即可
Code
#include <bits/stdc++.h>
#define ll long long
using namespace std;
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T;
cin>>T;
while(T--){
int n;
cin>>n;
vector<int>a(n+1);
vector<ll>f(4);
for(int i=1;i<=n;i++) cin>>a[i];
vector<vector<int>>E(n+1);
vector<vector<int>>col(n+1,vector<int>(4));
for(int i=1;i<=3;i++){
col[i][i]=1;
f[i]=a[i];
}
for(int i=4;i<=n;i++){
int u,v;
cin>>u>>v;
for(int j=1;j<=3;j++){
if(!col[u][j] && !col[v][j]){
col[i][j]=1;
f[j]+=a[i];
}
}
}
cout<<max({f[1],f[2],f[3]})<<'\n';
}
}
Counting Stickmen(计数)
Problem
给定一棵树,统计树上有多少个火柴人,树上火柴人的形状如下
o
|
o
/|\
o o o
/ / \ \
o o o o
其中o
表示树节点,|/\
表示树边,最上面的o
表示头,第二个o
表示脖子,第\(3\)层的o
依次表示左肩膀、身体、右肩膀。
Solve
发现,一个点作为脖子就可以很好计算出有多少个火柴人。而一个节点可以作为脖子当且仅当它的度数大于\(3\),然后计算与它相连的节点中作为手臂,身体的点的方案数,一个节点可以作为身体当且仅当它的度数大于\(2\),然而一个可以作为身体的节点我们也可以从它的出边中选择一条来作为手臂。所以可以预处理出每个节点可以产生的手臂、身体的数量,然后对于每个点作为脖子的时候去计算产生的火柴人的数量。假设一个节点\(u\)作为脖子的时候,由于这里的火柴人没有要求一定是按照树高来构造,所以在\(DFS\)的时候要考虑它的父亲,假设\(u\)所有相连的点可以构成的总手臂数是\(hh\),总身体数是\(bb\),对于一个于\(u\)相连的节点\(v\),我们计算它作为身体时的贡献,那么除\(v\)外可以产生的手臂数量是\(\binom{hh-h[v]}{2}-(bb-b[v])\),意思就是在其他手臂中任选\(2\)条作为当前的手臂,但这包含在同一个身体里面的手臂的数量,然后减去在同一个身体的手臂的数量即可,然后再选择哪个点作为头,方案数是\(deg[u]-3\),那么总方案数就是\(\sum_{v}b[v](\binom{hh-h[v]}{2}-(bb-b[v]))(deg[u]-3)\)
Code
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int mod=998244353;
ll power(ll x,ll y){
ll res=1;
while(y){
if(y&1) res=res*x%mod;
x=x*x%mod;
y>>=1;
}
return res;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T;
cin>>T;
ll inv2=power(2,mod-2);
auto binom2=[&](ll x)->ll{
return x*(x-1)%mod*inv2%mod;
};
while(T--){
int n;
cin>>n;
vector<vector<int>>E(n+1);
vector<ll>deg(n+1);
for(int i=1;i<n;i++){
int u,v;
cin>>u>>v;
E[u].push_back(v);
E[v].push_back(u);
deg[u]++;
deg[v]++;
}
vector<ll>b(n+1),h(n+1);
for(int i=1;i<=n;i++){
h[i]=deg[i]-1;
if(deg[i]>=3) b[i]=binmo2(deg[i]-1);
}
ll ans=0;
auto dfs=[&](auto self,int u,int fa)->void{
ll bb=0,hh=0;
for(auto v:E[u]){
bb=(bb+b[v])%mod;
hh=(hh+h[v])%mod;
if(v==fa) continue;
self(self,v,u);
}
if(deg[u]<4) return ;
for(auto v:E[u]){
ans=(ans+b[v]*(binom2(hh-h[v])-(bb-b[v])+mod)%mod*(deg[u]-3)%mod)%mod;
}
return ;
};
dfs(dfs,1,1);
cout<<ans<<'\n';
}
return 0;
}
Sumire(数位DP)
Problem
计算
其中\(f(x,B,d)\)表示\(x\)在\(B\)进制表示下数字\(d\)的个数
\(0\le k\le 10^9\),\(2\le B\le 10^9\),\(0\le d\lt B\),\(1\le l\le r\le 10^{18}\)
Solve
定义\(dp_{i,0/1,1/0,j}\)表示来到第\(i\)位,是否有高位限制,是否有前导零、当前数包含\(d\)的个数为\(j\)时的答案。然后就是常规的数位DP,具体分类情况参考代码
Code
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int mod=1e9+7;
int p[65],len;
ll power(ll x,int y){
if(!x) return 0;
ll res=1;
while(y){
if(y&1) res=res*x%mod;
x=x*x%mod;
y>>=1;
}
return res;
}
int k,B,d;
ll dp[65][2][2][65];
ll dfs(int pos,int limit,int zero,int cnt){
if(pos==0) return dp[pos][limit][zero][cnt]=power(cnt,k);
if(dp[pos][limit][zero][cnt]!=-1) return dp[pos][limit][zero][cnt];
int mx=limit?p[pos]:B-1;
ll res=0;
if(d<=mx){
if(d){
res=(res+dfs(pos-1,0,zero,cnt))%mod; //填0,并且d!=0
res=(res+dfs(pos-1,limit&&(d==mx),1,cnt+1))%mod; //填d,并且d!=0
}else
res=(res+dfs(pos-1,limit&&(d==mx),zero,cnt+zero));// 填入d=0,不是前导0的加入答案
if(d<mx) res=(res+dfs(pos-1,limit,1,cnt))%mod; //填入mx
int num=max({0,d-1,mx-1-(d!=0)}); //不是0,d,mx的数的个数
res=(res+num*dfs(pos-1,0,1,cnt)%mod)%mod;
}else{
if(mx){
res=(res+dfs(pos-1,0,zero,cnt))%mod; //填入0,且mx!=0
}
res=(res+dfs(pos-1,limit,(zero || mx>0),cnt))%mod; //填mx
int num=max(0,mx-1);
if(num) res=(res+num*dfs(pos-1,0,1,cnt))%mod; //填入非0
}
return dp[pos][limit][zero][cnt]=res;
}
int cal(ll x){
len=0;
memset(dp,-1,sizeof dp);
while(x) p[++len]=x%B,x/=B;
return dfs(len,1,0,0); //zero=0表示有前导0
}
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T;
cin>>T;
while(T--){
cin>>k>>B>>d;
ll l,r;
cin>>l>>r;
ll ans=(cal(r)-cal(l-1)+mod)%mod;
cout<<ans<<'\n';
}
}
Triangle Game(博弈论)
Problem
给定三个数\(a,b,c\),\(\text{Alice},\text{Bob}\)轮流操作,每个人每次可以选择其中一个数并将这个数减去一个正整数,使得这三个数仍然可以组成三角形,不能操作的数输掉比赛,问谁可以获胜
Solve
hit1
:\((1,1,1)\)是必败态
博弈论玄学,记结论好了。
Code
#include<bits/stdc++.h>
using namespace std;
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T;
cin>>T;
while(T--){
int a,b,c;
cin>>a>>b>>c;
if(((a-1)^(b-1)^(c-1))==0) cout<<"Lose\n";
else cout<<"Win\n";
}
}
Weighted Beautiful Tree(树形DP)
Problem
给定一个树,每个点有一个点权\(wn_i\)和花费\(c_i\),每条边有边权\(we_i\)。定义一条边是好的,满足\(\min(wn_u,wn_v)\le we\le \max(wn_u,wn_v)\)。更改一个点的点权代价是\(c_i|wn_i-wn_{i}^{'}|\)。求把所有边都变成好的边的最小代价
Solve
hit1
:一个节点的权值如果改变,那么改变的数值只能是与它相连的边的边权会更优
容易想到是树形DP,并且个\(dp\)定义方式就是\(dp_{u,0/1}\)表示\(u\)作为下界还是上界,但这个上下界相对于哪条边而言。如果是DFS过程中的儿子边,那么很多条,怎么确定,但发现\(u\)的父边只有一条,那么定义为\(u\)作为父边的下界或上界就很好确定了。
对于\(u\)最终确定的点权\(wn_u\),假设它此时作为父边的下界,那么它满足的约束是\(w_{v_1}\le wn_u,w_{v_2}\le wn_u,\cdots,wn_u\le w_{v_m},wn_u\le w_{v_{m+1}},wn_u\le w_{up},\cdots\),不妨设比\(wn_u\)小的最大值\(min_{max}\),比\(wn_u\)大的最小值是\(max_{min}\),那么\(\min_{max}\le wn_u\le max_{min}\),那么这两个上下界是最紧的,那么\(wu_n\)取这两个上下界之间的值是最优,而可以发现这是贡献是固定的,所以不妨就取\(min_{max}\)或\(max_{min}\)。因此我们可以枚举\(u\)的所有可能权值来转移
在转移的过程中,我们利用类似反悔的操作,先假设\(u\)的所有儿子都是作为与\(u\)相连边的上界,记录他们的总贡献为\(up\),然后把边权按照从小到大排序(包括\(u\)的父边和\(wn_u\)),依次枚举\(u\)取到的值,边权相同的一起处理,假设当前处理的边权是\(w\),首先把所有\(up\)都减去边权为\(w\)的点\(v\)作为上界时的贡献,用\(add\)记录这些点\(v\)作为上界还是下界是可以取到的最小贡献。由于这里是枚举\(wn_u\)的取值,那么之前比\(w\)小的边所连接\(v\)只能作为下界了,所以要记录前面的点作为下界的贡献为\(low\)。比如假设现在的情况是这样的000 111 1111
,前面\(3\)个作为下界贡献之前计算是\(low\),现在\([4,6]\)的边权相同,令他们的情况未知,就变成了000 xxx 1111
。假设此时这个区间的最优情况时010
,那么\(add\)就是这个区间的点作为上下界的情况时010
是的最优解,情况就变成了000 010 1111
,贡献就是\(pre+add+up\)。
Code
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const ll inf=1LL<<60;
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T;
cin>>T;
while(T--){
int n;
cin>>n;
vector<ll>c(n+1),wn(n+1);
for(int i=1;i<=n;i++) cin>>c[i];
for(int i=1;i<=n;i++) cin>>wn[i];
vector<vector<pair<int,ll>>>e(n+1);
for(int i=1;i<n;i++){
int u,v,w;
cin>>u>>v>>w;
e[u].emplace_back(v,w);
e[v].emplace_back(u,w);
}
vector<vector<ll>>dp(n+1,vector<ll>(2));
vector<ll>upw(n+1);
auto cal=[&](ll c,ll a,ll b)->ll{
return c*abs(a-b);
};
auto dfs=[&](auto self,int u,int fa)->void{
dp[u][0]=dp[u][1]=inf; //作为最小值/最大值
ll low=0,up=0;
vector<pair<ll,int>>s;
for(auto t:e[u]){
int v=t.first,w=t.second;
if(v==fa) continue;
upw[v]=w;
self(self,v,u);
s.emplace_back(w,v);
up+=dp[v][1]; //反悔,假定一开始所有儿子都是上界
}
s.emplace_back(wn[u],0);
if(u!=0) s.emplace_back(upw[u],0);
sort(s.begin(), s.end());
//类似一种带反悔的树形DP
for(int i=0;i<(int)s.size();i++){ //枚举wn[w]的取值
int j=i;
up-=dp[s[j].second][1]; //反悔
ll add=min(dp[s[j].second][1],dp[s[j].second][0]); //反悔的边将选择答案更小的
while(j<(int)s.size()-1 && s[j+1].first==s[j].first){ //处理边权相同的边
j++;
up-=dp[s[j].second][1];
add+=min(dp[s[j].second][1],dp[s[j].second][0]);
}
add+=up+low;
//儿子的边小于upw,并且wn[u]<=upw,则wn[u]是可以尝试选择去儿子的边权作为下界
if(s[j].first <= upw[u] || u==1){
dp[u][0]=min(dp[u][0],add+cal(c[u],wn[u],s[j].first));
}
//儿子的边权大于upw,并且wn[u]>=upw,则wn[u]可以尝试选择儿子的边权作为上界
if(s[j].first >= upw[u] || u==1){
dp[u][1]=min(dp[u][1],add+cal(c[u],wn[u],s[j].first));
}
while(i<j){
low+=dp[s[i].second][0];
i++;
}
low+=dp[s[i].second][0];
}
};
dfs(dfs,1,0);
cout<<min(dp[1][0],dp[1][1])<<'\n';
}
}
Counting Good Arrays(数论、DP、计数)
Problem
求长度不超过\(n\),最大值不超过\(m\),且满足\(a_{i-1}|a_i\)的序列的个数
\(1\le n,m\le 10^9\)