Logo lzx 的博客

博客

标签
暂无

ABC397F 题解

本文章由 WyOJ Shojo 从洛谷专栏拉取,原发布时间为 2025-03-17 14:54:16

可以看看这个 CF833B

题意

给你一个序列,你要把这个序列分成连续的三段,每一段的权值为每一段中不同的数的个数,问你这个序列的最大权值是多少。

思路

首先发现可以预处理来第一段和最后一段的权值,设为 $ans1$ 和 $ans2$,$ans1_i$ 即为从 $1$ 到 $i$ 这一段的权值,$ans2_i$ 即为从 $i$ 到 $n$ 这一段的权值,于是我们就有了 $O(n^2)$ 的 dp 做法。

设 $f_i$ 表示以 $i$ 为第二段结尾的最大权值,答案即为 $\max f_i$。

$$f_i=\max_{j=1}^{i-1} ans1_j+val(j+1,i)+ans2_j$$

$val(i,j)$ 即为从 $i$ 到 $j$ 这一段的权值。

考虑优化。

我们发现,难点在于如何计算 $val(j+1,i)$。

考虑说我们可以把每一个 $ans1$ 扔到线段树上,一个点 $j$ 就表示说以 $j$ 为第一段的结尾最大权值,然后我们枚举第二段的结尾 $i$,每次 dp。

我们记 $a_i$ 上一次出现的位置为 $la_i$。

考虑说我们每个数只对于起点在 $[la_i+1,i]$ 这个闭区间的所有区间有 $1$ 的贡献,也就是线段树上 $[la_i,i-1]$ 这个区间。

最后一段的直接加上即可。

然后每次转移找最大值。

复杂度 $O(n\log n)$。

不理解可以结合代码食用。

Code

#include <bits\/stdc++.h>
#define endl '\n'
#define int long long
#define fi first
#define se second
using namespace std;
const int N=3e5+10;
const int inf=0x3f3f3f3f3f3f3f3f;
int n;
int a[N];
int ans1[N],ans2[N];
unordered_map<int,int> mp;
int ans;
int pre[N],nxt[N];
struct Node
{
	int l,r,w;
	int lt;
}tr[N<<2];
void build(int rt,int l,int r)
{
    tr[rt].l=l,tr[rt].r=r;
    if(l==r)
    {
        tr[rt].w=ans1[l];
        return ;
    }
    int mid=(tr[rt].l+tr[rt].r)>>1;
    build(rt<<1,l,mid);
    build(rt<<1|1,mid+1,r);
    tr[rt].w=max(tr[rt<<1].w,tr[rt<<1|1].w);
}
void pushdown(int rt)
{
    int &tag=tr[rt].lt;
    tr[rt<<1].w+=tag;
    tr[rt<<1|1].w+=tag;
    tr[rt<<1].lt+=tag;
    tr[rt<<1|1].lt+=tag;
    tag=0;
}
void add(int rt,int l,int r,int k)
{
    if(tr[rt].l>=l&&tr[rt].r<=r)
    {
        tr[rt].w+=k;
        tr[rt].lt+=k;
        return ;
    }
    pushdown(rt);
    int mid=(tr[rt].r+tr[rt].l)>>1;
    if(l<=mid) add(rt<<1,l,r,k);
    if(r>mid) add(rt<<1|1,l,r,k);
    tr[rt].w=max(tr[rt<<1].w,tr[rt<<1|1].w);
}
int check(int rt,int l,int r)
{
    if(tr[rt].l>=l&&tr[rt].r<=r) return tr[rt].w;
    pushdown(rt);
    int mid=(tr[rt].l+tr[rt].r)>>1;
    int res=0;
    if(l<=mid) res=max(res,check(rt<<1,l,r));
    if(r>mid) res=max(res,check(rt<<1|1,l,r));
    return res;
}
signed main()
{
	\/\/freopen(".in","r",stdin);
    \/\/freopen(".out","w",stdout);
	cin.tie(0);
	cout.tie(0);
	ios::sync_with_stdio(false);
	cin>>n;
	for(int i=1;i<=n;i++) cin>>a[i];
	for(int i=1;i<=n;i++)
	{
	    pre[i]=mp[a[i]];
	    mp[a[i]]=i;
	    ans1[i]=mp.size();
    }
    mp.clear();
    for(int i=n;i>=1;i--)
    {
        nxt[i]=mp[a[i]];
        mp[a[i]]=i;
        ans2[i]=mp.size();
    }
    build(1,1,n);
    for(int i=2;i<n;i++)
    {
        add(1,(pre[i]?pre[i]:1),i-1,1);
        ans=max(check(1,1,i-1)+ans2[i+1],ans);
    }
    cout<<ans;
    return 0;
}

2024.2.2

本文章由 WyOJ Shojo 从洛谷专栏拉取,原发布时间为 2024-02-02 10:44:52

上课

坑:A*,CDQ分治,dfs

P1008 [NOIP1998 普及组] 三连击

废题

**洛谷

「LibreOJ NOIP Round #1」DNA 序列

A,C,G,T化为4进制,双指针实现,开桶统计

4^10约为1e6

[SCOI2005] 栅栏

dfs+剪枝

“随便剪剪枝说不定就过了”

