文章目录
E - Strange Integers
签到
D - Strange Fractions
签到
G - Edge Groups
题意:
求将树边分解成两两匹配的方案数
思路:
- 树形dp,分子树分配的边剩余是奇数还是偶数即可
- d p i , 0 表示 i 子树边数恰好两两匹配, d p i , 1 表示多余一条边没有匹配,每个节点只有其中一个状态合法 dp_{i,0}表示i子树边数恰好两两匹配,dp_{i,1}表示多余一条边没有匹配,每个节点只有其中一个状态合法 dpi,0表示i子树边数恰好两两匹配,dpi,1表示多余一条边没有匹配,每个节点只有其中一个状态合法
- 对于一条边 ( u , v ) ,如果 v 的状态为 d p v , 1 ,则 ( u , v ) 需要与 v 子树剩余的一条边匹配 对于一条边(u,v),如果v的状态为dp_{v,1},则(u,v)需要与v子树剩余的一条边匹配 对于一条边(u,v),如果v的状态为dpv,1,则(u,v)需要与v子树剩余的一条边匹配
- 否则, ( u , v ) 是未匹配的边, c n t + 1 ,留给 u 子树计算贡献 否则,(u,v)是未匹配的边,cnt+1,留给u子树计算贡献 否则,(u,v)是未匹配的边,cnt+1,留给u子树计算贡献
- 假设当前枚举到当前节点,剩余cnt条边没匹配,假设可以分成x组,则方案数为 C n 2 C n − 2 2 . . . A x x \frac{C_n^2C_{n-2}^2...}{A_x^x} AxxCn2Cn−22...
#include<bits/stdc++.h>
#define int long long
using namespace std;
typedef pair<int,int> PII;
const int N=1e5+10;
const int INF=1e9;
const int mod=998244353;
int qpow(int a,int b){
int ans=1;
for(;b;b>>=1){
if(b&1)ans=ans%mod*a%mod;
a=a%mod*a%mod;
}
return ans;
}
int sum[N],n,fac[N],inv_fac[N];
vector<int> adj[N];
int dp[N][2];
int C(int n,int m){
return fac[n]*inv_fac[m]%mod*inv_fac[n-m]%mod;
}
void init(){
fac[0]=inv_fac[0]=1;
for(int i=1;i<=n;i++)fac[i]=fac[i-1]*i%mod,inv_fac[i]=qpow(fac[i],mod-2);
sum[2]=C(2,2),sum[3]=C(3,2),sum[0]=sum[1]=1;
for(int i=4;i<=n;i++){
sum[i]=C(i,2)*sum[i-2]%mod;
}
for(int i=0;i<=n;i++)dp[i][0]=dp[i][1]=-1;
}
void dfs(int u,int fa){
if(u!=1&&adj[u].size()==1){
dp[u][0]=1;
return;
}
int cnt=0;
int res=1;
for(auto v:adj[u]){
if(v==fa)continue;
dfs(v,u);
if(dp[v][0]!=-1)res=res*dp[v][0]%mod,cnt++;
else res=res*dp[v][1]%mod;
}
// cout<<cnt<<" "<<res<<" "<<u<<"\n";
res=res*sum[cnt]%mod*inv_fac[cnt/2]%mod;
if(cnt&1)dp[u][1]=res;
else dp[u][0]=res;
}
void solve(){
cin>>n;
init();
for(int i=1;i<n;i++){
int u,v;
cin>>u>>v;
adj[u].push_back(v);
adj[v].push_back(u);
}
dfs(1,0);
// for(int i=1;i<=n;i++)cout<<sum[i]<<" \n"[i==n];
// for(int i=1;i<=n;i++)cout<<dp[i][0]<<" "<<dp[i][1]<<"\n";
cout<<dp[1][0]<<"\n";
}
signed main(){
cin.tie(0)->sync_with_stdio(0);
int T=1;
// cin>>T;
while(T--)solve();
return 0;
}
I - Steadily Growing Steam
思路:
带负权的背包问题,直接转移即可
d
p
i
,
j
,
k
表示枚举到第
i
个物品,操作了
j
次,背包容量为
k
的最大值
dp_{i,j,k}表示枚举到第i个物品,操作了j次,背包容量为k的最大值
dpi,j,k表示枚举到第i个物品,操作了j次,背包容量为k的最大值
为了防止背包容量为负,需要设置一个偏移量为p,表示背包容量为0的位置
答案:
m
a
x
i
=
0
k
(
d
p
n
,
i
,
p
)
max_{i=0}^k(dp_{n,i,p})
maxi=0k(dpn,i,p)
#include<bits/stdc++.h>
#define int long long
using namespace std;
typedef pair<int,int> PII;
const int N=1e5+10;
const int INF=1e16;
const int mod=998244353;
int dp[110][110][2610],n,k,v[110],c[110];
void solve(){
cin>>n>>k;
for(int i=0;i<=n;i++){
for(int j=0;j<=k;j++){
for(int u=0;u<=2600;u++)dp[i][j][u]=-INF;
}
}
dp[0][0][1300]=0;
for(int i=1;i<=n;i++)cin>>v[i]>>c[i];
for(int i=1;i<=n;i++){
for(int j=0;j<=k;j++){
for(int u=0;u<=2600;u++){
if(dp[i-1][j][u]==-INF)continue;
int tmp=dp[i-1][j][u]+v[i];
if(u+c[i]<=2600)dp[i][j][u+c[i]]=max(dp[i][j][u+c[i]],tmp);
if(u-c[i]>=0)dp[i][j][u-c[i]]=max(dp[i][j][u-c[i]],tmp);
if(j+1<=k&&u+2*c[i]<=2600)dp[i][j+1][u+2*c[i]]=max(dp[i][j+1][u+2*c[i]],tmp);
if(j+1<=k&&u-2*c[i]>=0)dp[i][j+1][u-2*c[i]]=max(dp[i][j+1][u-2*c[i]],tmp);
dp[i][j][u]=max(dp[i][j][u],dp[i-1][j][u]);
}
}
}
int ans=-INF;
for(int i=0;i<=k;i++)ans=max(ans,dp[n][i][1300]);
cout<<ans<<"\n";
}
signed main(){
cin.tie(0)->sync_with_stdio(0);
int T=1;
// cin>>T;
while(T--)solve();
return 0;
}
H - Life is a Game
题意:
⼀张带边权带点权⽆向图。从某点出发,有初始声望。
每第⼀次到达⼀个点将获得点权等值的声望加成。
经过⼀条边需要满⾜边权等值的最低声望限制。
多次给出起点和初始声望,询问能达到的最⼤声望。
思路:
- kruscal重构树
- 按照边权从⼩到⼤建⽴Kruskal 重构树。每次询问都是从叶子出发,在树上倍增。向上找到第⼀条不能通过的边(即,该边下⾯的⼦树的叶⼦点权和加上初始声望小于该边边权),把下⾯⼦树的叶⼦点权和加上初始声望即为答案。
- 时间复杂度: O ( ( n + m ) l o g m + q l o g n ) O((n+m)logm+qlogn) O((n+m)logm+qlogn)
#include<bits/stdc++.h>
#define int long long
using namespace std;
typedef pair<int,int> PII;
const int N=2e5+10;
const int INF=1e16;
const int mod=998244353;
struct Edge{
int u,v,w;
bool operator<(const Edge&tmp)const{
return w<tmp.w;
}
};
struct DSU{
vector<int> par;
void init(int n){
par.resize(n);
iota(par.begin(),par.end(),0);
}
int find(int x){
return par[x]==x?x:par[x]=find(par[x]);
}
bool same(int x,int y){
return par[find(x)]==par[find(y)];
}
bool merge(int x,int y){
x=find(x),y=find(y);
if(x==y)return false;
par[y]=x;
return true;
}
} dsu;
vector<Edge> e;
int n,m,q,tot,a[N],w[N],sum[N],par[N][30],cost[N][30];
void Ex_kruscal(){
for(auto [u,v,c]:e){
int fu=dsu.find(u),fv=dsu.find(v);
if(fu==fv)continue;
++tot;
a[tot]=a[fu]+a[fv];
w[tot]=c;
par[fu][0]=par[fv][0]=tot;
cost[fu][0]=w[tot]-a[fu];
cost[fv][0]=w[tot]-a[fv];
dsu.par[fu]=dsu.par[fv]=tot;
}
}
void solve(){
cin>>n>>m>>q;
tot=n;
for(int i=1;i<=n;i++)cin>>a[i];
for(int i=1;i<=m;i++){
int u,v,w;
cin>>u>>v>>w;
e.push_back({u,v,w});
}
sort(e.begin(),e.end());
dsu.init(2*n+1);
Ex_kruscal();
for(int j=1;j<=25;j++){
for(int i=1;i<=tot;i++)par[i][j]=par[par[i][j-1]][j-1],cost[i][j]=max(cost[i][j-1],cost[par[i][j-1]][j-1]);
}
// for(int i=1;i<=tot;i++)cout<<w[i]<<" \n"[i==tot];
while(q--){
int x,k;
cin>>x>>k;
for(int i=25;i>=0;i--){
if(cost[x][i]<=k&&par[x][i])x=par[x][i];
}
cout<<a[x]+k<<"\n";
}
}
signed main(){
cin.tie(0)->sync_with_stdio(0);
int T=1;
// cin>>T;
while(T--)solve();
return 0;
}
更容易想到的做法:离线,启发式合并
- 对于每个节点存它的询问,然后从小到大构造最小生成树的过程中计算
- 对于当前边的两个点,拿出点里面当前权值最小的点,判断是否能通过该条边,如果不能则计算答案,将该询问从该点移除,对于两个点都做一遍
- 最后两边的点存储的询问都是可以通过该边的,因此启发式合并这两个点询问
- 最后还存在的询问一定是可以遍历所有点的,计算答案
#include<bits/stdc++.h>
#define int long long
using namespace std;
typedef pair<int,int> PII;
const int N=2e5+10;
const int INF=1e16;
const int mod=998244353;
struct Edge{
int u,v,w;
bool operator<(const Edge&tmp)const{
return w<tmp.w;
}
};
struct DSU{
vector<int> par;
void init(int n){
par.resize(n);
iota(par.begin(),par.end(),0);
}
int find(int x){
return par[x]==x?x:par[x]=find(par[x]);
}
bool same(int x,int y){
return par[find(x)]==par[find(y)];
}
bool merge(int x,int y){
x=find(x),y=find(y);
if(x==y)return false;
par[y]=x;
return true;
}
} dsu;
vector<Edge> e;
int n,m,q,a[N],w[N];
set<array<int,2>> s[N];
void solve(){
cin>>n>>m>>q;
for(int i=1;i<=n;i++)cin>>a[i],w[i]=a[i];
for(int i=1;i<=m;i++){
int u,v,w;
cin>>u>>v>>w;
e.push_back({u,v,w});
}
sort(e.begin(),e.end());
vector<int> ans(q+1);
for(int i=1;i<=q;i++){
int x,k;
cin>>x>>k;
s[x].insert({k,i});
}
dsu.init(n+1);
auto merge=[&](int u,int v){
if(s[u].size()<s[v].size())swap(u,v);
w[u]+=w[v];
dsu.merge(u,v);
for(auto it:s[v])s[u].insert(it);
s[v].clear();
};
for(auto [u,v,c]:e){
int fu=dsu.find(u),fv=dsu.find(v);
if(fu==fv)continue;
while(s[fu].size()&&(*s[fu].begin())[0]+w[fu]<c){
auto [x,id]=*s[fu].begin();
s[fu].erase(s[fu].begin());
ans[id]=x+w[fu];
}
while(s[fv].size()&&(*s[fv].begin())[0]+w[fv]<c){
auto [x,id]=*s[fv].begin();
s[fv].erase(s[fv].begin());
ans[id]=x+w[fv];
}
merge(fu,fv);
}
for(int i=1;i<=n;i++){
while(s[i].size()){
auto [x,id]=*s[i].begin();
s[i].erase(s[i].begin());
ans[id]=x+w[i];
}
}
for(int i=1;i<=q;i++)cout<<ans[i]<<"\n";
}
signed main(){
cin.tie(0)->sync_with_stdio(0);
int T=1;
// cin>>T;
while(T--)solve();
return 0;
}
M - Harmony in Harmony
题意:
将地分成n部分,第二次忘了前一次的分配,又将地分成n部分,求每个人两次分配的土地面积的交集最小值最大
思路:
神奇的构造题
- 此题等价于存在⼀个两侧各有n个结点的带边权的满⼆分图,边权为实数,并且对任何⼀个结点,其所连边的权值和等于1/n。要求寻找⼀个尽可能⼤的 ans,使得在任何满⾜上述条件的⼆分图下,都能够找到⼆分图的完美匹配,使得匹配边的权值都不低于ans
- 取左边 t − 1 个黑点和右边 t 个白点,假设,左边每个黑点对右边的每个白点的贡献 1 n t 取左边t-1个黑点和右边t个白点,假设,左边每个黑点对右边的每个白点的贡献\frac{1}{nt} 取左边t−1个黑点和右边t个白点,假设,左边每个黑点对右边的每个白点的贡献nt1
- 则每个白点的贡献还剩余 1 n − t − 1 n t = 1 n t 则每个白点的贡献还剩余\frac{1}{n}-\frac{t-1}{nt}=\frac{1}{nt} 则每个白点的贡献还剩余n1−ntt−1=nt1
- 将贡献平分给 n − t + 1 个黑点,每个黑点得到的贡献为 1 n t ( n − t + 1 ) 将贡献平分给n-t+1个黑点,每个黑点得到的贡献为\frac{1}{nt(n-t+1)} 将贡献平分给n−t+1个黑点,每个黑点得到的贡献为nt(n−t+1)1
- t 取 ⌊ n + 1 2 ⌋ 取最小,答案为 1 n ⌊ n + 1 2 ⌋ ⌊ n + 1 2 ⌋ t取\lfloor \frac{n+1}{2} \rfloor取最小,答案为\frac{1}{n \lfloor \frac{n+1}{2} \rfloor \lfloor \frac{n+1}{2} \rfloor} t取⌊2n+1⌋取最小,答案为n⌊2n+1⌋⌊2n+1⌋1
#include<bits/stdc++.h>
#define int long long
using namespace std;
typedef pair<int,int> PII;
const int N=2e5+10;
const int INF=1e9;
const int mod=998244353;
void solve(){
int n;
cin>>n;
double ans=1.0/(1.0*n*((n+1)/2)*((n+2)/2));
cout<<setprecision(9)<<fixed<<ans<<"\n";
}
signed main(){
cin.tie(0)->sync_with_stdio(0);
int T=1;
// cin>>T;
while(T--)solve();
return 0;
}
K - Circle of Life
构造题
- 注意到长度为2时,有"10"
- 长度为4时,有"1001"
- 长度为5时,有"10001"
- 且4和5可以拼接
- 除了3都可以构造出来
#include<bits/stdc++.h>
#define int long long
using namespace std;
typedef pair<int,int> PII;
const int N=2e5+10;
const int INF=1e9;
const int mod=998244353;
void solve(){
int n;
cin>>n;
if(n==3)cout<<"Unlucky\n";
else{
int x=0,y=0,z=0;
if(n%2)z=1,n-=5;
y=n/4;
if(n%4!=0)x=1;
for(int i=1;i<=z;i++)cout<<"10001";
for(int i=1;i<=y;i++)cout<<"1001";
for(int i=1;i<=x;i++)cout<<"10";
}
}
signed main(){
cin.tie(0)->sync_with_stdio(0);
int T=1;
// cin>>T;
while(T--)solve();
return 0;
}
J - Two Binary Strings 问题
题意:
给出
01
序列,
A
,
B
给出01序列,A,B
给出01序列,A,B
定义
f
(
l
,
r
)
,如果
1
的个数大于等于长度一半,则为
1
,否则为
0
定义f(l,r),如果1的个数大于等于长度一半,则为1,否则为0
定义f(l,r),如果1的个数大于等于长度一半,则为1,否则为0
对于一个
k
,如果所有
i
满足
f
(
m
a
x
(
1
,
i
−
k
+
1
)
,
i
)
=
B
i
则称
k
合法
对于一个k,如果所有i满足f(max(1,i-k+1),i)=B_i则称k合法
对于一个k,如果所有i满足f(max(1,i−k+1),i)=Bi则称k合法
输出一个长度为
n
的二进制,为
1
表示第
k
位合法
输出一个长度为n的二进制,为1表示第k位合法
输出一个长度为n的二进制,为1表示第k位合法
思路:
- 转化为求前缀和,如果 s i − k ≤ s i 则 k 合法 转化为求前缀和,如果s_{i-k} \leq s_i 则k合法 转化为求前缀和,如果si−k≤si则k合法
- 对于每个 i 求出满足的前缀即可 对于每个i求出满足的前缀即可 对于每个i求出满足的前缀即可
- 可以用bitset优化暴力
- 使用一个val[i]存储前缀和值为i的下标集合,a集合维护当前大于sum[i]的下标集合,b集合维护小于等于的
- 注意到每次sum[i]值只会增加一或减少一,根据此信息更新i下标在哪或者i-1下标在哪个集合即可
#include<bits/stdc++.h>
using namespace std;
const int N=50010;
bitset<N> val[N],a,b,ans,all;
void solve(){
all.set();
int n;
cin>>n;
string s,t;
cin>>s>>t;
s=" "+s,t=" "+t;
vector<int> sum(n+1);
a.reset(),b.reset(),ans.set();
for(int i=0;i<=n;i++)val[i].reset();
for(int i=1;i<=n;i++)sum[i]=sum[i-1]+(s[i]=='1'?1:-1);
map<int,int> mp;
int idx=0;
auto id=[&](int x){
if(mp.count(x))return mp[x];
return mp[x]=++idx;
};
val[id(0)][0]=1;
b[0]=1;
for(int i=1;i<=n;i++){
if(sum[i]>sum[i-1])a^=val[id(sum[i-1])],b^=val[id(sum[i-1])];
else a^=val[id(sum[i])],b^=val[id(sum[i])];
if(t[i]=='1'){
bitset<N> tmp=a<<(n-i+1);
if(a[0])tmp|=(all>>(N-n+i-1));
ans&=tmp;
}
else{
bitset<N> tmp=b<<(n-i+1);
if(b[0])tmp|=(all>>(N-n+i-1));
ans&=tmp;
}
val[id(sum[i])][i]=1;
b[i]=1;
}
for(int i=n;i>=1;i--)cout<<ans[i];
cout<<"\n";
}
signed main(){
cin.tie(0)->sync_with_stdio(0);
int T=1;
cin>>T;
while(T--)solve();
return 0;
}