The 2019 ICPC Asia-East Continent Final
大部分学习于:The 2019 ICPC Asia-East Continent Final(部分题解)
欠了一屁股的题目要补,倒序开始做吧!
大概有10套题目 & 7套网络赛题目…(署训就…)
A
在瓜大签了个A,之后坐着数了4个小时气球。
补题
M
做的时候为什么没有想到暴力…?
从1到n获取一些基底,满足基底不是任何其他数字的幂次;以这些基底和其若干次幂组成一串元素,枚举这一串数字的选取情况,不断更新最大值即可。
容易知道一串元素至多有20个,所以可以用二进制数表示这20个元素的选取情况。在每一种选取情况中嵌套循环来搜索那些需要减去 b [ i ] b[i] b[i]的位置并打上标记,读题有一个地方不是很对:只要有一对 ( i , j ) (i, j) (i,j)满足 i k = j i ^ k = j ik=j就要减去一次 b [ j ] b[j] b[j],最开始的理解是存在上述元素对 ( i , j ) (i, j) (i,j)就仅减去一次 b [ j ] b[j] b[j],这也是后来补题的时候卡了很久的地方。
AC代码
E
给出一个有向简单图,顶点1到顶点n之间有若干条长度相同、互不相交的路径;每条路径具有容量,一次操作可以将某条边容量减一而另一条边容量加一,问最少多少次操作后可以达到最大流量?
自己考虑的时候先注意到了最大流量即为 t o t a l F l o w / l i n k L e n g t h totalFlow / linkLength totalFlow/linkLength,之后打算通过某种手段最优地进行分配…不过走的有点偏 不是很会做
这道题的关键在于这种往楼梯灌水
的贪心思想。我们不关心总流量最大是多少,只需要知道一定存在某种策略可以将高处盈余的流量拿到低部补充水位,据此我们就尝试将这段楼梯的最低水位尽量变高。
我们先统计totalFlow(以下简称flow). 然后我们将k条路径按照权值排序后合并成一条大路径。这是因为如果固定升高的水位高度,将那些比较靠后的楼梯填满比那些比较靠前的楼梯填满要花费更多的水(因为长度差异),因而我们应该在横向上逐个进行装水,并且可以将k条楼梯全部合并在一起考虑;在合并的过程,我们将flow减去每一条楼梯的基准线,即
flowchat -= len * 1ll * vec[cnt][0]
这样子操作完后,得到的flow就是我们拆完除了基准平面后的所有楼梯后可以自由支配的用于再造楼梯的水。之后,我们记录大楼梯每一个小台阶之间的间距,并以此做以下扩流操作:
ll ans = 0;
for(int i = 1; i <= len - 1; i++){
if(flow < len) break;
ll temp = min(addflow[i], flow / len);
ans += i * temp;
flow -= len * temp;
}
如果剩余的流量较多,可以让第i个空隙的高度被填满,那么消耗的flow为 l e n ∗ t e m p len * temp len∗temp,但是其中有一部分是原本存在的楼梯,所以再造的花费只有 i ∗ t e m p i * temp i∗temp;如果剩余流量不够造完整i号空隙那么高的水平面,那么水流就只能上升 f l o w / l e n flow / len flow/len的高度,ans和flow的变化与前一种情况类似,就不予赘述了。
不过为什么WA了10发呢…?
因为我傻逼没有看到int * int爆long long的情况
: )
H
本题的突破点在于,在n个数的序列中需要找出至少n/2个数构成新的等比数列,所以这些被选择的数字之间的平均间距不可能太大,下面我们就定量的进行分析计算。
我们假设已经选择了不少于n/2个数字构成了等比数列。设x1表示原序列中这些元素之间间距 d < = 2 d<=2 d<=2的间距个数,x2表示间距 d > 2 d>2 d>2的间距个数,有:
x 1 + x 2 > = c e i l ( n / 2 ) − 1 x1 + x2 >= ceil(n/2) - 1 x1+x2>=ceil(n/2)−1
n − 1 > = c 1 ∗ x 1 + c 2 ∗ x 2 > = x 1 + 3 ∗ x 2 n - 1>=c1*x1 + c2*x2 >=x1 + 3*x2 n−1>=c1∗x1+c2∗x2>=x1+3∗x2
联立两不等式可得
n − 1 > = x 1 + 3 ( c e i l ( n / 2 ) − 1 − x 1 ) n-1 >= x1 + 3(ceil(n/2)-1-x1) n−1>=x1+3(ceil(n/2)−1−x1)
即
x 1 > = 1 2 ( 3 ∗ c e i l ( n / 2 ) − n − 2 ) x1>=\frac12(3*ceil(n/2)-n-2) x1>=21(3∗ceil(n/2)−n−2)
所以我们只需要用map统计相邻一位、两位的公比,并对那些出现次数大于上述下限的公比进行搜索答案即可,而搜索办法为开设map倒序进行dp,即可获得最大长度。
AC代码
const int maxn = 2e5 + 10;
int n, m;
int a[maxn];
unordered_map<int, int> ma;
vector<int> q;
//传入x,返回在模m下的逆元。
ll getInv(int a){
ll x, y;
exgcd((ll)a, m, x, y);
return x % m;
}
int inv[maxn];
int count(int q){
int res = 0;
unordered_map<ll, int> mp;
if(q == 1){
for(itn i = n - 1; i >= 0; i--){
mp[a[i]]++;
res = max(res, mp[a[i]]);
}
return res;
}
for(int i = n - 1; i >= 0; i--){
mp[a[i]] = 1;
mp[a[i]] = max(mp[a[i]], mp[a[i] * 1ll * q % m] + 1);
res = max(res, mp[a[i]]);
}
return res;
}
//#define LOCAL
int main(){
#ifdef LOCAL
freopen("in.txt", "r", stdin);
#endif
int _; scanf("%d", &_);
while(_--){
scanf("%d %d", &n, &m);
for(int i = 0; i < n; i++){
scanf("%d", a + i);
inv[i] = getInv(a[i]) % m;
}
for(int i = 0; i + 1 < n; i++){
ll temp = (ll)a[i + 1] * inv[i]; temp %= m;
if(temp < 0) temp += m;
ma[(int)temp]++;
}
for(int i = 0; i + 2 < n; i++){
ll temp = (ll)a[i + 2] * inv[i]; temp %= m;
if(temp < 0) temp += m;
ma[(int)temp]++;
}
int least = 3 * (n + 1) / 2 - n - 2; least /= 2;
int ans = 0;
for(auto i: ma) if(i.second >= least){
ans = max(ans, count(i.first));
}
if(ans < (n + 1) / 2) puts("-1");
else printf("%d\n", ans);
ma.clear();
}
return 0;
}
随机化做法
随机枚举原序列中的一些点,以其相邻公比和隔一项公比作为待选公比,用该公比往前往后寻找新序列最长长度,不断更新答案即可。
C
学习于2019ICPC西安 C. Dirichlet k-th root & 开头提到的CSDN博客
已知函数 f f f的k次狄利克雷卷积后的 n n n个函数值: g [ 1 ] , g [ 2 ] , … , g [ n ] g[1], g[2], \dots,g[n] g[1],g[2],…,g[n],求原 n n n个函数值 f [ 1 ] , f [ 2 ] , … , f [ n ] f[1], f[2], \dots,f[n] f[1],f[2],…,f[n]。
虽然想做这么简洁的题目,但是我太菜了。
解决办法的关键是发现:(
f k ( n ) = k ∗ f ( n ) + b ( n , k ) … … … ( ∗ ) f^k(n) = k*f(n) + b(n, k)\dots\dots\dots(*) fk(n)=k∗f(n)+b(n,k)………(∗)
其实如果画出了k次卷积的树状图后容易发现该性质,但是下一步的递推优化并不会(更何况M题也过不去)
之后我们尝试使用一些办法来递推地获取要求的数值,不过我们并不需要知道所有情况,只需要知道一些必要的情况即可。为此,我们采用如下的递归式:
f 2 k ( n ) = f k ( n ) ∗ f k ( n ) = 2 f k ( n ) + ∑ d ∣ n , d ≠ 1 , d ≠ n f k ( d ) f k ( n d ) f^{2k}(n)=f^k(n)*f^k(n)=2f^k(n)+\sum_{d|n ,d≠1,d≠n}f^k(d)f^k(\frac nd) f2k(n)=fk(n)∗fk(n)=2fk(n)+d∣n,d=1,d=n∑fk(d)fk(dn)
f k + 1 ( n ) = f k ( n ) ∗ f ( n ) = f k ( n ) + f ( n ) + ∑ d ∣ n , d ≠ 1 , d ≠ n f k ( d ) f ( n d ) f^{k+1}(n)= f^k(n)*f(n)=f^k(n)+f(n)+\sum_{d|n ,d≠1,d≠n}f^k(d)f(\frac nd) fk+1(n)=fk(n)∗f(n)=fk(n)+f(n)+d∣n,d=1,d=n∑fk(d)f(dn)
显然,每一个 k ( k = 1 , 2 , … , K ) k(k=1,2,\dots,K) k(k=1,2,…,K)都可以用这种关系在log的时间下进行递推求解,下面我们就考虑如何进行状态转移。
由 ( ∗ ) (*) (∗)式,将上述表达式化简:
b ( n , 2 k ) = 2 b ( n , k ) + ∑ d ∣ n , d ≠ 1 , d ≠ n f k ( d ) f k ( n d ) b(n,2k)=2b(n,k)+\sum_{d|n ,d≠1,d≠n}f^k(d)f^k(\frac nd) b(n,2k)=2b(n,k)+d∣n,d=1,d=n∑fk(d)fk(dn)
b ( n , k + 1 ) = b ( n , k ) + b ( n , 1 ) + ∑ d ∣ n , d ≠ 1 , d ≠ n f k ( d ) f ( n d ) b(n,k+1)=b(n,k)+b(n,1)+\sum_{d|n ,d≠1,d≠n}f^k(d)f(\frac nd) b(n,k+1)=b(n,k)+b(n,1)+d∣n,d=1,d=n∑fk(d)f(dn)
容易知道,为了更新 b ( n , k ) b(n,k) b(n,k)只需要知道 f f f和 b b b在 n n n和 k k k更小的情况下的数值,而更新 f ( n , k ) f(n,k) f(n,k)则可以根据 ( ∗ ) (*) (∗)式由 b ( n , k ) b(n,k) b(n,k)获得。值得注意的是, ( ∗ ) (*) (∗)只能更新 k > 1 k>1 k>1的情况,那么 k = 1 k=1 k=1的情况应该如何处理呢?解决办法是,将 k k k取为 K K K,利用K次卷积后的函数值反解出 f ( i , 1 ) f(i,1) f(i,1),再利用 f ( i , 1 ) f(i,1) f(i,1)迭代计算下一次递推的结果。
代码
const int maxn = 1e5 + 10;
const int maxm = 64;
const int M = 998244353;
int g[maxn];
int id[maxm];
int f[maxn][maxm], b[maxn][maxm], top = 0;
int getInv(int a){
ll x, y;
exgcd(a, M, x, y);
return x % M;
}
vector<int> d[maxn];
//#define LOCAL
int main(){
#ifdef LOCAL
freopen("in.txt", "r", stdin);
#endif
int n, k; scanf("%d %d", &n, &k);
ll invk = getInv(k); invk %= M; if(invk < 0) invk += M;
for(int i = 1; i <= n; i++) scanf("%d", g + i);
//获取所有因子
for(int i = 2; i <= n; i++) for(int j = 2; j * 1ll * i <= n; j++)
d[j * i].emplace_back(i);
//id数组从大到小存储了递归求解的k顺序。
int temp = k;
while(temp){
id[top++] = temp;
temp = temp & 1? temp - 1: temp >> 1;
}
for(int i = 0; i < top; i++) f[1][i] = 1;
//f[1] - f[i - 1] is known, now count f[i]
for(int i = 2; i <= n; i++){
for(int j = top - 2; j >= 0; j--){
ll temp = b[i][j + 1];
if(!(id[j] & 1)) temp <<= 1;
temp %= M;
int sec = id[j] & 1? top - 1: j + 1;
for(auto p: d[i]){
temp += f[p][j + 1] * 1ll * f[i / p][sec];
temp %= M;
}
b[i][j] = temp % M;
}
//g(n) is known, so f[i][1] can be calculated.
f[i][top - 1] = ((g[i] - b[i][0] + M) % M * 1ll * invk) % M;
if(f[i][top - 1] < 0) f[i][top - 1] += M;
for(int j = top - 2; j >= 0; j--){
f[i][j] = ((id[j] * 1ll * f[i][top - 1]) % M + b[i][j]) % M;
if(f[i][j] < 0) f[i][j] += M;
}
}
for(int i = 1; i <= n; i++) printf("%d ", f[i][top - 1]);
printf("\n");
return 0;
}
一些注意点
id数组是用来记录那些必要的k的值的,而且其下标大小关系与对应的k的大小关系相反,所以在使用的时候一定要仔细考虑!
在牛客上49行最初还多了一个取模操作,不过在CF上TLE在了Test23上…找了很久才找到49行的取余操作是T点…尽管知道取模很慢,但是也没有想到会慢到一个oj能过一个oj被卡…为此我还是去具体测试了一下取模到底多慢:
在 n = 5 e 7 n=5e7 n=5e7下进行测试:
for循环耗时:0.1s
for循环内x自减: 0.07s…(??)
n次long long加法: 0.1s
n次long long乘法: 0.1s
n次long long除法: 0.4s
n次long long % int: 0.5s
以前好像做过这个实验,不过这还是第一次被%卡住,以后打题目遇到数学题要注意尽量减少大数取模。
Dirchlet性质
快速幂&卷积: 如果 f f f在模 m o d mod mod下进行 k k k次卷积得到 g g g,那么 g g g进行 i n v ( k ) inv(k) inv(k)次卷积后就会得到 f f f。
具体代码容易实现,可以参见CSDN博客的代码。
但是该怎么证明…?
没学好数论,肯定是不会证明的。。
过一阵子打算开一个有关Dirichlet卷积的专题练习…到时候回头来看一下。