F - String Cards
给N个字符串, 恰好选其中K个, 拼接出的最小字典序的字符串是什么
范围
N 50
|Si| [1,50], 每个长度
只含小写英文字母
2s
1024 mb
我的思路
例如
b和ba
b <= ba
但是
bab <= bba
非前缀的 s1 < s2, 一定s1
s1 是 s2的前缀的 s1 < s2
如果只剩下一个,则s1
否则,形如
1 2 3 4 5 6 7 8 9
| [A] [A][B] [A][B][C] [A][B][C][D]
[A][A] ? [A][B] [A] ? [A][B][C] [A] ? [A][B][C][D] [A] ?
|
如果本身A和B有直接大小, 那么就能知道第一个还是后面3个,A和C再比,A和D再比
但如果A和B依然是前后缀关系, 这会考虑长度,也会考虑要选的个数
少考虑一点, 对于两个
两种排列
长度一致, 考虑是否交换
但感觉两个到三个之间局部性不知道是否成立:
(A,AB)组合A 放在前, (AB,ABC) 组合 AB 放在前, 能否推出 (A,ABC) 中 A要放在前
其实我在想,是不是n^4 可以暴力dp?, 好像会是n^6?
dp[i][j][length]
, 前i选j,长度=length的最小前缀
题解
有意思, 还给了一些错误解法的反例
反例1, sort取前k个连起来
就是我已经想到的 b, ba
反例2, 还是sort, 保证相邻拼接 < 相邻逆序拼接, 的前k 个
这也是想到了的, 当个数为1时ba,b 还是会选b
反例3, sort, 保证相邻拼接 < 相邻逆序拼接
定义dp[i][j]=
前i个选j个能得到的最小字典序
dp[i][j] = min(dp[i-1][j],dp[i-1][j-1] + Si)
的转移方程, 输出dp[N][K]
对于 baa,ba,b, 选出两个, 期望是baab, 而输出是baaba
应该就是没有最小的性质
前两个选1个最小是ba, 拼接b得到bab
而前两个选2个最小是baaba, 这两个都不是最小答案
最小的构成是 baab = 前两个中大的一个 和最后一个拼接
反例4
同样的排序, 同样的dp
定义
但是改变转移方程
dp[i][j] = min(dp[i-1][j],dp[k < i][j-1] + Si)
的转移方程, 输出dp[N][K]
然后
bbaba
bba
b
b
选3个也是反例
其实这列我们可以看到, dp的定义 只能说是希望它是这样,一旦你的转移方程之类出现问题, 它的结果将不满足定义
正确方案
尝试排序 让排序后 任意(i < j) 满足, Si+Sj < Sj + Si
这里有点和我想的有没有偏序关系的问题了一样了, 怎么证明存在啊?
证明
把字符串变成数字 a,b
AB < BA
等价a * 26^len(B) + b < b * 26^len(A) + a
a/(26^len(A)-1) < b/(26^len(B)-1)
好有道理, 事后看起也不难, 我咋自己想不到简单转换一下就证明了
那这样整个问题就简单了
也就是 如果选定了字符串, 那么 它们的顺序就是一定的, 有偏序关系,就可以按照上面的排序来排
那么这样再dp[i][j]
= S[i..N] 中选j个能构成的最小字典需
和前面不一样的是, 变成后缀中选j个
转移方程dp[i][j] = min(dp[i+1][j], S[i]+ dp[i+1][j-1])
显然如果S[i]
本来就非前缀的小于S[i+1]
, 那么min 一定取右侧, 且子问题已经被计算了
而如果要S[i]
则S[i]
一定是开头的
答案是dp[1][K]
这里其实dp的问题在于 为啥正着做不对,而倒着的是对的?
其实问题就在于 选第i个时
正着选第 i个时, 它也在末尾, 但是并不一定意味着 前缀是dp[i-1][j-1]
, 因为dp[i-1][j-1]
可能是通过长度胜出的最小,而不是字符胜出的最小,
可能有 D0 < D1, 但是D0是D1前缀,
从而 D1+Si < D0+Si的情况, 没有了局部性
而倒过来, Si放在最前
如果同样有 有 D0 < D1, 但是D0是D1前缀
但 Si+D0 < Si + D1始终成立的, 保证了局部性
代码
https://atcoder.jp/contests/abc225/submissions/33909604
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| #include <bits/stdc++.h> using namespace std;
typedef long long ll; #define rep(i,a,n) for (ll i=a;i<(ll)n;i++)
ll read(){ll r;scanf("%lld",&r);return r;}
char s[60][60]; int sz[60]; int idx[60]; char dp[60][60][2500];
int main(){ int n = read(); int k = read(); rep(i,0,n) { scanf("%s",s[i]); sz[i] = strlen(s[i]); } iota(idx,idx+n,0); sort(idx,idx+n,[=](int i,int j){ rep(p,0,sz[i]+sz[j]){ char ch0 = p < sz[i] ? s[i][p] : s[j][p-sz[i]]; char ch1 = p < sz[j] ? s[j][p] : s[i][p-sz[j]]; if(ch0 != ch1) return ch0 < ch1; } return false; }); rep(i0,0,n){ char *t = s[idx[n-1-i0]]; rep(j,0,i0+1){ strcpy(dp[i0][j], t); if(i0 > 0) { if(j > 0) strcpy(dp[i0][j] + strlen(t), dp[i0-1][j-1]); if(i0 > j && strcmp(dp[i0][j], dp[i0-1][j]) > 0) strcpy(dp[i0][j], dp[i0-1][j]); } } } printf("%s\n",dp[n-1][k-1]); return 0; }
|
G - X
https://atcoder.jp/contests/abc225/tasks/abc225_g
矩阵H x W, 每个上面Aij
选任意个格子,每个被选的格子画X, 连通左上右下,左下右上角
然后 分数 = 画了X的格子的数字和 - C 乘 画X的最小需要的线段数, 没有选的格子不能画,任何东西
例如这样选的话, 左下角到右上只需要一个长线段,一共是3个线段
求 最大分数 = sum(选的格子) - C 倍最小画线段数
范围
H,W 100
C 1e9
Aij [1,1e9]
2s
1024 mb
我的思路
先试试形状
1个格子, -2C
4个格子, -6C
9个格子, -10C
16个格子,-14C
4个格子,-4C
9个格子, -6C
收获不大至少说明一点,即使格子本身小于C,但是可以通过相邻的线段可以贯通,而让均摊下来每个减去的不到C
第二个思路就是
如果一排一排的铺
dp[state i] => dp[state i+1]
, 根据上面左上角和右上角的两个是否被选决定当前选择的贡献是什么
这样 做个类似插头dp的样子, 每次转移O(1) 但是,因为要存边界状态
所以状态 高达 N^2 * 2^N
, 分别是切割线的位置, 和 切割线的状态
这里最主要的就是 会受到它顶部两个的选择状态的影响它的贡献
然后考虑说在位置上相邻,在实际填写中互不影响, 其实可以把 横纵坐标和为奇数/ 和偶数分开考虑
然后可以转个45度, 补成个长方形, 原来空白的地方放0,问题变成选一些数的 和 - C 乘 (纵向连续块数+横向连续块数), 这样最大答案不会变
但这依然有和左侧和上面的关联决定一个块的贡献
整体的考虑
如果出现这样的选择, 那么? 必然选择,因为选择它不会增加带来的收益$\ge 0$
因此在新的里面选的一定是长方形
但可能不止一个长方形
1 2 3 4
| 00000000 0YY11110 0YY11YY0 01111YY0
|
例如这样 = S - 8C
1 2 3 4
| 00000000 0YYYYYY0 0YYYYYY0 0YYYYYY0
|
而这样是 = S+10-9C
如果是C很大,则上面的更优
每个长方形代价 = sum(长方形) - C 乘 (长+高)
并且显然长方形的 轮廓的 横向和 >= C, 纵向和 >= C,( 内部不一定
这样是 = S - 10C
这样是 = S+4 - 7C
C很大,后面会更好,但中间行和可能 < C
找不到什么合并不合并规则
如何在100x100量级中 选多个长方形, 每个贡献 = Sum - C(长 + 宽)
让贡献最大
虽然对于单个来说, 可以矩阵前缀,很快算出单个的贡献
不太会了
题解
每个选, +Aij
每个选 左上角不选, -C
每个选 右上角不选, -C
又有加又有减, 不好
变化
如果你不选, Aij代价, 选无代价
每个选 左上角不选, C代价
每个选 右上角不选, C代价
这样就全是 增加了
现在答案 = sum A - 代价
要总代价最小
代价最小 => 最小割问题
S源(与S同块的视作选择),T汇(与T同块的视作不选)
S -> (i,j), 容量Aij, (割这条边相当于不选它 的代价)
对于左上角没有块的(i,j): (i,j) -> T , 容量C (割这条边相当于选它的代价)
对于右上角没有块的(i,j): (i,j) -> T , 容量C (同理,割这条边相当于选它的代价), 相当于可能重边
对于左上角有块的, (i,j) -> (i-1,j-1) 容量C, (割这条边,相当于它和S同块(选择),左上角和T同块(不选择),所产生的代价)
对于右上角有块的, (i,j) -> (i-1,j+1) 容量C, (同理, 割这条边,相当于它和S同块(选择),右上角和T同块(不选择))
这个复杂度, 不理解???
看数据, 似乎没有调小, 还是和题目说的量级
但实际跑起来, 我本地i7-7700HQ, 跑数据最长的0.2~0.3s
但是提交的远端是几十ms
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117
| TestCase G.in.014 => G.out.014 Time spend: 0.002467s
TestCase G.in.006 => G.out.006 Time spend: 0.002532s
TestCase G.in.033 => G.out.033 Time spend: 0.264098s
TestCase G.in.031 => G.out.031 Time spend: 0.235067s
TestCase G.in.021 => G.out.021 Time spend: 0.012753s
TestCase G.in.012 => G.out.012 Time spend: 0.00644s
TestCase G.in.015 => G.out.015 Time spend: 0.003208s
TestCase G.in.039 => G.out.039 Time spend: 0.04113s
TestCase G.in.002 => G.out.002 Time spend: 0.002386s
TestCase G.in.034 => G.out.034 Time spend: 0.193138s
TestCase G.in.028 => G.out.028 Time spend: 0.227858s
TestCase G.in.005 => G.out.005 Time spend: 0.002228s
TestCase G.in.000 => G.out.000 Time spend: 0.014386s
TestCase G.in.045 => G.out.045 Time spend: 0.163s
TestCase G.in.001 => G.out.001 Time spend: 0.002495s
TestCase G.in.049 => G.out.049 Time spend: 0.166109s
TestCase G.in.040 => G.out.040 Time spend: 0.143258s
TestCase G.in.016 => G.out.016 Time spend: 0.003306s
TestCase G.in.018 => G.out.018 Time spend: 0.003002s
TestCase G.in.023 => G.out.023 Time spend: 0.012607s
TestCase G.in.025 => G.out.025 Time spend: 0.255023s
TestCase G.in.042 => G.out.042 Time spend: 0.150962s
TestCase G.in.003 => G.out.003 Time spend: 0.001938s
TestCase G.in.017 => G.out.017 Time spend: 0.011363s
TestCase G.in.0 => G.out.0 Time spend: 0.001994s
TestCase G.in.004 => G.out.004 Time spend: 0.018588s
TestCase G.in.051 => G.out.051 Time spend: 0.152942s
TestCase G.in.037 => G.out.037 Time spend: 0.017386s
TestCase G.in.047 => G.out.047 Time spend: 0.195896s
TestCase G.in.029 => G.out.029 Time spend: 0.244589s
TestCase G.in.009 => G.out.009 Time spend: 0.017067s
TestCase G.in.041 => G.out.041 Time spend: 0.185784s
TestCase G.in.053 => G.out.053 Time spend: 0.134323s
TestCase G.in.046 => G.out.046 Time spend: 0.199048s
TestCase G.in.019 => G.out.019 Time spend: 0.004681s
TestCase G.in.030 => G.out.030 Time spend: 0.24435s
TestCase G.in.1 => G.out.1 Time spend: 0.002198s
TestCase G.in.035 => G.out.035 Time spend: 0.277394s
TestCase G.in.024 => G.out.024 Time spend: 0.269645s
TestCase G.in.032 => G.out.032 Time spend: 0.276123s
TestCase G.in.026 => G.out.026 Time spend: 0.235927s
TestCase G.in.010 => G.out.010 Time spend: 0.015773s
TestCase G.in.027 => G.out.027 Time spend: 0.239164s
TestCase G.in.013 => G.out.013 Time spend: 0.00688s
TestCase G.in.022 => G.out.022 Time spend: 0.119993s
TestCase G.in.038 => G.out.038 Time spend: 0.038151s
TestCase G.in.007 => G.out.007 Time spend: 0.002414s
TestCase G.in.020 => G.out.020 Time spend: 0.015595s
TestCase G.in.052 => G.out.052 Time spend: 0.134451s
TestCase G.in.044 => G.out.044 Time spend: 0.151272s
TestCase G.in.011 => G.out.011 Time spend: 0.015062s
TestCase G.in.043 => G.out.043 Time spend: 0.163996s
TestCase G.in.036 => G.out.036 Time spend: 0.019596s
TestCase G.in.050 => G.out.050 Time spend: 0.187503s
TestCase G.in.054 => G.out.054 Time spend: 0.123545s
TestCase G.in.048 => G.out.048 Time spend: 0.162755s
TestCase G.in.055 => G.out.055 Time spend: 0.124003s
TestCase G.in.008 => G.out.008 Time spend: 0.016901s
TestCase G.in.2 => G.out.2 Time spend: 0.00257s
|
这样看来, 如果变成我的那样转化, 其实也可以类似的, 左侧依赖,上侧依赖,不选负贡献 来做最小割
甚至可以变成新的题?
代码
https://atcoder.jp/contests/abc225/submissions/33910465
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| #include <bits/stdc++.h> #include<atcoder/maxflow> using namespace std;
typedef long long ll; #define rep(i,a,n) for (ll i=a;i<(ll)n;i++)
ll read(){ll r;scanf("%lld",&r);return r;}
int A[110][110];
int main(){ int H = read(); int W = read(); int C = read(); auto enc = [=](int i,int j){return i*W+j;}; atcoder::mf_graph<ll> graph(H*W+2); rep(i,0,H) rep(j,0,W) A[i][j] = read(); int S = H*W; int T = S+1; rep(i,0,H) rep(j,0,W) graph.add_edge(S,enc(i,j),A[i][j]); rep(i,0,H) rep(j,0,W) graph.add_edge(enc(i,j),(i && j )?enc(i-1,j-1):T,C); rep(i,0,H) rep(j,0,W) graph.add_edge(enc(i,j),(i && j+1<W)?enc(i-1,j+1):T,C); ll ans = -graph.flow(S,T); rep(i,0,H) rep(j,0,W) ans += A[i][j]; printf("%lld\n",ans); }
|
H - Social Distance 2
https://atcoder.jp/contests/abc225/tasks/abc225_h
N个椅子排成一排, 每个最多坐一个人
M个人坐(每个人两两不同)
分数 = 相邻坐标差的乘积,
给定编号1~K的人的坐的位置
问剩下的M-K 的所有坐的方案的分数的和
mod 998244353
范围
2e5
2s
1024 mb
我的思路
类型应该至少是组合数+ 贡献统计吧
要么就是想法容斥
那么考虑是如何归类
一个不满足范围的DP
dp[i][j] =
小于等于给定的第i大的位置内, 坐了j个人的乘积的和,
`dp[i][j] = sum_t ( dp[i-1][t] * (A[i]-A[i-1] 空隙中(最后位置要放一个), 放入 j-t 个 的乘积和) )
状态就是 N^2的
如果后面的乘积和能直接算的话, 也需要N^3的时间复杂度, 即使走fft一类, 也是 N^2 log^2 N
题解
先学一下maspy的三篇博文, 见底部链接
对于未确定的$M-K$, 我们不要关心它们的区别, 直接所有方案乘上 $(M-K)!$ 即可
定义f1[n][k] =
, 跨度n
的椅子,两端都坐了人, 一共k+2
个人,的内部的乘积权和
定义f2[n][k] =
, 跨度n
的椅子,左端坐了人, 一共k+1
个的 内部的乘积权和
定义f3[n][k] =
, 跨度n
的椅子, 一共k
个的 内部的乘积权和
其实区别只在端上
那么有
$\displaystyle f_1(n,k) = \lbrack x^{n-1} \rbrack (x + 2x^2 + 3x^3 + \cdots)^{k+1} $
相当于 从$1$走到$n$,走$k+1$次,每步任意正长度(每步对最终的贡献倍数也是长度的贡献)
$ = \lbrack x^{n-k-2} \rbrack (1 + 2x + 3x^2 + \cdots)^{k+1} $
$ = \lbrack x^{n-k-2} \rbrack \frac{1}{((1-x)^2)^{k+1}}$
$ = \lbrack x^{n-k-2} \rbrack \frac{1}{(1-x)^{2k+2}}$
$ = \lbrack x^{n-k-2} \rbrack \frac{1}{(1-x)^{2k+2}}$
$ = \binom{(n-k-2) + (2k+2-1)}{2k+2-1}$
$ = \binom{n+k-1}{2k+1}$
类似的有
$f_2(n,k) = \binom{n+k-1}{2k}$
$f_3(n,k) = \binom{n+k-1}{2k-1}$
那么对于原问题, 如果$K=0$, 就是所有人都未确定, 使用$f_3$
对于$K\neq 0$
那么其实就是 两端用$f_2$, 中间是$K-1$个$f_1$
然后不要挨个乘,用归并的思想,尽量小乘小,大乘大, 不要太多 大乘小, 就可以了
代码
看起来atcoder的执行是没有-fsanitize
参数的, 很快
https://atcoder.jp/contests/abc225/submissions/33929839
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| #include <bits/stdc++.h> #include <atcoder/modint> #include <atcoder/convolution> using mint=atcoder::modint998244353; using namespace std;
typedef long long ll; #define rep(i,a,n) for (ll i=a;i<(ll)n;i++) #define per(i,a,n) for (ll i=n;i-->(ll)a;) #define pb push_back ll read(){ll r;scanf("%lld",&r);return r;} const int N = 200000; ll a[N+10]; mint fac[2*N+10] = {1}; mint ifac[2*N+10]; vector<mint> polys[N+10]; mint C(int n,int m){return fac[n]*ifac[m]*ifac[n-m];} ll n; ll m; ll k;
vector<mint> solve(ll l,ll r) { if(l == r) return polys[l]; auto res = atcoder::convolution(solve(l,(l+r)/2),solve((l+r)/2+1,r)); if((int)res.size() > m-k+1) res.resize(m-k+1); return res; }
int main() { rep(i,1,2*N+1) fac[i] = fac[i-1]*i; ifac[2*N] = fac[2*N].inv(); per(i,0,2*N) ifac[i] = ifac[i+1]*(i+1);
n = read(); m = read(); k = read(); rep(i,0,k) a[i] = read();
if(k==0) { printf("%d\n",(C(n+m-1,2*m-1) * fac[m]).val()); return 0; } rep(i,0,k-1) { ll len = a[i+1]-a[i]+1; rep(seg,0,len-1) polys[i].pb(C(len+seg-1,2*seg+1)); } rep(i,0,2) { auto len = (i==0) ? a[0] : (n-a[k-1]+1); rep(seg,0,len) polys[k-1+i].pb(C(len+seg-1,2*seg)); } printf("%d\n",(solve(0,k)[m-k] * fac[m-k]).val()); return 0; }
|
总结
F
这个字符串的偏序证明 事后看起来还挺简单的,虽然我有这个方向想法, 但没去简单尝试一下
然后就是倒着dp的局部性保证了, 感觉对于这种通过长度悬空,的非真实的小于 似乎都可以类似思考一下
G
题意转化 成 只有增的世界, 题意转化还是不会
最小 转化成 最小割
不过感觉看了第二次这种 最小变成 最小割的建图, 悟出了一点东西
大概,想表达的意思就是,每个点和S同块还是T同块, 而和S同块/T同块 分别表示选和不选的一个对立状态, 而建立的容量边, 便是这些选择状态会长生的代价贡献
H
学了maspy的生成方程,感觉又多会了一点
参考
官方题解
maspy
我基于maspy做的翻译和笔记