前言:
又垫底了呜呜呜
T1. 整齐的队列
CF1458A Row GCD
题目描述
给定两个正整数序列 \(a_1, \ldots, a_n\) 和 \(b_1, \ldots, b_m\)。对于每一个 \(j = 1, \ldots, m\),求 \(\gcd(a_1 + b_j, \ldots, a_n + b_j)\) 的值。
输入格式
第一行包含两个整数 \(n\) 和 \(m\)(\(1 \leq n, m \leq 2 \times 10^5\))。
第二行包含 \(n\) 个整数 \(a_1, \ldots, a_n\)(\(1 \leq a_i \leq 10^{18}\))。
第三行包含 \(m\) 个整数 \(b_1, \ldots, b_m\)(\(1 \leq b_j \leq 10^{18}\))。
输出格式
输出 \(m\) 个整数,第 \(j\) 个数表示 \(\gcd(a_1 + b_j, \ldots, a_n + b_j)\) 的值。
输入 #1
cpp
4 4
1 25 121 169
1 2 7 23
输出 #1
cpp
2 3 8 24
其实这题非常简单,但是我忽略了一个gcd的性质\(gcd(x,y)=gcd(x,x-y)\)
于是我就挂了
我们先求出a1\~n的\(gcd=gcd{(a1,a2),(a1,a3)...(a1,an)} =gcd{(a1,a2-a1),(a1,a3-a1)...(a1,an-a1)} =gcd{(a1,a2-a1,a3-a1...an-a1)}\)
然后将a1\~n整体+b后\(gcd=gcd{(a1+b,a2+b),(a1+b,a3+b)...(a1+b,an+b)} =gcd{(a1+b,a2-a1),(a1+b,a3-a1)...gcd(a1+b,an-a1)} =gcd{(a1+b,a2-a1,a3-a1...an-a1)}\)
所以每次整体+b后改变的只有a1
我们就把后面的这些差的gcd提前预处理出来,每次询问就取a1+b和后面的gcd
时间复杂度\(O(m)\)
cpp
#include<bits/stdc++.h>
using namespace std;
const int N=2e6+7;
#define int long long//在这里沉重悼念翔哥
int n,m,g,a[N],b;
signed main(){
cin>>n>>m;
for(int i=1;i<=n;i++)cin>>a[i],g=__gcd(g,abs(a[i]-a[1]));
for(int i=1;i<=m;i++)cin>>b,cout<<__gcd(a[1]+b,g)<<" ";
return 0;
}
T2. 词典压缩:KMP
CF1200E Compress Words
题目描述
Amugae 有一个由 \(n\) 个单词组成的句子。他想把这个句子压缩成一个单词。Amugae 不喜欢重复,因此当他将两个单词合并成一个单词时,会移除第二个单词中与第一个单词后缀相同的最长前缀。例如,他将 "sample" 和 "please" 合并成 "samplease"。
Amugae 会从左到右依次合并他的句子(即,先合并前两个单词,然后将结果与第三个单词合并,依此类推)。请编写程序输出合并过程结束后得到的压缩单词。
输入格式
第一行包含一个整数 \(n\)(\(1 \le n \le 10^5\)),表示 Amugae 句子中的单词数。
第二行包含 \(n\) 个由单个空格分隔的单词。每个单词非空,仅由大小写英文字母和数字('A', 'B', ..., 'Z', 'a', 'b', ..., 'z', '0', '1', ..., '9')组成。所有单词的总长度不超过 \(10^6\)。
输出格式
输出一行,表示合并过程结束后得到的压缩单词。
输入 #1
5
I want to order pizza
输出 #1
Iwantorderpizza
输入 #2
5
sample please ease in out
输出 #2
sampleaseinout
暴力\(O(n^2)\)不用多说
考虑正解
这题看到匹配字符串立刻想到KMP,但是匹配一个单词的后缀和另一个单词的前缀似乎不太好搞
于是我们就构造一个字符串把原串放在后面,现在要加入的字符串放前面
对这个字符串跑一个KMP求出nxt数组,就得到了最长的相同前缀和后缀
但是为了保证匹配数量不要太多比整个要匹配的单词都长,就在中间加一个#
还有我们的原串很长,如果全部加入复杂度太劣,并且我们只匹配它的后缀与新串前缀
如果原串比整个新串都长,原串前面的部分也没必要加入,只用取后面新串的长度
时间复杂度\(O(2*答案长度)\)
cpp
#include<bits/stdc++.h>
using namespace std;
const int N=2e6+7;
#define int long long
string s,ans;
int KMP(string t){
int n=t.size()-1;
vector<int>nxt(n+3);
for(int i=2,j=0;i<=n;i++){
while(j&&t[i]!=t[j+1])j=nxt[j];
if(t[i]==t[j+1])j++,nxt[i]=j;
}
return nxt[n];
}
signed main(){
int t;cin>>t>>ans;t--;
while(t--){
cin>>s;
string t=' '+s+'#'+ans.substr(max(0LL,(int)(ans.size()-s.size())),ans.size());
ans+=s.substr(KMP(t),s.size());
}
cout<<ans;
return 0;
}
T3. 密码碎片:字符串Hash+欧拉路径
CF508D Tanya and Password
题目描述
在爸爸上班的时候,小女孩 Tanya 决定玩弄一下爸爸的秘密数据库密码。爸爸的密码是一个包含 \(n+2\) 个字符的字符串。她把密码所有可能的 \(n\) 个连续三字母子串都写在小纸条上,每个子串出现几次就写几次,然后把密码扔了。每张纸条上都只写了一个出现在密码里的三字母子串。
之后 Tanya 忽然意识到如果爸爸知道她玩的这个游戏会生气,于是决定尝试复原密码,或者至少还原出一个能对应这些子串集合的字符串。你需要帮她完成这项艰巨的任务。已知爸爸的密码只包含大小写拉丁字母和数字。大小写字母视为不同字符。
输入格式
第一行包含一个整数 \(n\)(\(1 \le n \le 2 \cdot 10^{5}\)),表示 Tanya 得到的三字母子串的数量。
接下来的 \(n\) 行每行包含三个字符,表示爸爸密码的一个子串。输入中的每个字符为小写字母、大写字母或数字。
输出格式
如果 Tanya 在游戏过程中出现了错误,导致不存在能由这些子串组成的字符串,则输出 "NO"。
如果存在可能的原始密码字符串,请输出 "YES",并在下一行输出任意一个满足条件的密码字符串。
输入 #1
5
aca
aba
aba
cab
bac
输出 #1
YES
abacaba
输入 #2
4
abc
bCb
cb1
b13
输出 #2
NO
输入 #3
7
aaa
aaa
aaa
aaa
aaa
aaa
aaa
输出 #3
YES
aaaaaaaaa
这个构造题看起来挺想AC自动机,但是手玩样例后发现并不好做
首先爆搜30分到手
然后我们来看正解:
我们发现这n个长度为3字符串两两拼接长度为n+2
那么每加一个字符串总长+1,
所以前后拼接的两个字符串前面的后两个字符和后面的前两个字符长度相等
考虑将每个字符串前两个字符哈希值向后两个字符哈希值建边
这样相同的前后缀就都连上了
我们只需要判断能不能一条路径把所有边一次遍历到
也就是找一条欧拉路径,然后把欧拉路径上的点输出就行了
复杂度\(O(n)\)
cpp
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=4e5+7;
string ans;
int n,in[N],out[N],S,x,vis[N];
vector<int>g[N];
void dfs(int u){
for(int &i=vis[u];i<(int)g[u].size();)dfs(g[u][i++]);
ans+=(char)(u%131);
}
signed main(){
cin>>n;
for(int i=1;i<=n;i++){
char a,b,c;cin>>a>>b>>c;
int u=a*131+b,v=b*131+c;
g[u].push_back(v),out[u]++,in[v]++,x=u;
}
for(int i=0;i<N;i++){
if(!out[i]&&!in[i])continue;
if(abs(out[i]-in[i])>1)puts("NO"),exit(0);
if(out[i]-in[i]==1){
if(!S)S=i;else puts("NO"),exit(0);
}
}
S=S?S:x;
dfs(S);
ans+=(char)(S/131);
if((int)ans.size()!=n+2)puts("NO"),exit(0);
reverse(ans.begin(),ans.end());
cout<<"YES\n"<<ans;
return 0;
}
T4 费里斯转轮: 决策单调性优化dp
CF321E Ciel and Gondolas
题目描述
Fox Ciel 正在游乐园,现在她正在摩天轮前面排队。队伍中共有 \(n\) 个人(准确来说是狐狸):我们用第一个人指队伍最前面的人,用第 \(n\) 个人指队伍最后一个人。
将有 \(k\) 个座舱,分配座舱的方法如下:
- 当第一个座舱到来时,队伍前面的 \(q_1\) 个人进入座舱。
- 当第二个座舱到来时,剩下队伍的前面 \(q_2\) 个人进入座舱。......
- 剩下的 \(q_k\) 个人进入最后(第 \(k\) 个)座舱。
注意,\(q_1, q_2, ..., q_k\) 必须都是正整数。你可以从题意得出 \(q_1 + q_2 + \cdots + q_k = n\),且 \(q_i > 0\)。
你知道,每个人都不愿和陌生人同舱,所以你的任务是找到一种最优分配方式(即找到一个最优的 \(q\) 序列),使得大家都尽可能满意。对于每一对乘客 \(i\) 和 \(j\),存在一个值 \(u_{ij}\) 表示他们之间的陌生程度。你可以假设对于所有的 \(i, j\) 都有 \(u_{ij} = u_{ji}\),且对于所有 \(i\) 有 \(u_{ii} = 0\),其中 \(1 \leq i, j \leq n\)。一个座舱的陌生值等于该座舱中所有两两乘客之间的陌生程度之和。
总陌生值等于所有座舱的陌生值之和。请你帮助 Fox Ciel 找到某种最优分配方式,使总陌生值的最小可能值是多少。
输入格式
第一行为两个整数 \(n\) 和 \(k\)(\(1 \leq n \leq 4000\),\(1 \leq k \leq \min(n, 800)\)),分别表示排队人数和座舱数量。接下来的 \(n\) 行,每行包含 \(n\) 个整数,表示矩阵 \(u\),其中 \(0 \leq u_{ij} \leq 9\),且 \(u_{ij} = u_{ji}\),\(u_{ii} = 0\)。
请使用高效的输入方式(例如,对于 Java 请使用 BufferedReader 而不是 Scanner)。
输出格式
输出一个整数,表示最小可能的总陌生值。
输入 #1
5 2
0 0 1 1 1
0 0 1 1 1
1 1 0 0 0
1 1 0 0 0
1 1 0 0 0
输出 #1
0
输入 #2
8 3
0 1 1 1 1 1 1 1
1 0 1 1 1 1 1 1
1 1 0 1 1 1 1 1
1 1 1 0 1 1 1 1
1 1 1 1 0 1 1 1
1 1 1 1 1 0 1 1
1 1 1 1 1 1 0 1
1 1 1 1 1 1 1 0
输出 #2
7
输入 #3
3 2
0 2 0
2 0 3
0 3 0
输出 #3
2
说明/提示
在第一个样例中,可以这样分配:{1, 2} 进同一个座舱,{3, 4, 5} 进另一个座舱。
在第二个样例中,最优方案是:{1, 2, 3} | {4, 5, 6} | {7, 8}。
这题一看就是dp啊
状态定义:dpikk表示选完前i个数,分成kk组的最小陌生值
显然:\(dpikk=\min\limits_{1<=j<=i}(dpikk,dpj-1kk-1+calc(j,i))\)
复杂度\(O(n^2*k)\)
卡一卡可以获得65的高分
考虑优化:
我们用刚写的暴力输出一下每次取j的位置
cpp
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=4000+7;
int dp[N][N],n,k,sum[N][N],x[N][N];
int calc(int u,int v){
return sum[v][v]-sum[v][u-1]-sum[u-1][v]+sum[u-1][u-1];
}
signed main(){
ios::sync_with_stdio(0);cin.tie(0);
cin>>n>>k;
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
int v;cin>>v;sum[i][j]=sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1]+v;
}
}
memset(dp,0x3f,sizeof(dp));
dp[0][0]=0;
for(int i=1;i<=n;i++){
for(int kk=1;kk<=k;kk++){
for(int j=1;j<=i;j++){
if(dp[j-1][kk-1]+calc(j,i)<dp[i][kk]){
dp[i][kk]=min(dp[i][kk],dp[j-1][kk-1]+calc(j,i));
x[i][kk]=j;
}
}
}
}
for(int i=1;i<=n;i++){
for(int kk=1;kk<=k;kk++)cout.width(5),cout<<x[i][kk]<<" ";
cout<<"\n";
}
cout<<dp[n][k]/2;
return 0;
}
找几个大样例输出一下就回发现决策j的位置从左上到右下非严格单调递增
其实也可以证出来,但是我太菜了不会
所以我们每次找这个点的左面=边xikk-1和下边xi+1kk为xikk的左右边界
每次枚举边界内的j进行决策,复杂度会大幅下降
具体降到多少我不知道也不会证
cpp
#include<bits/stdc++.h>
using namespace std;
const int N=4000+7;
int dp[N][810],n,k,sum[N][N],x[N][N];
int calc(int u,int v){
return sum[v][v]-sum[v][u-1]-sum[u-1][v]+sum[u-1][u-1];
}
signed main(){
cin>>n>>k;
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
int v;cin>>v;
sum[i][j]=sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1]+v;
}
}
memset(dp,0x3f,sizeof(dp));
dp[0][0]=0;
for(int kk=1;kk<=k;kk++){
for(int i=n;i>=1;i--){
int l=x[i][kk-1],r=(!x[i+1][kk])?i:x[i+1][kk];
for(int j=l;j<=r;j++){
if(dp[j-1][kk-1]+calc(j,i)<dp[i][kk]){
dp[i][kk]=dp[j-1][kk-1]+calc(j,i);
x[i][kk]=j;
}
}
}
}
cout<<dp[n][k]/2;
return 0;
}