P4467 [SCOI2007] k短路

「JOISC 2014 Day2」水壶

P2324 [SCOI2005] 骑士精神

“加了个剪枝就过了”

P3810 【模板】三维偏序(陌上花开)

P4093 [HEOI2016/TJOI2016] 序列

LCA

本文章由 WyOJ Shojo 从洛谷专栏拉取,原发布时间为 2024-02-03 10:11:16

LCA(最近公共祖先)

【模板】最近公共祖先(LCA)

​最近公共祖先_百度百科

定义

有根树上,两点的祖先有公共部分,这些点叫做他们的公共祖先,而其中深度最深的点,叫作它们的最近公共祖先(​LCA⁡,Lowest Common Ancestors)。

求解

暴力

先自上到下将a节点的所有祖先打一个标记,再自下至上找第一个打了标记的点,即为二者的LCA

倍增

在用st表处理区间最值查询时,我们用到了倍增的思想,那么在求解LCA时,是否可以也用倍增呢?答案是可以的。 定义$fa_{i,j}$是i的$2^j$个祖先(可用递推),$dep_i$表示$i$节点的深度(dfs) 由于一个整数一定能化作二进制,那么我们每次向上跳到a节点的2的若干次方祖先处,一定能跳到a,b,的LCA处 对于b同理 那么可以先将a,b跳到同一深度处,再让两个点同时向上跳,直到两个点的父亲相同

代码

#include <bits\/stdc++.h>
#define endl '\n'
using namespace std;
const int N=5e5+10;
int n,m,s; 
vector<int > G[N];
int dep[N];
int fa[N][21];
void dfs(int u,int _fa)\/\/求深度 
{
    fa[u][0]=_fa;
    dep[u]=dep[_fa]+1;
    for(auto v:G[u])
    {
        if(v==_fa)
        {
            continue;
        }
        dfs(v,u);
    }
}
int lca(int x,int y)
{
    if(dep[x]<dep[y])\/\/便于操作 
    {
        swap(x,y);
    }
    for(int i=20;i>=0;i--)\/\/从大到小枚举,能跳就跳,因此不会出现越界或没跳到的情况,后面同理 
    {
        if(dep[fa[x][i]]>=dep[y])
        {
            x=fa[x][i];
        }
    }
    if(x==y)\/\/如果跳到同一个点上,则这个点为LCA 
    {
        return x;
    }
    for(int i=20;i>=0;i--)\/\/一起跳 
    {
        if(fa[x][i]!=fa[y][i])\/\/当二者的同辈祖先不一致时,向上跳 
        {
            x=fa[x][i];
            y=fa[y][i];
        }
    }
    return fa[x][0];\/\/由于fa[x][0]中放的是父亲, 因此要返回二者的父亲 
}
signed main()
{
	\/\/freopen(".in","r",stdin);
    \/\/freopen(".out","w",stdout);
    ios::sync_with_stdio(false);\/\/加快读入 
    cin>>n>>m>>s;
    for(int i=1;i<n;i++)
    {
        int u,v;
        cin>>u>>v;
        G[u].push_back(v);
        G[v].push_back(u);\/\/存树 
    }
    dfs(s,0);
    for(int j=1;j<=20;j++)
    {
        for(int i=1;i<=n;i++)
        {
            fa[i][j]=fa[fa[i][j-1]][j-1]; \/\/递推  i的2^j次祖先即为i的2^(j-1)祖先的2^(j-1)次祖先(2^(j-1)+2^(j-1)=2^j) 
        }
    }
    while(m--)
    {
        int x,y;
        cin>>x>>y;
        cout<<lca(x,y)<<endl;
    }
    return 0;
}

欧拉序

欧拉序

类似于dfs序,我们每访问到这个点(包括回溯),就在序列后加上这一项 由于我们每一个孩子回溯时,都会访问一次父亲,因此,欧拉序的长度是n+n1=2n1

与LCA

当我们dfs访问到a,b,的LCA时,继续遍历,一定会遍历到a,b且a,b不在同一子树上(反证法,如果在,这个点就不是LCA了) 所以,a,b(第一次出现在欧拉序中)之间的欧拉序一定会回溯到LCA,且LCA一定为其中深度最小的点 因此先求出全树的欧拉序,然后用st表储存2的若干次方内深度最小的点,每次询问时,查询a,b之间的深度最小的点

代码

