Codeforces Round #708 (Div. 2) 賽後補題
比賽傳送門
B題傳送門
題解:
本題其實只要將所有的數讀進來,將每個數模上m後的數值的個數存下來就好了。利用\(map\)來記錄,這樣我們只要通過\(map\)記錄下來的個數來計算我們的結果。
假設數模上\(m\)後的值為\(x\)。
①當\(x\)等於\(0\)的時候,我們就可以把所有\(x=0\)的數值放到同一個組中。
②當\(x\)不等於\(0\)的時候,我們可以放到一起的情況就是\(x,m-x,x,....\),這樣的一組中,\(x\)與\(m-x\)的個數差為1,那麼如果其中一個的個數偏多的話,多出的部分都是單獨一組,也就是\(abs(cnt[x]-cnt[m-x])-1\)
程式碼:
code
#include<bits/stdc++.h> using namespace std; int main() { int t; cin>>t; while(t--) { int n,m; scanf("%d%d",&n,&m); map<int,int> cnt;//cnt用來記錄 for(int i=0;i<n;i++) { int x; scanf("%d",&x); cnt[x%m]++; } int ans=0; for(auto it=cnt.begin();it!=cnt.end();it++) { if(it->first==0) ans++; else if(it->first*2==m) ans++; else { if(it->first*2<m||cnt.find(m-it->first)==cnt.end()) { int x=it->second,y=cnt[m-it->first]; ans+=1+max(0,abs(x-y)-1); } } } cout<<ans<<endl; } return 0; }
D題傳送門
題解:
本題其實比較容易看出是動態規劃的問題,但是題目中的記憶體限制為\(32mb\),這樣我們就不能開一個二維的\(dp\)陣列來計算所有的可能性。
最開始的時候我沒有看\(n\)的限制,以為是一個狀態壓縮\(dp\)(狀壓\(dp\)資料範圍小),等想到一半看了一眼n的範圍就知道肯定不是狀壓\(dp\)了,最後還是去看了官方的\(Tutorial\).下面就是官方給出來的思路:
根據資料我們可以構造出一個圖,其中節點就是我們的要解決的問題,那麼對於節點\((i,j)\)來說,邊就是\(|c_i-c_j|\),並且這個值還是獨一無二的。為什麼這個值是獨一無二的呢,這裡我們每個問題的複雜度(題目給出)的形式是很特殊的,如果我們用\(i_{th}\)
我們來定義一下dp陣列代表什麼意思,\(dp[i]\)表示在解決問題i後能夠獲得的分數。最開始初始化的時候,\(dp\)陣列應該全部為\(0\)(由題目計算分數的方式得出)。由於在解決問題的過程中\(IQ\)會不斷增長,我們要獲得多的分數也需要解決更多的問題,那麼就需要從小到大計算邊。具體我們只要對於每個\(j\)\((2<j<n)\) 時,\(i\)從\(j-1\)開始遞減即可(思考一下為什麼)。
接下來我們需要思考一下如何更新\(dp\)陣列中的值,首先是兩個問題的\(tag\)是不能一樣的,所以我們就不需要計算這種情況。計算當前\(i\)和\(j\)的時候,我們是先解決\(i_{th}\)問題還是\(j_{th}\)問題,權值是不會變化的,也就是\(dp[i]=max(dp[i],dp[j]+p)\)與\(dp[j]=max(dp[j],dp[i]+p)\)要同時進行更新。由於這裡會套用兩個\(dp\)的值,而且會更新,所以我們可以用兩個變數先存一下就好。
程式碼:
code
#include<bits/stdc++.h>
using namespace std;
int main()
{
int t;
cin>>t;
while(t--)
{
int n;
scanf("%d",&n);
vector<long long> tags(n),s(n),dp(n,0);
for(int i=0;i<n;i++) scanf("%lld",&tags[i]);
for(int i=0;i<n;i++) scanf("%lld",&s[i]);
for(int j=1;j<n;j++)
{
for(int i=j-1;i>=0;i--)
{
if(tags[i]==tags[j]) continue;
long long p=abs(s[j]-s[i]);
long long dpj=dp[j],dpi=dp[i];
dp[i]=max(dp[i],dpj+p);
dp[j]=max(dp[j],dpi+p);
}
}
printf("%lld\n",*max_element(dp.begin(),dp.end()));
}
return 0;
}
E1. Square-free division (easy version)
題解:
本題是需要我們去判斷任意兩個數是否相乘後會變成一個完全平方數,如果我們使用暴力的辦法將所有加入佇列的數和目前這個數去一一判斷的話,時間複雜度極高,最壞情況下會達\(O(N^3)\),就無法在題目要求的範圍內完成。
本題需要知道的知識點,完全平方數、質因數以及質因數分解。
由質因數我們可以知道,任意一個合數都是有一個獨一無二的質因子分解式。因此每個數其實我們都可以用他們的質因子分解式來替代。那麼什麼時候兩個數相乘的結果會是一個完全平方數呢?我們先來看一下題目中的例子,比如說\(6=2^1*3^1\),而\(24=3^1*2^3\),當\(6*24=144\)的時候,\(144=3^2*2^4\),,這個時候\(144\)是一個完全平方數,因為\(3\)和\(2\)的指數都是偶數,我們繼續取\(8\),得到\(8=2^3\),當\(8*24=192\)時,\(192=3^1*2^4\),但是\(192\)並不是一個完全平方數,其質因數分解式的指數並不全為偶數。
從上面的分析我們可以知道,如果兩個數的乘積是完全平方數的話,這兩個數就需要滿足其質因數分解式相乘後各項因子的指數為偶數,既然如此,我們就定義一個\(mask\)函式,讓\(mask(x)=p_1^{k_1mod2}p_2^{k_2mod2}...p_n^{k_nmod2}\),因此,只要兩個數的\(mask\)是相等的,那麼我們就知道這兩個數相乘的乘積為完全平方數。
所以在最開始,我們需要預處理出資料範圍內的素數,然後將記下每個數的質因數分解式中最大的素數,用一個數組來儲存,這樣我們之後計算\(mask\)的時候就可以直接得到想要的素數。完成預處理後,我們將每個數讀進來,將每個數的\(mask\)計算出來,用一個\(cnt\)來記錄某個素數的指數,如果是奇數的話,我們就將\(mask\)乘上該素數。最後計算答案的時候,我們只要用貪心的思想,如果不能加入當前分段的話,我們就新開一個分段,並結束當前分段的計算。
程式碼:
code
#include<bits/stdc++.h>
using namespace std;
const int N=1e7;
vector<int> primes;
int mind[N+1];
int main()
{
//預處理
for(int i=2;i<=N;i++)
{
if(mind[i]==0)
{
primes.push_back(i);
mind[i]=i;
}
for(auto &x:primes)
{
if(x>mind[i]||x*i>N) break;
mind[i*x]=x;
}
}
int t;
scanf("%d",&t);
while(t--)
{
int n,k;
scanf("%d%d",&n,&k);
vector<int> a(n,1);//我們的mask陣列
for(int i=0;i<n;i++)
{
int x;
scanf("%d",&x);
//現在要對x進行拆分
int cnt=0;//用來記錄每個素數的個數
int last=0;//記錄最後一個標記到的素數
while(x>1)
{
int p=mind[x];
if(p==last) cnt++;//這個素數的個數增加一個
else
{
if(cnt%2==1)//我們要求的是讓指數在0到1這個區間,如果是奇數的話,說明可以留下
a[i]*=last;
cnt=1;
last=p;
}
x/=p;
}
if(cnt%2==1)
{
a[i]*=last;
}
}
int L=0,ans=1;//從第一個開始
map<int,int> last;
for(int i=0;i<n;i++)
{
if(last.find(a[i])!=last.end()&&last[a[i]]>=L)
{
L=i;
ans++;
}
last[a[i]]=i;
}
printf("%d\n",ans);
}
return 0;
}
E2. Square-free division (hard version)
題解:
本題是\(E1\)的困難版本,與\(E1\)不同的是,本題允許我們做\(k\)次的修改,可以將陣列中任意一個數變成另外一個正數。讓我們求出經過不超過\(k\)次修改後的陣列能夠分成的最小分段數。
如何判斷兩個數的乘積是否為完全平方數的方法,我們在\(E1\)中已經瞭解過了,所以我們這裡繼續使用\(mask(x)=p_1^{k_1mod2}p_2^{k_2mod2}...p_n^{k_nmod2}\),接下來我們就需要思考如何進行不超過\(k\)次的操作對陣列的影響。
容易想到的是,我們可以利用深度優先搜尋,將所有可能的修改位置的情況都列舉一遍,記錄下每種情況下我們可以獲得的最小分段數。但是對於題目中給出的資料範圍來說,這樣的辦法是一定會超時的,而且在實現上,我們是需要重新修改我們對應\(pos\)上數值的\(mask\)值的。
於是,我們想到可以利用動態規劃的思路來列舉情況,定義\(dp_{i,j}\)為經過不超過\(j\)次修改後,從\(1\to i\)能夠得到的分段數,\(dp\)陣列的屬性是得到最小值。接下來我們需要思考用什麼來進行狀態劃分,本題有點類似石子合併的思路,對於\(dp_{i,j}\),我們需要列舉\(\left\{{0,j}\right\}\)間所有的修改情況,所以我們還缺乏一個數組用來記錄不同位置數值修改後對陣列分段的影響。
我們現在定義一個\(left_{i,j}\)陣列,我們找到一個\(p\),也是我們\(left\)陣列中需要儲存的值,在經過\(j\)次修改後,讓\(a_p,a_{p+1},a_{p+2},...,a_{i}\)間不存在\(mask\)相同的兩個數。對於一個確定的\(j\),如果我們讓\(i\)增大,那麼\(left_{i,j}\)也會增大,因此對於一個確定的\(j\),我們可以利用雙指標的辦法計算出\(left_{i,j}\),這裡演算法時間複雜度為\(O(nk)\)。
現在有了left陣列後,我們繼續思考\(dp_{i,j}\)的狀態轉移方程,首先\(dp_{i,j}\)可以從\(dp_{i,j-1}\)轉移而來,然後我們還需要列舉從\(\left\{{0,j}\right\}\)之間的狀態,假設\(p=left_{i,x}\),那麼\(dp_{i,j}=min(dp_{i,j},dp_{p-1,j-x}+1)\)。該演算法的時間複雜度為\(O(nk^2)\),所以整體演算法的時間複雜度為\(O(nk^2)\),可以滿足題目的時限。
程式碼:
code
#include<bits/stdc++.h>
using namespace std;
const int N=1e7;
const int INF=0x3f3f3f3f;
vector<int> primes;
int mind[N+1];
int main()
{
//第一步操作和E1是一樣的,計算出mind
for(int i=2; i<=N; i++)
{
if(mind[i]==0)
{
mind[i]=i;
primes.push_back(i);
}
for(auto &x:primes)
{
if(x>mind[i]||x*i>N) break;
mind[x*i]=x;
}
}
int t;
cin>>t;
vector<int> cnt(N+1);
while(t--)
{
int n,k;
scanf("%d%d",&n,&k);
vector<int> mask(n,1);
for(int i=0; i<n; i++)
{
int x;
scanf("%d",&x);
int cnt=0,last=0;
while(x>1)
{
int p=mind[x];
if(p==last) cnt++;
else
{
if(cnt&1) mask[i]*=last;
last=p;
cnt=1;
}
x/=p;
}
if(cnt&1) mask[i]*=last;
}
vector<vector<int>> left(n,vector<int>(k+1));
for(int j=0; j<=k; j++)
{
int p=n,now=0;
for(int i=n-1; i>=0; i--)
{
while((p-1)>=0 &&(now + (cnt[mask[p - 1]] > 0)) <= j)
{
p--;
now+=(cnt[mask[p]]>0);
cnt[mask[p]]++;
}
left[i][j]=p;
if(cnt[mask[i]]>1) now--;
cnt[mask[i]]--;
}
}
vector<vector<int>> dp(n+1,vector<int>(k+1,INF));
for(auto &c:dp[0]) c=0;
for(int i=1; i<=n; i++)
{
for(int j=0; j<=k; j++)
{
if(j>0) dp[i][j]=dp[i][j-1];
for(int x=0; x<=j; x++)
{
dp[i][j]=min(dp[i][j],dp[left[i-1][j-x]][x]+1);
}
}
}
int ans=INF;
for(auto &c:dp.back()) ans=min(ans,c);
printf("%d\n",ans);
}
return 0;
}