J BFS
传送系统两个操作
- 立即传送到 ( i + a i ) m o d n (i+a_i)\bmod n (i+ai)modn
- 调整 a i ← a i + 1 a_i\gets a_i+1 ai←ai+1,也就相当于传送到 a i + 1 a_i+1 ai+1的位置,到 a i a_i ai后再向前跳一个
BFS遍历找最短路
cpp
const int N=1e5+5,mod=1e9+7,inf=1e9+10;
int vis[N][2],dis[N][2];
void solve() {
memset(dis,0x3f,sizeof dis);
int n,x;cin>>n>>x;
vector<int>a(n+1);
forr(i,0,n-1)cin>>a[i];
queue<pii>q;
q.push({0,0});//{位置,是否可以向前跳}
//此时还没跳到a_i的地方 不能向前
dis[0][0]=0;
while (q.size())
{
auto [i,st]=q.front();
q.pop();
if(vis[i][st])continue;
vis[i][st]=1;
//向下一个点跳
int nxt=(i+a[i])%n;//跳到a_i
if(dis[nxt][1]>dis[i][st]+1){
dis[nxt][1]=dis[i][st]+1;
q.push({nxt,1});
}
if(st==1){//跳到这个点 可以+1
nxt=(i+1)%n;
if(dis[nxt][1]>dis[i][1]+1){
dis[nxt][1]=dis[i][1]+1;
q.push({nxt,1});
}
}
}
cout<<dis[x][1]<<endl;
}
I 贪心 dp
cpp
void solve(){
int a,b,m;cin>>a>>b>>m;
vector<int>dp(m+1,0);
dp[0]=2;//dp[i] 到时间i 两道具都用 用前面的转移到后面
/*两种思路
1.等到R刷新好了就用 周期是b
2.R刷新好了之后 等待H冷却好了再用R 周期是 ceil(b/a)*a=(b+a-1)/a *a
*/
int ans=0;
forr(i,0,m){
if(dp[i]){
ans=max(dp[i]+(m-i)/a,ans);//后面的不再用b
//向后转移
if(i+b<=m)dp[i+b]=max(dp[i+b],dp[i]+1+b/a);//+1是因为H冷却好了
if(i+(b+a-1)/a*a<=m)dp[i+(b+a-1)/a*a]=max(dp[i+(b+a-1)/a*a],dp[i]+1+(b+a-1)/a);
}
}
cout<<ans*160<<endl;
//废片 一开始只想了一种以b为周期的策略
// if(a>b){
// cnt=2+m/b;
// cout<<1<<' '<<cnt<<endl;
// }else {
// int rd=m/b,dis=m%b;
// int cnta=(b+a-1)/a;
// cout<<cnta<<' '<<rd<<endl;
// cnt=cnta*rd+(dis/a+1)+1;
// cout<<cnta*rd<<' '<<(dis/a+1)<<' '<<(rd+1)<<endl;
// cout<<cnt<<endl;
// // if(a==b){
// // cnt=2*(m/b+1);
// // cout<<2<<' '<<cnt<<endl;
// // }else{
// // int per=(b+a-1)/a;
// // cnt=per*(m/b+1)+1;
// // cout<<3<<' '<<cnt<<endl;
// }
}
D 思维 并查集 暴力
注意题设条件:可以保证 G 的最大度数最多为 3
诱导子图有n个点,红蓝总共最多有 3 n 2 3n\over 2 23n条边
满足合法,最少有 2 ( n − 1 ) 2(n-1) 2(n−1)
3 n 2 ≥ 2 ( n − 1 ) {3n\over 2}\geq 2(n-1) 23n≥2(n−1),得 n ≤ 4 n\leq 4 n≤4
枚举n暴力找点
cpp
const int N=1e5+2,mod=1e9+7,inf=1e9+10;
int fa[N],vis[N];
vector<pii>ve[N];
set<pii>e[N];
int find(int x){
return fa[x]==x?x:find(fa[x]);
}
void merge(int x,int y){
int fx=find(x),fy=find(y);
fa[fx]=fy;
}
int check(vector<int>v){//验证蓝边是否也联通
for(auto i:v)fa[i]=i;
//两两枚举红联通集中的点 在蓝边里合并 看是否能合成一个联通集
forr(i,0,3){
forr(j,0,3)if(e[1].count({v[i],v[j]}))merge(v[i],v[j]);
}
forr(i,1,3)if(find(v[i])!=find(v[0]))return 0;
return 1;
}
void dfs(int now,vector<int>&v){
vis[now]=1;
v.push_back(now);
if(v.size()==4)return;
for(auto [to,c]:ve[now]){
if(c==0&&vis[to]==0)dfs(to,v);//只找红边
}
}
//关键在于找出两种颜色最小联通的情况 边的数量2(m-1)
//根据题设每个点最多3条边 3m/2>=2(m-1) m<=4
void solve(){
int n,m;cin>>n>>m;
forr(i,1,m){
int u,v,c;
cin>>u>>v>>c;
ve[u].push_back({v,c}),ve[v].push_back({u,c});
e[c].insert({u,v});
}
int ans=0;
//sz=2,3
for(auto [u,v]:e[0]){
if(e[1].count({u,v})||e[1].count({v,u})){
ans++;//2
pii rnxt,bnxt;
rnxt=bnxt=pii(-1,-1);
for(auto [to,c]:ve[u])if(to!=v)rnxt={to,c};
for(auto [to,c]:ve[v])if(to!=u)bnxt={to,c};
if(rnxt.fir==-1||bnxt.fir==-1)continue;
if(rnxt.fir==bnxt.fir&&rnxt.sec!=bnxt.sec)ans++;//3
}
}
// sz=1
ans+=n;
// sz=4 暴力找四个点 验证是否合法
forr(i,1,n){
if(vis[i]==0){
vector<int>getv;dfs(i,getv);
// for(auto x:getv)cout<<x<<' ';cout<<endl;
if(getv.size()==4)ans+=check(getv);
}
}
cout<<ans<<endl;
}
A 思维 问题转化
cpp
const int N=4e3+2,mod=1e9+7,inf=1e9+10;
string s[N];
int a[N][N],nd[N][N];
//关键在于发现取与不取后值的变化
//-1如果先都取上(达到最小值) 不取就相当于+1
//取一个1也相当于+1
//关注从最小值到目标值需要多少个+1
void solve(){
int n;cin>>n;
vector<int>row(n+1),col(n+1);
forr(i,1,n){
cin>>s[i];
forr(j,0,n-1)a[i][j+1]=(s[i][j]=='-'?-1:1);
}
forr(i,1,n)cin>>row[i];
forr(i,1,n)cin>>col[i];
//统计每行-1的和
priority_queue<pii>q;//使用堆 优先填补需求+1多的
forr(i,1,n){
int cnt=0;
forr(j,1,n)if(a[i][j]<0)cnt+=a[i][j];
if(cnt>row[i])rno;//return no;
else {
q.push({row[i]-cnt,i});//需要多少个+1
}
}
//统计每列需要+1的位置 和每行需要+1的位置交叉
forr(j,1,n){
int cnt=0;
forr(i,1,n)if(a[i][j]<0)cnt+=a[i][j];
if(cnt>col[j])rno;
else if(col[j]-cnt>q.size())rno;//每列的+1不能满足需要的
vector<pii>tp;//把当前需要+1的行全遍历一遍
while(q.size())tp.push_back(q.top()),q.pop();
for(auto [d,id]:tp){
if(cnt<col[j]){
cnt++;
d--,nd[id][j]=1;//这里需要一个+1
}
if(d>0)q.push({d,id});//还没填满该行需要的1 放回去
}
}
if(q.size())rno;
cout<<"yes"<<endl;
forr(i,1,n){
forr(j,1,n){
if(nd[i][j]==1)cout<<(a[i][j]==1);//需要1且正好有1
else cout<<(a[i][j]==-1);//不需要1 把-1算上
}cout<<endl;
}
}
E 单调栈找逆序对 拓扑排序
如
a:1 3 2 (3->2)
b:3 2 1
逆序一定有边
cpp
const int N = 1e5+10, C = 1e6 + 10, mod =998244353, inf = 1e9 + 10;
vector<int>tr[N];
void solve()
{
int n;cin>>n;
vector<int>a(n+1,0),b(n+1,0),deg(n+1,0);
forr(i,1,n)cin>>a[i];
forr(i,1,n)cin>>b[i];
int ans=0;
//单调栈找逆序 逆序的一定有边
stack<int>st;
//小字典序
forr(i,1,n){
while (st.size()&&st.top()<a[i])st.pop();
if(st.size()&&st.top()>a[i]){
tr[st.top()].push_back(a[i]);
deg[a[i]]++;
ans++;
}
st.push(a[i]);
}
while(st.size())st.pop();
//大字典序 前面大->小的边在大字典序中不会产生逆序 不会重复计算
forr(i,1,n){
while (st.size()&&st.top()>b[i])st.pop();
if(st.size()&&st.top()<b[i]){
tr[st.top()].push_back(b[i]);
deg[b[i]]++;
ans++;
}
st.push(b[i]);
}
//成图后 大小字典序分别拓扑验证是否合理
int ac,bc;ac=bc=0;
int ai,bi;ai=bi=0;
vector<int>aj(n+1,0),bj(n+1,0);
priority_queue<int,vector<int>,greater<int>>qa;//小根堆
priority_queue<int>qb;//默认大根堆
auto degt=deg;
forr(i,1,n)if(deg[i]==0)qa.push(i),qb.push(i);
while (qa.size())
{
int now=qa.top();qa.pop();
aj[++ai]=now;
for(auto x:tr[now]){
if(--deg[x]==0)qa.push(x);
}
}
while (qb.size())
{
int now=qb.top();qb.pop();
bj[++bi]=now;
for(auto x:tr[now]){
if(--degt[x]==0)qb.push(x);
}
}
if((ai!=n||bi!=n)&&ans!=0)rno;
else if(aj!=a||bj!=b)rno;
else {
cout<<"Yes"<<endl<<ans<<endl;
forr(i,1,n){
for(auto x:tr[i]){
cout<<i<<' '<<x<<endl;
}
}
}
}
H 树形dp 组合数学 可重复排列
约束:选择停在子树now的车数量范围 s z n o w ≤ c u r ≤ s z n o w + d e p n o w sz_{now}\leq cur\leq sz_{now}+dep_{now} sznow≤cur≤sznow+depnow,多出来的可以向根节点推最多 d e p n o w dep_{now} depnow
cpp
const int N = 1e5+10, C = 1e6 + 10, mod =998244353, inf = 1e9 + 10;
vector<int>tr[N];
int sz[N],dep[N],fact[N],ifac[N];
int qpow(int x,int p=mod-2){
int res=1;
for(;p;p>>=1,(x*=x)%=mod)if(p&1)(res*=x)%=mod;
return res;
}
void init(int n){
fact[0]=1;
forr(i,1,n)fact[i]=fact[i-1]*i%mod;
ifac[n]=qpow(fact[n]);
reforr(i,0,n-1)ifac[i]=(i+1)*ifac[i+1]%mod;
}
vector<int>dp[N];//dp[i] i点的状态
void dfs(int now=1){
//本节点子树数据更新
sz[now]=1;
for(auto x:tr[now])dep[x]=dep[now]+1,dfs(x),sz[now]+=sz[x];
//子树合并
int L=0,R=0;//L,R是已合并子树的车数范围
static int g[N],tmp[N];//g已合并子树的方案数
g[0]=1;//now一个点的情况
for(auto x:tr[now]){
forr(i,L+sz[x],R+sz[x]+dep[x])tmp[i]=0;//新合并的用tmp存用到哪里初始化哪里
forr(i,L,R){
forr(j,sz[x],sz[x]+dep[x]){//枚举停在x子树中的车数
// sz[x](把x子树填满)<=j<=sz[x]+dep[x](可以向上填最多dep[x]个)
// 乘法原理 已合并的子树方案数*目前子树方案数
(tmp[i+j]+=g[i]*dp[x][j-sz[x]])%=mod;//用子树的dp更新本层 j-sz[x]是向上超出的层数
}
}
L+=sz[x],R+=sz[x]+dep[x];//更新子树范围
forr(i,L,R)g[i]=tmp[i];//新子树合并进去
}
//计算本节点dp:本节点方案数
dp[now].resize(dep[now]+1);
forr(ch,L,R)//枚举now的子树车数
forr(cur,sz[now],sz[now]+dep[now]){//枚举now下面的子树车数 更新方案数
if(ch<=cur)(dp[now][cur-sz[now]]+=g[ch]*ifac[cur-ch])%=mod;
//*ifac[cur-ch] cur-ch是选择停在now点的车数 用到了可重全排列
/*
可重全排列 eg. 1 1 2 1
有4!/3!=4个排列情况 n!/val! val是重复元素的个数
n!可以在最后计算总结果时乘上,进行答案的全排列
*/
}
}
void solve()
{
int n;cin>>n;
forr(i,2,n){
int f;cin>>f;
tr[f].push_back(i);
}
init(n);
dfs();
int ans=(dp[1][0]*fact[n])%mod;
cout<<ans<<endl;
}