#include <bits\/stdc++.h>
#define endl '\n'
using namespace std;
const int N=5e5+10;
vector<int> G[N];
int n,m,s;
int f[2*N][22];
int oula[2*N];
int cnt;
int dep[N];
int vis[2*N];
void dfs(int u,int fa)\/\/处理深度和欧拉序 
{
    cnt++;
    dep[u]=dep[fa]+1;
    oula[cnt]=u;
    vis[u]=cnt;\/\/记录第一次出现的欧拉序的下标 
    for(auto v:G[u])
    {
        if(v==fa)
        {
            continue;
        }
        dfs(v,u);
        cnt++;
        oula[cnt]=u;
    }
}
void build()\/\/处理st表 
{
    for(int i=1; i<=cnt; i++)
	{
		f[i][0]=oula[i];
	}
	for(int j=1; j<=20; j++)
	{
		for(int i=1; i+(1<<j)<=cnt; i++)
		{
			int t1=f[i][j-1];
			int t2=f[i+(1<<(j-1))][j-1];
			if(dep[t1]<dep[t2])
			{
			    f[i][j]=t1;
            }
            else
            {
                f[i][j]=t2;
            }
		}
	}
}
int st(int l,int r)\/\/查询l,r之间的深度最小的点 
{
    int p=log2(r-l+1);
    if(dep[f[l][p]]>dep[f[r-(1<<p)+1][p]])
    {
        return f[r-(1<<p)+1][p];
    }
    else
    {
        return f[l][p];
    }
}
int lca(int l,int r)\/\/求LCA 
{
    if(vis[l]>vis[r])
    {
        swap(l,r);
    }
    l=vis[l];
	r=vis[r];
	return st(l,r);
}
signed main()
{
	\/\/freopen(".in","r",stdin);
    \/\/freopen(".out","w",stdout);
    cin>>n>>m>>s;
    for(int i=1;i<n;i++)
    {
        int x,y;
        cin>>x>>y;
        G[x].push_back(y);
        G[y].push_back(x);
    }
    dfs(s,0);
    build();
    for(int i=1;i<=m;i++)
    {
        int x,y;
        cin>>x>>y;
        cout<<lca(x,y)<<endl;
    }
    return 0;
}

2024.2.3

本文章由 WyOJ Shojo 从洛谷专栏拉取,原发布时间为 2024-02-03 10:35:40

STL

链表list

vector

set

multiset

unordered_set

map/unordered_map

queue

deque 慎用,常数巨大

priority_queue

st表,RMQ

lca

倍增

st表 (用欧拉序)

四毛子,RMQ

四毛子算法(Method of Four Russians)的一些简单应用

笛卡尔树

左偏树

树状数组

“大家都会”

线段树

二维树状数组

哈希

unordered_map;

cc_hash_table

gp_hash_table

字典树

平衡树

splay,treap,替罪羊树(红黑树,val不常用没用)

kmp

番外

“你为什么这么自信” “因为我过了”

“出题人用脚做数据”

2024.2.4+2024.2.5

本文章由 WyOJ Shojo 从洛谷专栏拉取,原发布时间为 2024-02-04 10:37:13

DP

前置

最难点在于看出来是dp

(易与网络流混淆)

状态,转移,初始化

状态最基础(关键)

转移:状态之间的关系(简单)

初始化:状态的边界条件

设计状态:找到题目中所有在变化的量,将其作为状态(先塞再删)

不能用状态以外的信息

eg:数字三角形上,求走到最后%100最大为多少

[tyvj-1076] - 数字三角形2

不同于数字三角形原题,目前最优不保证一直最优

(所有dp的错误都来自状态不够)

所以再加一维状态

$f_{i,j,k}$ 表示在 $( i , j )$ 位置上,模数为 $k$ 是否可能

转移方式

用别人求自己

用自己求别人

(博弈论dp)记忆化搜索(不好for的时候)

分类

有模版:背包,区间,树形,数位,状压,插头(不考),排列,博弈论

一般dp(更难)

eg:Paint Pearls

$n^2$ dp $f_i=f_j+[j+1,i]$中不同颜色的种数

$n\sqrt{n}$ dp(相同颜色的区间越长越好,最大代价为n,则最多$\sqrt{n}$种不同颜色,双指针处理区间)

排列dp

$n!$ 种排列中,满足要求的有多少

转移方法

把所有数从小到大或从大到小插入的过程

$f_i$ 表示 $i$ 已经被插入

枚举插入的位置,计算方案

eg $[1,n]$ 所有的排列中,逆序对数为偶数的数量

$f_{i,j}$表示,$[1,i]$已经被插入,当前j个逆序对的方案数

第i+1个数,插入k位置,会与其后的所有数形成逆序对

$f_{1,0}=1$

cin>>n;
f[1][0]=1;
for(int i=1;i<=n;i++)
{
	for(int j=0;j<=i*(i-1)\/2;j++)\/\/逆序对数最多为每个数都 与后面的数 形成逆序对的情况 
	{
		for(int k=0;k<=i;k++)
		{
			f[i+1][j+i-k]+=f[i][j];
		}
	}
}

针对本题优化: j:0/1

cin>>n;
f[1][0]=1;
for(int i=1;i<=n;i++)
{
	for(int j=0;j<=1;j++)
	{
		for(int k=0;k<=i;k++)
		{
			f[i+1][(j+i-k)&1]+=f[i][j];
		}
	}
}

可优化到O(n)

O(1)

$n!/2$ (数学结论)

eg

Little Elephant and Movies

$f_{i,j}$ 表示 $[i,n]$插入,激动值为j的方案数

插入到前面,激动值+1; 插入到后面,激动值不变

背包

01背包

01背包 采药

$f_{i,j}$ 前 $i$ 个物品考虑完毕,用了 $j$ 的体积的最大价值

$f_{i,j}=max(f_{i-1,j},f_{i-1,j-v_i}+w_i)$

完全背包

状态一样,枚举个数

优化

由于不关心到底选了多少个

所以$f_{i,j}$ 不从 $f_{i-1,j-v_i}$ 转移,从 $f_{i,j-v_i}$ 转移

