2021ccpc女生赛
C (状压DP)
题意:给定一张图,每个节点隶属于一个公司,会有节点属于相同的公司,节点都有一个权值,问从节点1出发,到达第 i i i个点时能得到的最大价值是多少。
每个公司的红包只能领取一次,假设节点3和5都属于公司2,则若在节点3处领取了红包,则在5处就不能领取了。
数据保证无重边和环。其中点数 2 ≤ n ≤ 36 2\leq n \leq 36 2≤n≤36。
考虑状态压缩dp,
f [ i ] [ j ] 表示当前在点 i ,且当前各个公司的状态为 j , ( j 可看成 01 序列,这一位为 1 表示已经取过,为 0 表示未取过 ) ,能得到的最大价值 f[i][j]表示当前在点i,且当前各个公司的状态为j,(j可看成01序列,这一位为1表示已经取过,为0表示未取过),能得到的最大价值 f[i][j]表示当前在点i,且当前各个公司的状态为j,(j可看成01序列,这一位为1表示已经取过,为0表示未取过),能得到的最大价值。
则状态转移为:
if(j&(1<<c[v])) f[v][j]=max(f[v][j],f[u][j-(1<<c[v])]); //取v
else f[v][j]=max(f[v][j],f[u][j]); //不取v
但是注意点的数量为36,因此这样做空间和时间都会超,因此考虑优化。
可以发现,若某个公司只有一家分店,那么我们可以直接取走,因为不会对后续的结果有影响。记为第一类公司。
只有有两家以上分店的公司才会对答案有影响。记为第二类公司。
因为总点数为36,因此第二类公司数量最多为 n / 2 = 18 n/2=18 n/2=18个,那么我们就可以进行转移了:
- 若当前终点为第一类公司,则直接取就行。
- 若当前终点为第二类公司,需要判断当前状态来进行转移。
为什么这样是对的呢,因为dp是无后效性的,本题也是如此,我们算出了当前节点的最优解,即可推出下一个节点的最优解,因此可以保证结果一定正确。
同时本题的图是一个天然的拓扑图,因此不用在拓扑序上dp,循环枚举起点即可。
code:
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define ios std::ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
#define int long long
#define x first
#define y second
typedef pair<int,int> pii;
//head
const int N=110;
vector<int> e[N];
int c[N],w[N];
int dp[50][1<<19];
vector<int> vv;
int ans[N];
void work()
{
int n,m;
cin>>n>>m;
map<int, int> mp;
for(int i=1;i<=n;i++){
cin>>c[i];
mp[c[i]]++;
}
for(int i=1;i<=n;i++) cin>>w[i];
for(int i=1;i<=m;i++){
int a,b;
cin>>a>>b;
e[a].push_back(b);
}
e[0].push_back(1);
//这里也要连条边,不然点1无法转移
for(auto [x,y]:mp){
if(y>1) vv.push_back(x);//把所有第二类公司放入
}
int cnt=vv.size();
for(int i=0;i<=n-1;i++){
for(int k=0;k<(int)e[i].size();k++){//遍历所有邻点
int v=e[i][k];
int id=0;
bool f=0;
for(int j=0;j<cnt;j++){
if(c[v]==vv[j]) {
id=j; f=1;
break;
}
}
if(f){//当前终点为第二类公司
for(int j=0;j<(1<<cnt);j++){
if(j&(1<<id)) dp[v][j]=max(dp[v][j],dp[i][j-(1<<id)]+w[c[v]]);
//若状态j是要取这家的红包,则只能由未取这家的状态转移过来
else dp[v][j]=max(dp[v][j],dp[i][j]);
ans[v]=max(ans[v],dp[v][j]);
}
}
else {//当前终点为第二类公司
for(int j=0;j<(1<<cnt);j++){
dp[v][j]=max(dp[v][j],dp[i][j]+w[c[v]]);
ans[v]=max(ans[v],dp[v][j]);
}
} }
}
for(int i=1;i<=n;i++) cout<<ans[i]<<endl;
}
signed main()
{
ios;
int t;
t=1;
while(t--) work();
return 0;
}
F (hash, 最小循环节)
题意:给定一个由字符组成的矩阵,询问q次,每次询问给出 x 1 , x 2 , y 1 , y 2 x1,x2,y1,y2 x1,x2,y1,y2,问这个矩阵最小是由哪个矩阵扩展来的,输出该矩阵的大小,即 长 ∗ 宽 长*宽 长∗宽的值。
可以看出,行和列的答案是相互独立的,也就是其可以分开计算,那么对于行或列,我们要求的是什么呢?最短的循环的字符串,那和什么很像?对!最小循环节!我们可以用 k m p kmp kmp来求出这个最小循环节,那么问题又来了,对于 x 1 , y 1 , x 2 , y 2 x1,y1,x2,y2 x1,y1,x2,y2这个矩阵,对于列来说,我们求的其实是个二维的东西,列的每个元素其实是当前行的从 y 1 到 y 2 y1到y2 y1到y2的这一段字符串。那么怎么把这一段的内容转化为一个值呢?
对于列来说,我们可以对每一行进行hash,利用hash值来表示这一段的内容是什么,也就把二维转化为了一维问题。
对于行来说操作也类似,对每一列hash即可。
code:
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define ios std::ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
#define int long long
#define x first
#define y second
typedef pair<int,int> pii;
typedef unsigned long long ull;
//head
const int N=2e3+10,P=1331,mod=1e9+7;
ull p[N];
ull r[N][N],c[N][N];
char g[N][N];
ull s[N];
int ne[N];
int n,q;
void init()
{
p[0]=1;
for(int i=1;i<=n;i++){
p[i]=p[i-1]*P;
}
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
r[i][j]=r[i][j-1]*P+g[i][j];
c[j][i]=c[j][i-1]*P+g[i][j];
}
}
}
int get(int len)
{
for(int i=2,j=0;i<=len;i++){
while(j&&s[i]!=s[j+1]) j=ne[j];
if(s[i]==s[j+1]) j++;
ne[i]=j;
}
return len-ne[len];//返回最小循环节的长度
}
void work()
{
cin>>n>>q;
for(int i=1;i<=n;i++){
cin>>g[i]+1;
}
init();
while(q--)
{
int x1,y1,x2,y2;
cin>>x1>>y1>>x2>>y2;
//分别处理行和列,找到最小循环节,相乘就是答案
for(int i=x1;i<=x2;i++){//处理列
s[i-x1+1]=r[i][y2]-r[i][y1-1]*p[y2-y1+1];
}
int len=x2-x1+1;
int L=get(len);
for(int i=y1;i<=y2;i++){//处理行
s[i-y1+1]=c[i][x2]-c[i][x1-1]*p[x2-x1+1];
}
len=y2-y1+1;
int R=get(len);
cout<<L*R<<endl;
}
}
signed main()
{
ios;
int t;
t=1;
while(t--) work();
return 0;
}
B (倍增)
题意:给定一个长度为 n n n,且只由字母表前 m m m个字母组成的字符串 S S S,(不一定全部包含前m个字母),给出q次询问,每次询问给出 l , r l,r l,r,让你构造出一个长度为 k k k的字符串 T T T(也只能由前m个字母组成),使得 T T T不是 S l . . . S r S_l...S_r Sl...Sr的子序列,求出最小的k的值(不用真的构造出 T T T)。
1 ≤ n ≤ 2 ∗ 1 0 5 1\leq n \leq 2*10^5 1≤n≤2∗105 , 1 ≤ q ≤ 2 ∗ 1 0 5 1\leq q \leq 2*10^5 1≤q≤2∗105
注意到n和q都比较大,因此考虑怎么把结果预处理出来。怎么计算出结果呢?考虑怎么构造T使得k最小,对于T的某个字符来说,可以有m种选择,假设当前选择了 S i S_i Si ,那么下一步要选的位置为这m种字符中离 i i i最远的位置,若某种字符不存在,直接跳出选择。
我们可以把这种不断选择看成跳到某个位置,另 f [ i ] [ j ] 表示当前在位置 i ,跳(选择) 2 j 次可以到达的最远位置 f[i][j]表示当前在位置i,跳(选择)2^j次可以到达的最远位置 f[i][j]表示当前在位置i,跳(选择)2j次可以到达的最远位置。
我们可以通过倍增来预处理出 f f f数组,同时注意 f f f数组的初始化。
code:
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define ios std::ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
#define int long long
#define x first
#define y second
typedef pair<int,int> pii;
//head
const int N=2e5+10;
int f[N][20];
int pos[50];
int m,n;
string s;
void init()
{
vector<int> pos(26,n+1);
//pos表示每个字符出现的位置,要实时更新
//f[i][j]表示当前在第i位置,下一步有m种选择可以跳,跳2^j次后到达的最大位置
for(int i=0;i<20;i++) f[n+1][i]=n+1;
for(int i=n;i>=0;i--){
for(int j=0;j<m;j++){
f[i][0]=max(f[i][0],pos[j]);//取最远的那个字符
}
if(i) pos[s[i]-'a']=i;//更新pos,因为要算下一步跳的最远位置
}
for(int j=1;j<20;j++){
for(int i=0;i<=n;i++){
f[i][j]=f[f[i][j-1]][j-1];
}
}
}
void work()
{
cin>>m>>n;
cin>>s; s=" "+s;
init();
int q;
cin>>q;
while(q--)
{
int l,r;
cin>>l>>r;
int ans=0,p=l-1;
for(int j=19;j>=0;j--){
if(f[p][j]<=r){
p=f[p][j];
ans+=(1<<j);
}
}
cout<<ans+1<<endl;
}
}
signed main()
{
ios;
int t;
t=1;
while(t--) work();
return 0;
}