2025年9月训练记录
2025/9/25
abc416_f
题意:树上选\(k\)条不相交的链,使得其贡献和最大.
可以考虑树形\(dp\).
考虑状态的设计如下:设\(dp_{u,i,0/1/2}\)表示当前选了以\(u\)为根的子树,且当前子树的根节点的状态为\(0/1/2\)的最大子树贡献.
- \(0:\)所选的子树不在任何一条链中
- \(1:\)所选的子树留了向上转移的接口,可以作为一条链向上拼接
- \(2:\)所选的子树不留向上转移的接口,不能作为链向上拼接
可以以树形背包的方式去做转移。
首先根据定义可以得到这样的转移:
- \(dp_{u,i+j,0}\leftarrow max(dp_{u,i,0}+max(dp_{v,j,0},dp_{v,j,1},dp_{v,j,2}))\)
- \(dp_{u,i+j,1}\leftarrow max(dp_{u,i,1}+max(dp_{v,j,0},dp_{v,j,1},dp_{v,j,2}))\)
- \(dp_{u,i+j,2}\leftarrow max(dp_{u,i,2}+max(dp_{v,j,0},dp_{v,j,1},dp_{v,j,2}))\)
考虑拼出一条留接口的链的转移:
- \(dp_{u,i+j,1}\leftarrow max(dp_{u,i,0}+dp_{v,j,1}+a_u)\)
同理,考虑不留接口的向上转移,值得注意的是,你把\(u\)节点同时接入两条边,会让链数减\(1\),故转移方程如下:
- \(dp_{u,i+j-1,2}\leftarrow max(dp_{u,i,1}+dp_{v,j,1})\)
注意保证\(dp\)的无后效性,可以用滚动数组处理一下.
时间复杂度\(O(nk^2)\).
9.26
感觉一整天都在想这一道题来着
首先比较套路地,把\(\text{Bob}\)的序列处理成\([1,2,3...n]\),同步更新\(Alice\)的序列和值域序列。
模拟一下两人取数的过程,你发现,当第\(i\)个位置的\(a_i\)被\(\text{Bob}\)取走后,那么满足如下条件的,\(Alice\)一定无法取得:\(j>i\and a_j<a_i\).
不妨设计\(dp_{i,j}\)表示当前考虑到了第\(i\)个位置,且被\(Bob\)取走的最大值,即\(\text{Alice}\)放弃的最大值,此时\(\text{Alice}\)取数能取的最大贡献.
设计推表转移,可以大致分为四种情况:
- \(a_i<j\)选\(a_i\),由上面得到的性质,这部分是不合法的,所以不做转移
- \(a_i<j\)不选\(a_i\),\(dp_{i-1,j}\rightarrow dp_{i,j}\)
- \(a_i>j\)选\(a_i\),\(dp_{i-1,j}+w_{a_i}\rightarrow dp_{i,j}\)
- \(a_i>j\)不选\(a_i\),\(max(dp_{i-1,j}+w_{a_i})\rightarrow dp_{i,a_i}\)
这样暴力转移是\(O(n^2)\)的.
但是,你发现转移分为了三段区间,\([0,a_i),a_i,(a_i,n]\),由上述转移可以发现,实际上就是对\([0,a_i)\)做区间加,对\(a_i\)做覆盖,对\((a_i,n]\)保持不变,所以可以维护一颗线段树,维护区间最值以及区间加辅助转移。
这样可以将时间复杂度优化到\(O(n\log n)\).
洗完澡补了一下网络赛的题
25CCPC网络赛C
首先问题可以直接转化成,进行\(n-1\)轮的连边,每次连边的代价是\((a_u+a_v)\% k\),问最小代价,因此把每个点的权值由\(a_i\)处理成\(a_i\% k\).
这是稠密图的最小生成树问题,我们可以用类似\(\text{Prim}\)算法的方式解决,首先考虑朴素版的算法,也就是每次枚举当前联通块,找到与当前联通块内最小的边权并加入.
本题有完全图的特殊性,而且所有边权都是可以计算得到的,对于某个点的延伸,要加入的边权,一定是当前与它没有联通的点中边权最小的那个,这样贪心是不会劣的,最小的边权的确定可以二分找第一个\(\geq k-a_u\)的位置,如没找到就加入第一个点.
我们用一个\(\text{set}\)维护当前还没有被加入大联通块的点,每次向大联通块中加入新的点时,按以上规则更新新边,这样的操作一共会进行\(n-1\)轮.
因此,我们得到\(O(n\log n)\)的做法.
2025/9/27
今天主要是和队友\(\text{vp}\).
晚上写\(\text{abc}\)状态前所未有的差.
abc_425f
考虑一个显然的状压\(dp\)如:\(dp_{i,sta}\)表示当前考虑到了第\(i\)个被放入的字符串,且当前选入的字符的二进制状态为\(sta\).
转移的时候,我们只要枚举当前状态从哪里转移过来,如果多个转移进入的状态的哈希值是相同的,只记一个就好了,那我们可以预处理\(2^n\)种不同的子序列的哈希值,然后开桶去重即可,不作任何优化,时间复杂度是\(O(n^2 2^n)\)的.
我认为本题的优化是挺启发式的,优化如下:
- 枚举了第\(i\)个被放入的状态时,有效的状态一定满足其二进制位恰好是\(i\)位,故我们可以按二进制编码的位数从小排序,然后双指针.
- 在转移的时候,我们只需要枚举当前状态所有二进制下的\(1\)位,可以用\(\text{lowbit}\)优化.
计算一下复杂度:
\((2^1+2^2+.....2^n)n=O(n2^{n+1})\).
常数很大,卡了过去,不知道是不是评测机波动.
代码实现如下.
/*
* ┏┓ ┏┓
* ┏┛┗━━━━━━━┛┗━━━┓
* ┃ ┃
* ┃ ━ ┃
* ┃ > < ┃
* ┃ ┃
* ┃... ⌒ ... ┃
* ┃ ┃
* ┗━┓ ┏━┛
* ┃ ┃ Code is far away from bug with the animal protecting
* ┃ ┃ 神兽保佑,代码无bug
* ┃ ┃
* ┃ ┃
* ┃ ┃
* ┃ ┃
* ┃ ┗━━━┓
* ┃ ┣┓
* ┃ ┏┛
* ┗┓┓┏━━━━━━━━┳┓┏┛
* ┃┫┫ ┃┫┫
* ┗┻┛ ┗┻┛
*/
#include<iostream>
#include<algorithm>
#include<set>
#include<map>
#include<vector>
#include<cmath>
#include<bitset>
#include<cstring>
#include<queue>
#include<iomanip>
#define mt make_tuple
//cout << setprecision(3) ; //输出3位小数,3.142
#define pb push_back
#define fi first
#define se second
using namespace std;
using ll=long long;
using i128=__int128;
typedef pair<int,int>pii;
typedef tuple<int,int,int>ti3;
const int N=3e5+10;
//读错题了,傻逼,你是人吗
//可以任意插 显然有类似状压dp的做法
//998244353
struct Hash{
private:
const int base=1331;//具体使用可以修改成双模或双base防止hack
const i128 mod=100000000000000049;
i128 mo(i128 x){
return (x%mod+mod)%mod;
}
public:
int n;//长度
i128 s=0;
Hash(){}
Hash(string str){//传入的参数不需要前面加空格,正常输入即可
n=str.length();
for(int i=1;i<=n;i++){
s=mo(s*base+(str[i-1]-'a'+1))%mod;//这里具体问题记得修改
}
}
ll get(){
return s;
}
};
const int mod=998244353;
int lowbit(int x){
return x&-x;
}
void Silverwolf(){
int n;
cin>>n;
string s;
cin>>s;
vector<int>hash(1<<n,-1);
vector<int>lsh;
for(int sta=1;sta<(1<<n);sta++){
string su;
for(int i=0;i<n;i++){
if(sta>>i&1){
su.pb(s[i]);
}
}
hash[sta]=Hash(su).get();
lsh.pb(hash[sta]);
}
lsh.pb(-1);
lsh.pb(-2);
sort(lsh.begin(),lsh.end());
lsh.erase(unique(lsh.begin(),lsh.end()),lsh.end());
for(int sta=0;sta<(1<<n);sta++){
hash[sta]=lower_bound(lsh.begin(),lsh.end(),hash[sta])-lsh.begin();
}
vector<int>bit;
for(int i=0;i<(1<<n);i++)bit.pb(i);
sort(bit.begin(),bit.end(),[&](int x,int y){
return __builtin_popcount(x)<__builtin_popcount(y);
});
vector<int>dp(1<<n,0);
dp[0]=1;
vector<bool>vis(1<<(n+1),false);
int l=1;
for(int i=1;i<=n;i++){
for(int j=l;j<(1<<n);j++){
l=j;
int sta=bit[j];
if(__builtin_popcount(sta)>i)break;
for(int j=sta,k;j;j-=1<<k){
k=__lg(j);
if(!(sta>>k&1))continue;
int nsta=sta^(1<<k);
if(!vis[hash[nsta]]){
dp[sta]=(dp[sta]+dp[nsta])%mod;
vis[hash[nsta]]=true;
}
}
for(int j=sta,k;j;j-=1<<k){
k=__lg(j);
if(!(sta>>k&1))continue;
int nsta=sta^(1<<k);
vis[hash[nsta]]=false;
}
}
}
cout<<dp[(1<<n)-1];
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
// int T;cin>>T;while(T--)
Silverwolf();
//愿艾利欧三度为你窥见,令你的生命永不停歇,骇入永远成功,游戏永远胜利。
//游戏就只是为了游戏,仅此而已
//acm这种东西,三两下搞定就好啦
return 0;
}
[2024ICPC沈阳M]
赛时出的思路很快,但是最后没想出判非\(0\)环的方法.
考虑由\(a\% n\)向\((a+b)\% n\) 建边,若能为无穷集当且仅当出现了非\(0\)权环,或当前点能够指向非\(0\)权环.
不妨缩点后跑\(dp\),那么现在问题瓶颈只在于判定\(0\)权环,我们考虑对于强连通分量内的点\(\text{bfs}\),如果出现了冲突,则说明有非\(0\)权值的环。
时间复杂度\(O(n+m+q)\),感觉代码实现的很丑。
#include<bits/stdc++.h>
//#define int long long
#define pb push_back
#define mt make_tuple
#define fi first
#define se second
using ll=long long;
const int N=5e5+3;
using namespace std;
typedef pair<int,int>pii;
typedef pair<string,int>psi;
typedef tuple<int,int,int>ti3;
class Tarjan{
public:
int n,idx,top;
vector<int> dfn,low,col,into;
vector<ti3> edg;
vector<vector<int>> e;
vector<vector<int>> scc;
vector<vector<pii>>scc2;//把环上的边记录下来
vector<int> stk;
bitset<N>in;
vector<int>tag;//标记某些位置为1
vector<int>dp;
//vector<int>cnt_edge;//一个联通块中的边的数量
//vector<int>cnt_point;
ll ans=1;
Tarjan(int n):n(n){
idx=0,top=0;
e.resize(n+1);
scc.resize(n+1);
scc2.resize(n+1);
dfn.assign(n+1,0);
low.assign(n+1,0);
col.assign(n+1,0);
into.assign(n+1,0);
stk.assign(n+1,0);
//in.assign(n+1,0);
tag.assign(n+1,0);
dp.assign(n+1,0);
//cnt_edge.assign(n+1,0);
//cnt_point.assign(n+1,0);
}
void add(int u,int v,int w){
e[u].pb(v);
edg.pb(mt(u,v,w));
}
void tarjan(){
for(int i=0;i<n;++i){
if(!dfn[i])
tarjan(i);
}
}
void tarjan(int u){
dfn[u]=low[u]=++idx;
stk[++top]=u;
in[u]=true;
for(int &v:e[u]){
if(!dfn[v])
tarjan(v),low[u]=min(low[u],low[v]);
else
if(in[v])
low[u]=min(low[u],dfn[v]);
}
if(dfn[u]==low[u]){
while(stk[top]!=u){
in[stk[top]]=false;
col[stk[top--]]=u;
//cnt_point[u]++;
}
col[stk[top]]=u;
//cnt_point[u]++;
in[stk[top--]]=false;
}
}
void check_cir(){
vector<bool>vis(n+1,false);//bfs 一下
vector<ll>dis(n+1,0);
auto bfs=[&](int u)->bool{//判是否有非零环
queue<int>q;
q.push(u);
vis[u]=true;
while(q.size()){
//assert(q.size()<5e7);
auto now=q.front();
q.pop();
for(auto &[v,w]:scc2[now]){
if(!vis[v]){
vis[v]=true;
q.push(v);
dis[v]=dis[now]+w;
}else{
if(dis[now]+w!=dis[v]){
return true;
}
}
}
}
return false;
};
for(int i=0;i<n;i++){
if(col[i]==i){
bool ok=bfs(i);
if(ok)tag[i]=1;
}
}
}
void get_scc(){
for(auto &[u,v,w]:edg){
// cout<<col[u]<<' '<<col[v]<<'\n';
if(col[u]==col[v]){
// tag[col[u]]+=w;
scc2[u].pb({v,w});
// scc2[v].pb({u,w});
//cnt_edge[col[u]]++;
}
else{
scc[col[v]].pb(col[u]);//建反边
into[col[u]]++;
}
}
}
void DP(){
queue<int>q;
for(int i=0;i<n;i++){
if(col[i]==i&&into[i]==0)q.push(i);
}
while(q.size()){
//assert(q.size()<5e7);
auto u=q.front();
q.pop();
dp[u]=max(dp[u],tag[u]);
for(int v:scc[u]){
dp[v]=max(dp[v],dp[u]);
if(--into[v]==0)q.push(v);
}
}
}
void work(){
tarjan();
get_scc();
check_cir();
DP();
// for(int i=0;i<n;i++)cout<<cnt[i]<<' ';cout<<"\n";
// for(int i=0;i<n;i++)cout<<col[i]<<' ';cout<<'\n';
// for(int i=0;i<n;i++)cout<<tag[i]<<' ';cout<<"\n";
// for(int i=0;i<n;i++)cout<<dp[i]<<' ';cout<<"\n";
}
};
int get(int x,int m){
return (x%m+m)%m;
}
void Silverwolf(){
int n,m,q;
cin>>n>>m>>q;
Tarjan ta(n);
for(int i=1;i<=m;i++){
int a,b;
cin>>a>>b;
if(b==0)continue;
int u=get(a,n);
int v=get(a+b,n);
// cout<<u<<' '<<v<<"\n";
ta.add(u,v,b);
}
ta.work();
for(int i=1;i<=q;i++){
int u;cin>>u;
u=get(u,n);
if(ta.dp[ta.col[u]])cout<<"Yes\n";
else cout<<"No\n";
}
}
signed main(){
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
// int T;cin>>T;while(T--)
Silverwolf();
return 0;
}
2025/9/29
2024杭州站M
首先转化一下问题.
对于每个区间\([l,r]\),设最小值的位置在\(pos\),都满足\((a_{pos+k})|(a_j+k)[l\leq j\leq r ]\)成立.
每个区间都当且仅与最小值有关,考虑值域分治,每次首先找到最小值的位置\(pos\),并划分其统辖区间\([l,r]\).
上述式子可以做一个如下等价:
即\(a_{pos}+k\)必须是\(gcd(a_{l+1}-a_{l},a_{l+2}-a_{l+1},....,a_{r}-a_{r-1})\)的因子
递归上述过程,可以用笛卡树实现,用\(ST\)表快速维护区间内的\(\text{gcd}\)即可.
时间复杂度为\(O(n\log n\log A+nd(A))\),其中\(d(x)\)表示因子数量.
代码实现的真的非常丑。板子抄错猛猛\(\text{WA}\).
#include<bits/stdc++.h>
#define mt make_tuple
#define fi first
#define se second
using ll=long long;
using namespace std;
typedef pair<int,int>pii;
typedef tuple<int,int,int>ti3;
//最值分治听起来挺有道理的
//最值分治+区间gcd 之类的东西
template<class T>
class ST{
public:
vector<vector<T>>gd;
vector<T>a;
vector<int>lg2;
int n;
ST(int n,vector<T>a):n(n),a(a){
gd.assign(20,vector<T>(n+1,0));
// mn.assign(20,vector<T>(n+1,0));
lg2.assign(n+1,-1);
for(int i=1;i<=n;i++)lg2[i]=lg2[i/2]+1;
int k=lg2[n];
for(int i=0;i<=n;i++){
gd[0][i]=gd[0][i]=a[i];
}
for(int j=1;j<=k;j++){
for(int i=0;i<=n-(1<<j)+1;i++){
gd[j][i]=__gcd(gd[j-1][i],gd[j-1][i+(1<<(j-1))]);
}
}
}
T Gcd(int l,int r){
int k=lg2[r-l+1];
return __gcd(gd[k][l],gd[k][r-(1<<k)+1]);
}
};
const int inf=1e9+7;
class CatlanTree{//笛卡尔树板子 默认以最小值为根
public:
vector<int>p,fa;
vector<vector<int>>ch;
int n,root,k;
CatlanTree(){
cin>>n>>k;
p.resize(n+1);
fa.assign(n+1,0);
ch.assign(n+1,vector<int>(2,0));
for(int i=1;i<=n;i++)cin>>p[i];
vector<int>stk(n+2);
int top=0;
for(int i=1;i<=n;i++){
int lst=0;
while(top&&p[stk[top]]>=p[i]){
lst=stk[top];
top--;
}
if(top){
fa[i]=stk[top];
ch[stk[top]][1]=i;
}
if(lst){
ch[i][0]=lst;
fa[lst]=i;
}
stk[++top]=i;
}
for(int i=1;i<=n;i++)if(fa[i]==0)root=i;
}
void work(){
vector<int>cha(n+1,0);
set<int>val;
for(int i=2;i<=n;i++)cha[i]=abs(p[i]-p[i-1]);
ST<int> st(n,cha);
bool ok=true;
auto dfs=[&](auto &&dfs,int u)->ti3{
if(u==0)return mt(n,0,0);
auto [l1,r1,mn1]=dfs(dfs,ch[u][0]);
auto [l2,r2,mn2]=dfs(dfs,ch[u][1]);
int l=min({l1,l2,u});
int r=max({r1,r2,u});
int len=r-l+1;
if(len!=1){
int gcd=st.Gcd(l+1,r);
//枚举gcd的因子吧,感觉
int mn=p[u];
if((mn1==0||mn==mn1)&&(mn==mn2||mn2==0)){
return {l,r,p[u]};
}
if(ok){
//枚举因子
for(int x=1;x<=gcd/x;x++){
if(gcd%x==0){
if(x-mn>0)val.insert(x-mn);
if(gcd/x-mn>0)val.insert(gcd/x-mn);
}
}
ok=false;
}
vector<int>del;//待删除对象
for(auto x:val){
if(__gcd(gcd,mn+x)!=mn+x)del.push_back(x);
}
for(auto d:del)val.erase(d);
}
return {l,r,p[u]};
};
dfs(dfs,root);
ll ans=0,cnt=0;
int mn=*min_element(p.begin()+1,p.end());
int mx=*max_element(p.begin()+1,p.end());
if(ok){
//特判全相等
cnt=k;
ans=1ll*k*(k+1)/2;
}
for(auto x:val){
if(x<=k){
cnt++;
ans+=x;
}
}
cout<<cnt<<' '<<ans<<'\n';
}
};
void Silverwolf(){
CatlanTree tr;
tr.work();
// int n,k;
// cin>>n>>k;
// vector<int>a(n+1,0);
// for(int i=1;i<=n;i++)cin>>a[i];
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int T;cin>>T;while(T--)
Silverwolf();
return 0;
}
2024成都I
首先,将\(a_i>a_{i+1}\)的位置标记成\(1\),其中\(1\leq i<n\),发现任意一个段中除了段尾可以被标记成\(1\),其他位置都不能被标记成\(1\),注意对于每个\(k\),其段尾的位置一定是\(k,2k,3k....\lfloor\frac{n}{k}\rfloor k\),因此,对于一个被标记为\(1\)的位置\(p\),其可能合法的位置有且仅有\(p\)的因子,若有很多这样的位置,维护出其\(\text{gcd}\)后枚举因子即可.
动态维护\(gcd\),用线段树维护,时间复杂度\(O(q\log n)\).
枚举因子\(O(q\sqrt n)\).
到这里本题也已经可以通过了.
可以欧拉筛优化一下,这样复杂度就是\(O(q\log n)\)了.
今天还零零散散写了几道题,感觉比较水,不想写题解了(
posted on 2025-09-26 00:11 __Silverwolf 阅读(24) 评论(0) 收藏 举报
浙公网安备 33010602011771号