将斜线拆为一竖一横

for(int i=1;i<=n;i--)
{
	for(int j=0;j<m;j++)
		{
		f[i][j]=f[i-1][j];
		if(j>=v[i])
		{
			f[i][j]=max(f[i][j],f[i][j-v[i]]+w[i]);
		}
	}
}

多重背包

每个物品有限个

(想办法将未知变为已知)

(可能的出新题方法:TA+TB=TC)

将物品个数进行二进制拆分

eg: 17=1+2+4+8+2

转化为01背包

复杂度:$O(nm\log_2{n})$

for(int i=1;i<=cnt;i++)
{
	int tiji,jiazhi,geshu;
	cin>>tiji>>jiazhi>>geshu;
	int k=1;
	while(k<=geshu)
	{
		n++;
		v[n]=tiji*k;
		w[n]=jiazhi*k;
		geshu-=k;
		k*=2;
	}
	if(geshu!=0)
	{
		n++;
		v[n]=tiji*geshu;
		w[n]=jiazhi*geshu;
	}
}

P4095 [HEOI2013] Eden 的新背包问题

正着跑一遍,倒着跑一遍,每次不算这个物品的区间,将两个区间拼起来

数位dp

eg给出l,r,求出$[l,r]$有几个数(不能 $r-l+1$ )

$[l,r]=[0,r]-[0,l-1]$

转移方法

从高位开始,一位一位填数,枚举下一位填什么

状态

$f_{i,j(0/1)}$ 表示已经填了i位,是否等于限制的方案数

int calc(int z)\/\/计算0~z所有数的数位之和 
{
	if(z==0)
	{
		return 1;
	}
	int n=0;
	while(z)
	{
		n++;
		x[n]=z%10;
		z\/=10;
	}
	memset(f,0,sizeof(f));\/\/转化为前缀和,因此要清空 
	f[n+1][1]=1;\/\/只有一种方法,填 0 
	for(int i=n;i>=1;i--)\/\/当前要填y[i]的值,此时已经填了y[i+1~n] 
	{
		for(int j=0;j<=1;j++)\/\/填y[i]之前,是<还是= 
		{ 
			int up=9;\/\/能填的最大值
			if(j==1)
			{
				up=x[i];
			}
			for(int k=0;k<=up;k++)
			{
				f[i][(j==1)&&(k==up)]+=f[i+1][j];
			}
		}
	}
	return f[1][0]+f[1][1];
}

eg 计算 $[l,r]$ 的数位和

int calc(int z)\/\/计算0~z所有数的数位之和 
{
	if(z==0)
	{
		return 1;
	}
	int n=0;
	while(z)
	{
		n++;
		x[n]=z%10;
		z\/=10;
	}
   memset(f,0,sizeof(f));\/\/转化为前缀和,因此要清空 
	f[n+1][1]=1;\/\/只有一种方法,填 0 
	for(int i=n;i>=1;i--)\/\/当前要填y[i]的值,此时已经填了y[i+1~n] 
	{
		for(int j=0;j<=1;j++)\/\/填y[i]之前,是<还是= 
		{ 
			int up=9;\/\/能填的最大值
			if(j==1)
			{
				up=x[i];
			}
			for(int k=0;k<=up;k++)
			{
				f[i][(j==1)&&(k==up)]+=f[i+1][j];
				w[i][(j==1)&&(k==up)]+=f[i+1]*k+w[i+1][j];
			}
		}
	}
	return w[1][0]+w[1][1];
}

windy数

#include<bits\/stdc++.h>
#define endl '\n'
#define int long long
using namespace std;
int a,b;
int f[15][2][15];
int x[15];
int calc(int z)
{
	if(z==0)
	{
		return 0;
	}
	int n=0;
	memset(x,0,sizeof(x));
	while(z)
	{
		n++;
		x[n]=z%10;
		z\/=10;
	}
	memset(f,0,sizeof(f));
	for(int i=1;i<=x[n];i++)
	{
		f[n][(i==x[n])][i]++;
	}
	for(int i=n-1;i>=1;i--)\/\/填好所有最高位的可能情况,防止前导0; 
	{
		for(int j=1;j<=9;j++)
		{
			f[i][0][j]++;
		}
	}
	for(int i=n;i>=1;i--)
	{
		for(int j=0;j<=1;j++)
		{ 
			for(int k=0;k<=9;k++)
			{
				int up=9;
				if(j)
				{
					up=x[i];
				}
				for(int l=0;l<=up;l++)
				{
					if(abs(k-l)>=2)
					{
						f[i][(j==1)&&(l==up)][l]+=f[i+1][j][k];
					}
				}
			}
		}
	}
	int res=0;
	for(int j=0;j<=1;j++)
	{
		for(int k=0;k<=9;k++)
		{
			res+=f[1][j][k];
		}
	}
	return res;
}
signed main()
{
	cin>>a>>b;
	int ans=calc(b)-calc(a-1);
	cout<<ans;
	return 0;
}

Blinker 的仰慕者

$f_{i,j,k}$ 分别表示位数,是否有当前位限制,积为k的方案数

显然要炸

由于$ 10 $以内的数质因数分解只有2,3,5,7

$f_{i,0/1,a,b,c,d}$分别表示位数,是否有限制,积为 $2^a\times3^b\times5^c\times7^d$ 的方案数

但是还是过不了

但是 $a,b,c,d$ 不能同时取最大,肯定有大量不合法的

将合法的 $2^a\times3^b\times5^c\times7^d$ 存在z数组中(大约3万个)

将4维压回一维

这样就能过了

区间dp

石子合并(弱化版)

石子合并

特征(第一类区间dp,不绝对)

合并,相邻

方法

枚举分界点

(注意,按区间大小从小到大转移,先长度,再区间,否则就爆了)

第二类

eg 给定字符串,求回文子序列的数量(HDU4632)

$f_{l,r}=f_{l+1,r}+f_{l,r-1}-f_{l+1,r-1}$

$if( s_l == s_r ) f_{l,r}+=f_{l+1,r-1}$

方法

扔掉左端点和右端点

关键词(不绝对)

子序列

树形dp

在树上做的dp

eg 给定n个点的树,求这棵树有多少点

$f_i$ 表示以 $i$ 为根的子树内,有多少点

转移

把所有儿子的信息整合

eg 给定一棵n个点的树,每边有长度,$dist(i,j)$ 表示i到j的路径长度,求$\sum\limits_{i=1}^{n}\sum\limits_{j=1}^{n}dist(i,j)$

暴力求肯定炸

我们只关心一条边经过了几次

$(i,j)$ 肯定一个在子树内,一个在子树外

将内外的点数相乘,全部加起来

答案再乘2; ($i,j$ 都是1~n循环,$(i,j)$ 一次,$(j,i)$ 一次)

eg

求$max dist(i,j)$

求最长路,次长路即可

vector<Node> G[N];
int f[N];\/\/最长 
int g[N];\/\/次长距离 
void dfs(int u,int fa)
{
	f[u]=0;
	g[u]=0;
	for(auto x:G[u])
	{
		v=x.v;
		w=x.w;
		if(v==fa)
		{
			continue;
		}
		dfs(v,u);
		int dis=f[v]+w;
		if(dis>f[u])
		{
			g[u]=f[u];
			f[u]=dis;
		}
		else if(dis>g[u])
		{
			g[u]=dis;
		}
	}
}

eg 求最大独立集

最大独立集:选出尽量多的点,使得点与点之间不相邻

P1352 没有上司的舞会

#include <bits\/stdc++.h>
#define endl '\n'
using namespace std;
const int N=6e3+5;
short r[N];
int f[N][2];
int n;
vector<int> g[N];
int ind[N];
int s;
void dfs(int u,int fa)
{
    f[u][1]=r[u];
    f[u][0]=0;
    for(auto v:g[u])
    {
        if(v==fa)
        {
            continue;
        }
        dfs(v,u);
        f[u][0]+=max(f[v][0],f[v][1]);
        \/\/u不来,v可来可不来 
        f[u][1]+=f[v][0];
        \/\/u来,v一定不来 
    }
}
signed main()
{
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        cin>>r[i];
    }
    for(int i=1;i<n;i++)
    {
        int x,y;
        cin>>x>>y;
        g[y].push_back(x);
        ind[x]++;
    }
    for(int i=1;i<=n;i++)
    {
        if(ind[i]==0)
        {
            s=i;
            break;
        }
    }
    dfs(s,0);
    cout<<max(f[s][1],f[s][0]);
    return 0;
}

保安站岗

#include <bits\/stdc++.h>
#define int long long
#define endl '\n'
using namespace std;
const int N=1510;
const int INF=0x3f3f3f3f;
int n;
int r[N];
int m;
int f[N][3];
\/\/f[i][0] 表示 该节点不选 孩子看 
\/\/f[i][1] 表示 该节点选 
\/\/f[i][2] 表示 该节点不选 父亲看 
vector<int > g[N];
void dfs(int u, int fa)
{
	f[u][1]=r[u];
	bool flag=0;
	int tmp = INF;
	for(auto v : g[u])
	{
		if(v == fa)
		{
		    continue;
        }
		dfs(v, u);
		f[u][1] += min({f[v][0], f[v][1], f[v][2]});
		f[u][2] += min(f[v][0], f[v][1]);
		if(f[v][1] <= f[v][0])
		{
			flag=1;
		}
		else
		{
			tmp = min(tmp, f[v][1] - f[v][0]);
		}
		f[u][0] += min(f[v][0], f[v][1]);
	}
	if(flag == false)
	{
		f[u][0] += tmp;
	}
   \/\/f[u][0]也可通过辅助数组记录前i个儿子中是否有保安的情况最小花费
}
 
signed main()
{
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        int x;
        cin>>x;
        cin>>r[x];
        cin>>m;
        for(int j=1;j<=m;j++)
        {
            int y;
            cin>>y;
            g[x].push_back(y);
            g[y].push_back(x);
        }
    }
    dfs(1,0);
    cout<<min(f[1][0],f[1][1]);
    return 0;
}

状压dp

eg 旅行商问题

#include<bits\/stdc++.h>
using namespace std;
const int N=1e5+10;
const int inf=0x3f3f3f3f;
int n;
double f[1<<20][20];\/\/f[s][i] 当前在i点 s代表那些点被走过 
int main()
{
	cin>>n;
	for(int i=0;i<n;i++)
	{
		cin>>x[i]>>y[i];
	}
	for(int i=0;i<(1<<n);i++)
	{
		for(int j=0;j<n;j++)
		{
			f[i][j]=1e20;
		}
	}
	f[1][0]=0;
	for(int i=0;i<(1<<n);i++)
	{
		for(int j=0;j<n;j++)
		{
			if(i&(1<<j))
			{
				for(int k=0;k<n;k++)
				{
					if((1&(1<<k))==0)
					{
						f[i|(1<<k)][k]=min(f[i|(1<<k)][k],f[i][j]+dis(j,k));
					}
				}
			}
		}
	}
	double ans=1e20;
	for(int i=0;i<n;i++)
	{
		ans=min(ans,f[(1<<n)-1][i]);
	}
	cout<<ans;
	return 0;
}

特征

n不大

egCorn Fields G

$f_{i,j}$ 表示前i行,第 $i$ 行状态为j的方案数

eg互不侵犯

改状态,加一维表示国王数量

一般dp

Little Elephant and Mouses

中国象棋

优化

eg 从一号点出发,走 $k$ 步到 $n$ 点的方案数

$m[i][j]=1$ 表示 $i,j$ 之间有路

$ f_{i,j}=\sum\limits_{k=1}^{n} f_{i-1,k}\times m_{k,j}$

$f_{i,1,j}=\sum\limits_{k=1}^{n} f_{i-1,1,k}\times m_{k,j}$

$f_i[1][j]=\sum\limits_{k=1}^{n} f_{i-1}[1][k]\times m[k][j]$

$ f_t=f_{t-1}\times m$

$f_t=f_0\times m^{t}$

矩阵快速幂

特征

1.$f_{i}$ 只和 $f_{i-1}$ 有关

转移系数 $ m $ 与 $ i $ 无关

$ t $很大

eg

迷路

拆边,将其拆为边权为$1$

但是本题为有向图,还有自环,最多拆成共有$n+8n^2$ 个点

$800$ 多个点,$n^3$接个$\log_2 t$ 很愉快地炸了

但是有一些点可以重复利用

可以每个点连一条链,每当有原来的点相连,就向外拐出

最多$90$ 个点

这样就转化为上一题了

eg 题意同$T1$ 每个点 $r$ 个入度 $ r $ 个出度

$in_{i,j}$ 表示$i$ 点的第$j$个出度

$out_{i,j}$ 同理

$m_{i,j}$ : 从$ i$到$j$ 有几条边

$m_{i,j}=\sum\limits {k=1}^{r} out{i,k}\times in_{j,k}$

($n<=10^3,r<=20,t<=10^9$)

从$r$入手

观察$m$ ,发现如果将$in$的两维存反,$m=in\times out$

$m^t=(out\times in)^t = out\times (in\times out)^{t-1}\times in$

$m$ 是$n\times n$ 的矩阵

$in$ 是 $r\times n$ 的矩阵(存反后)

$out$ 是 $n\times r$ 的矩阵

所以$in\times out$ 是$r\times r$的矩阵

将$n^3\times log_2{t}$ 复杂度转化为 $r^3\times log_2{t}$

博弈论dp

eg $A,B$两人,有数$n$,两人有$4$种操作,将$n$ 减1,2,4,8

$f[i]$ 表示先手必输还是必赢

$f[-7]$到 $f[0]$ 为true

所有能转移到的状态如果有false,那么自己必胜

如果全为true 那么自己必败

bool f[N];\/\/f[i]表示数为i时必胜还是必败
bool g[N];\/\/g[i]代表f[i]求没求过 
int a[]={1,2,4,8};
bool dfs(int i)
{
	if(i<=0)
	{
		return true;
	}
	if(g[i])
	{
		return f[i];
	}
	g[i]=1;
	f[i]=0;
	for(int j=0;j<4;j++)
	{
		if(!dfs(i-a[j]))
		{
			f[i]=1;
		 } 
	}
	return f[i];
}

总结(第一类)

状态为游戏状态

如果能转移到必败态,那么必胜

如果不能,那么必败

Sereja and Game

eg

有$a$ 数组,不能把$a_i$ 变的$<=0$,谁无法操作,即为输

(每次$-1,-2,-4,-8$)

定义$sg$值

  • 必败态的$sg$值为$0$
  • $sg[i]=mex(sg_[i-1],sg_[i-2],sg_[i-4],sg_[i-8])$( $mex$ 表示最小没有出现过的自然数)

sg定理

$sg[a_1][a_2][a_3].......[a_n]= sg[a_1] \oplus sg[a_2] \oplus sg[a_3]......^sg[a_n]$

特征(第二类)

多个游戏

做法

  • sg值dp

  • 转化为Nim石子游戏问题

经典例题 Nim石子游戏问题

$n$堆石子,二人轮流取石子,至少取一个,谁先取完就输

$sg[0]=0;$

$sg[1]=1;$

$sg[2]=2$

$...$

将$n$ 堆石子数异或起来

番外

“我们今天不讲(插头dp),又不考,(插头dp)如果出了就尽情骂出题人就行了,不用克制,不用收敛”

英文题面读不懂怎么办? fanyi.baidu.com

比今年,啊不,去年noipT4简单,今年题是啥我还不知道,我也不知道我还出不出题,我已经有几年没出题了

"同学们私聊我问问题的时候注意一下是不是开了陌生人不能发消息"

"种国王"

"时间和空间都比较炸裂"

警钟撅烂!!!

本文章由 WyOJ Shojo 从洛谷专栏拉取,原发布时间为 2024-02-05 23:11:39

一定看好转移方程,别手误!!!

2024.2.6+2024.2.7上午

本文章由 WyOJ Shojo 从洛谷专栏拉取,原发布时间为 2024-02-06 08:54:21

图论

坑:同余最短路

存图

vector,链式前向星(邻接链表)

并查集

启发式合并,路径压缩

带权并查集

种类并查集(扩展域并查集)

食物链

把一个点扩为$x,x+n,x+2\times n$三个点表示三个类群,然后判断

Parity Game

转化为前缀和$s_y-s_x = $ 奇/偶

奇偶性相同的是一个集合,不同的是一个集合

然后连边,判冲突

也可用带权并查集

奇偶性相同的连$0$ 边,不同的连$1$ 边

序列并查集

P2391 白雪皑皑

$f_i$ 表示序列中 $i$ 及以后第一个没被染色的点

用并查集维护

连续攻击游戏

将两个属性值连无向边

如果有环,就都能满足

如果是树,除根节点,都能满足

那就选最大的为根

求联通块的 $mex$

(本题也可用二分图匹配)

最小生成树

Kruscal

Prim

P1396 营救

可二分

将$<=n$ 的边加入图,看$s,t$是否联通

但是最小生成树显然更优

从小到大加入边,看是否联通

SVEMIR

一眼最小生成树

但是两两连边的话显然要炸

显然要去除冗边

分别按$x,y,z$排序

每个点向前后的点连边

跨过其他点连的边显然没用(跟他连边的代价都够把其间的点都联通了)

然后排三次序,跑最小生成树

(重边也没关系,因为最小生成树每次取权值最小的)

思维题

Kuglarz

转化为得知$s_i$ (前缀和)的奇偶性

$s_i$ 向 $s_j$ 连边,表示奇偶性相关

$s_0 =0$

目的转化为求$s_0,s_1,s_2......s_n$ 在同一连通块内的最小代价

于是将 $s_{i-1}$ 与 $s_j$ 连边,代价为 $c_{i,j}$

跑最小生成树

严格次小生成树

结论:严格次小生成树一定是拿非树边换树边

枚举所有非树边,尝试替换环上的边

(记录最大值和次大值,如果等于最大值,那么替换次大值,否则替换最大值)(非树边一定大于等于树边)

最短路

bfs

dijistra

spfa

floyd

Switch the Lamp On

原来的边权为$0$,转之后的边权为$1$,跑最短路

(因为每次的坐标x+1,y+1或x+1,y-1...,因此坐标之和奇偶性不变,因此一个格子不会既有左上到右下,又有左下到右上)

小优化:

因为是$0/1$最短路,跑bfs即可,复杂度$O(n+m)$

拓展:

边权为$[0,k]$($k$较小)

开$k$ 个桶,把操作后的扔进相应桶里,从小到大枚举桶,时间复杂度(n+m),空间($nk+m$)

通往奥格瑞玛的道路

二分,判最短路

P1144 最短路计数

P1119 灾后重建

冻结

分层图板子

Sightseeing Cows G

看到除法,二分 $or$ 斜率优化

显然二分

如果二分出来的mid为 $k$,则问题就转化为是否存在一个$\sum F_i/\sum T_i \ge k$

$$ \sum (F_i-k\times T_i)\ge0$$

然后 spfa 判环

骑士游戏

设 $f_i$ 表示彻底消灭 $i$ 的最小代价

$$f_i=min(k_i,s_i+f_j)$$

类比 dijistra

同余最短路

墨墨的等式

同余方程$\to$ 最短路

$f_t=b$ 表示 满足$b mod a_i = t$的最小的 $ b $ ,无论加多少个$a_1$ 都是合法的

$f_0=0$

$f_t \to (f_t+a_i)mod a_1$ 边权$a_i$

跑最短路

$f_t=min(f_{t-a_i}moda_1+a_i)$

差分约束

$a\to b$,边权为 $w$ 表示$b-a\le w$

Layout G

差分约束板子

P5590 赛车游戏

差分约束妙用

$$1\le dis_b-dis_a\le9$$

$$dis_a-dis_b \le -1$$

$$dis_b-dis_a\le9$$

跑差分约束

拓扑排序

Andrew and Taxi

一眼二分

对$> mid$ 的边拓扑

对于$\le mid$ 的边

肯定是从小拓扑序的点指向大拓扑序的点

然后判断方向是否改变,判断是否可行,统计答案

菜肴制作

首先不是字典序最小

怎么办?

保证小的尽量在前面,即保证大的尽量在后面

把较小的放在后面,不如直接把最大的放在后面

我们可以建反图

跑一遍字典序最大的拓扑序,倒序输出,即为答案

基环树

Session in BSU

同前面的某一道题

城市环路

遍历切掉环上的一条边,跑最大独立集

(或随机选环上的一个点,强制选或不选,跑dp)

Island

转化题意,即求基环树(森林)上的直径

不经过环,直接dfs

经过环,求两段的最大值

Tarjan

BLO-Blockade

魔改割点

稳定婚姻

将夫妻和情人关系区分开连,一个男向女,一个女向男,跑tarjan

矿场搭建

二分图

假期的宿舍

板子

攻击装置

对能攻击到的染与自己不同的颜色,跑最大独立集

矩阵游戏

将每行看作点,每列看做点,一个黑格子就让行和列连边,跑最大匹配是否为 $n$

网络流

ek

dinic

重点在建图

番外

“奇技淫巧”

“精妙建图”

2024.2.7下午

本文章由 WyOJ Shojo 从洛谷专栏拉取,原发布时间为 2024-02-07 13:54:38

数论

老师:@realskc

01分数规划(5分钟讲完)

一般看到平均数

Talent Show G

二分答案,把分母乘到右边

然后背包

模(14:23)

在模合数意义下避免除法

逆元

$$a/b \to a\times1/b(mod p)$$

$$a^{-1} \times b^{-1}=(ab)^{-1}$$

费马小定理

对于质数 $p$,$a mod p\ne0$,$a^{p-1}modp=1$

欧拉定理

对于正整数$p\ge2$,$a^{\phi(p)}modp=1$

线性处理逆元

预处理阶乘逆元

离线求逆元

矩阵(见课件,记不动了)

矩阵快速幂跑最短路(点数少,长度很长)

组合(加容斥)

概率

博弈论

Nim游戏

分裂游戏

其他

exgcd

可解二元一次不定方程

中国剩余定理(大多数情况可用exgcd代替)

拉格朗日插值定理

$\phi$ 函数

可用线性筛筛出

番外

“原以为时间很紧张,现在看来比较充裕”

“我是洛谷紫名,大家可以关注我一下”

“洛谷爆了吗,我可以在管理群里骂kkk”

"昨天就骂了"

“我不审题解,我是题目管理员”

“我可以把红题改成黑题”

“能把 $a+b$ 改成黑题吗” “可以,不过可能会被骂”

“$a+b$ 现在是黑题了”

"好了,改回来了"

"杜教筛这种赫赫有名很阴间的东西"

做题技巧

本文章由 WyOJ Shojo 从洛谷专栏拉取,原发布时间为 2024-02-08 14:30:21

看清输入输出

看清题面

不考虑算法,只读题面

对拍(暴力就写纯暴力)

手搓数据(极限数据(极大和极小))

乱搞(好处:跟出题人想法不一样,不一定能被卡掉)

猜结论

不会的题先写能拿的分,先写暴力,然后跳过,写能拿分的

再不行就暴搜

时间不多了,想出新题正解,写新题or调旧题

看状态,如果写完不大用调就写新题,否则调旧题

肉眼观察代码

整理清单:整理脑抽错误(如n,m写反(可以测试时一个开的很大,一个很小,如n=2,q=1000000),数组开小,多测没清空)

memset(a,0,sizeof(a[0] $\times$ (N+10))(防止TLE,不写sizeof(a));

int ,long long

%d,%lld

void 不能return

bool,int,long long等函数一定要return

取模(看看有没有少取)

j++还是i++

变量名

板子(多写)

freopen写挂(NOI银牌也会错)

看清文件名

特判

文件夹名称,文件名(周康阳也错)

.cpp还是.cpp.cpp

调试别忘改文件名

1ll<<i;

运算符优先级(打括号)

2.15

本文章由 WyOJ Shojo 从洛谷专栏拉取,原发布时间为 2024-02-15 11:04:08

P2149 [SDOI2009] Elaxia的路线

4遍dij (一个点一遍)

x1存在dis[1]里 ,x2存在dis[2]里,y1存在dis[3]里,$y2\to dis[4]$

易证公共路径为一条链

如果$dis_{1,x}+dis_{3,y}+w==dis_{1,y}$ 则$x\to y$在 x1到y1的最短路上

同理,可以得到$x\to y$在 x2到y2的最短路上,$y\to x$在 x2到y2的最短路上的情况

这样就得到了一个DAG

同向逆向分开跑最长链,取max

[GXOI/GZOI2019] 旅行者

按二进制位分组,第 $i$ 位为1一组,0一组,对于每一组建超级源点

有向图,两遍

P2371 [国家集训队] 墨墨的等式

同于最短路,$n$ 元跳楼机

P3403 跳楼机

Legacy

线段树优化建图

P4001 [ICPC-Beijing 2006] 狼抓兔子

最短路/网络流(最小割)

P3398 仓鼠找 sugar

找LCA

看二者的LCA是否在对方的路径上

P4281 [AHOI2008] 紧急集合 / 聚会

两两求lca找出深度最大的点,集合点在那里

然后再求路径长度

加强

$m$ 次询问,每次 $k$ 个点

$1\le m,n\le5\times10^5,1\le \sum k \le2\times 10^6$

如果两两分组走,发现其中每条边恰好走两遍

答案即为 $sun/2$

[BJWC2010] 严格次小生成树

不断用非树边替换树边

Indecisive Taxi Fee

共 35 篇博客