בעיית תרמיל הגב

בעיית תרמיל הגב (באנגלית: Knapsack problem) היא בעיית מיטוב קומבינטורית הנחקרת בתחום מדעי המחשב. בבעיה זו נתונה קבוצת עצמים שלכל אחד מהם משקל ומחיר, ובנוסף נתון חסם על המשקל. המטרה היא למצוא אוסף של העצמים הנתונים שסכום משקליהם אינו עולה על החסם הנתון, ומחירו מרבי. הבעיה נחשבת לבעיה חשובה במדעי המחשב בגלל הערך התאורטי שלה (הבעיה היא NP-שלמה), ועקב השימושים שלה בתחומים כגון הקצאת משאבים וקריפטוגרפיה. נתונים n {\displaystyle n} עצמים ולכל עצם i {\displaystyle i} נתונים מחירו p i {\displaystyle p_{i}} ומשקלו w i {\displaystyle w_{i}} . לכל עצם i {\displaystyle i} נסמן ב- x i {\displaystyle x_{i}} את מספר העצמים מהחפץ שנכניס לתרמיל. המטרה היא למקסם את הביטוי ∑ i = 1 n p i x i {\displaystyle \sum _{i=1}^{n}p_{i}x_{i}} (המחיר הכולל של העצמים בתרמיל) בכפוף לאילוץ ∑ i = 1 n w i x i ≤ W {\displaystyle \sum _{i=1}^{n}w_{i}x_{i}\leq W} (משקלם הכולל של החפצים אינו עולה על החסם). הגרסה הנפוצה והפשוטה ביותר היא בעיית תרמיל הגב הבינארי: בבעיה זו לכל חפץ יש עותק אחד בלבד ולכן x i ∈ { 0 , 1 } {\displaystyle x_{i}\in \{0,1\}} (כלומר, אם חפץ מסוים נכנס לתרמיל אזי יש רק אחד מסוגו בתרמיל). בבעיית תרמיל הגב החסומה, לכל עצם i {\displaystyle i} יש כמות c i {\displaystyle c_{i}} ומותר להכניס לתרמיל מספר עותקים של העצם: x i ∈ { 0 , 1 , . . . , c i } {\displaystyle x_{i}\in \{0,1,...,c_{i}\}} . בבעיית תרמיל הגב הלא חסומה, מותר לקחת מספר עותקים בלתי מוגבל מכל עצם: x i ≥ 0 {\displaystyle x_{i}\geq 0} . גרסת ההכרעה של בעיית תרמיל הגב היא: בהינתן n {\displaystyle n} עצמים עם מחירים p i {\displaystyle p_{i}} ומשקלים w i {\displaystyle w_{i}} , ובהינתן מחיר כולל P {\displaystyle P} וחסם על המשקל W יש להכריע האם קיימת בחירה של העצמים כך ש- ∑ i = 1 n p i x i ≥ P {\displaystyle \sum _{i=1}^{n}p_{i}x_{i}\geq P} תחת האילוץ ∑ i = 1 n w i x i ≤ W {\displaystyle \sum _{i=1}^{n}w_{i}x_{i}\leq W} . בעיה זו היא NP-שלמה: הוכחה שהבעיה ב-NP מתבצעת על ידי ההבחנה שבהינתן פתרון x {\displaystyle x} ניתן לוודא בזמן פולינומי האם מתקיימים התנאים ∑ i = 1 n p i x i ≥ P {\displaystyle \sum _{i=1}^{n}p_{i}x_{i}\geq P} ו- ∑ i = 1 n w i x i ≤ W {\displaystyle \sum _{i=1}^{n}w_{i}x_{i}\leq W} . הוכחה שהבעיה היא NP-קשה מתבצעת על ידי רדוקציה מבעיית החלוקה: בהינתן מופע של בעיית החלוקה a 1 , a 2 , . . . , a n {\displaystyle a_{1},a_{2},...,a_{n}} נוכל להגדיר מופע של בעיית תרמיל הגב שבו לכל i {\displaystyle i} מתקיים w i = p i = a i {\displaystyle w_{i}=p_{i}=a_{i}} וכמו כן מתקיים W = P = ∑ i = 1 n a i 2 {\displaystyle W=P={\frac {\sum _{i=1}^{n}a_{i}}{2}}} . קיים תרמיל חוקי עבור מופע הבעיה אם ורק אם קיימת חלוקה של העצמים A = a 1 , a 2 , . . . , a n {\displaystyle A={a_{1},a_{2},...,a_{n}}} לשתי קבוצות בעלות סכום זהה: בהינתן חלוקה A = X ∪ Y {\displaystyle A=X\cup Y} התרמיל החוקי יהיה פשוט X {\displaystyle X} (נשים לב שמשקל התרמיל ומחירו הם בדיוק מחצית מסכום איברי A {\displaystyle A} ), ובהינתן תרמיל X {\displaystyle X} החלוקה של A {\displaystyle A} תהיה לקבוצות X , A / X {\displaystyle X,A/X} (שוב, מטעמי חוקיות הפתרון, סכום כל קבוצה הוא מחצית סכום איברי A {\displaystyle A} ). כיוון שהבעיה היא NP-שלמה, לא ידועים אלגוריתמים פולינומיים אשר מוצאים פתרון מיטבי. עם זאת, בגרסת הבעיה בה מובטח שכל משקלי הפריטים הם מספרים שלמים, קיים אלגוריתם המבוסס על תכנון דינמי אשר פותר באופן מיטבי את הבעיה בסיבוכיות זמן פסאודו-פולינומית (אם משקלי הפריטים לא שלמים מספר תתי הבעיות עלול להיות מעריכי ביחס לקלט). האלגוריתם הבא פותר את בעיית תרמיל הגב הבינארית בסיבוכיות זמן O ( n W ) {\displaystyle O(nW)} . בהינתן מופע של הבעיה כמתואר בחלק "הגדרת הבעיה", נגדיר m [ i , w ] {\displaystyle m[i,w]} להיות מחיר הפתרון המיטבי שניתן להשיג באמצעות עצמים שמספר הסידורי שלהם אינו עולה על i {\displaystyle i} ושסכום משקלם אינו עולה על w {\displaystyle w} . הגדרה זו מאפשרת פיתוח נוסחת נסיגה ל- m [ i , w ] {\displaystyle m[i,w]} : אם w i > w {\displaystyle w_{i}>w\,\!} אז מתקיים m [ i , w ] = m [ i − 1 , w ] {\displaystyle m[i,\,w]=m[i-1,\,w]} (לא ניתן להשתמש בעצם שמספרו i {\displaystyle i} ). אם w i ≤ w {\displaystyle w_{i}\leq w} אז מתקיים m [ i , w ] = max ( m [ i − 1 , w ] , m [ i − 1 , w − w i ] + p i ) {\displaystyle m[i,\,w]=\max(m[i-1,\,w],\,m[i-1,w-w_{i}]+p_{i})} (ניתן להשתמש בעצם שמספרו i {\displaystyle i} והמחיר המיטבי הוא המחיר המרבי מבין המקרה בו הערך נבחר והמקרה בו הוא לא נבחר). תנאי הבסיס ייקבע להיות m [ 0 , w ] = 0 {\displaystyle m[0,w]=0} לכל w {\displaystyle w} (ללא עצמים המחיר הכולל הוא תמיד 0). כעת ניתן, באמצעות חישוב ערכי הטבלה m {\displaystyle m} , לחשב ברקורסיה את m [ n , W ] {\displaystyle m[n,W]} , הלוא הוא המחיר המיטבי לבעיה. הקוד הבא בשפת Python מממש את האלגוריתם המתואר: def knapsack01_dp(items, limit): table = [[0 for w in range(limit + 1)] for j in range(len(items) + 1)] for j in range(1, len(items) + 1): item, wt, val = items[j-1] for w in range(1, limit + 1): if wt > w: table[j][w] = table[j-1][w] else: table[j][w] = max(table[j-1][w], table[j-1][w-wt] + val) result = [] w = limit for j in range(len(items), 0, -1): was_added = table[j][w] != table[j-1][w] if was_added: item, wt, val = items[j-1] result.append(items[j-1]) w -= wt return result גישה אחרת לפתרון הבעיה היא שימוש באלגוריתמי קירוב. לבעיה זו קיימים מספר אלגוריתמי קירוב כולל סכמת קירוב פולינומית מלאה (FPTAS). האלגוריתם החמדן הבא מוצא תרמיל שמחירו לכל הפחות מחצית המחיר של הפתרון המיטבי (2-קירוב): נמיין את העצמים (שמשקלם אינו גדול מהחסם; עצם שמשקלו גדול מהחסם W {\displaystyle W} אינו רלוונטי לבעיה) לפי יחס המחיר-משקל שלהם p i / w i {\displaystyle p_{i}/w_{i}} . נוסיף לתרמיל, כל עוד זה אפשרי, את העצם הבא ברשימה הממוינת לפי יחס מחיר-משקל (מתחילים עם העצם בעל היחס הגבוה ביותר). נסמן ב- j {\displaystyle j} את מספר העצמים שהצלחנו להכניס בצורה זו, ולכן ניתן לתאר את המחיר של העצמים שהכנסנו על ידי: ∑ i = 1 j p i {\displaystyle \sum _{i=1}^{j}p_{i}} . כעת נתבונן על המחיר של העצם ה- j + 1 {\displaystyle j+1} : ‏ p j + 1 {\displaystyle p_{j+1}} . אם ∑ i = 1 j p i > p j + 1 {\displaystyle \sum _{i=1}^{j}p_{i}>p_{j+1}} , נשאיר את j {\displaystyle j} העצמים בתיק, ואם ∑ i = 1 j p i < p j + 1 {\displaystyle \sum _{i=1}^{j}p_{i}<p_{j+1}} , נוציא את j {\displaystyle j} העצמים מהתיק, ונכניס במקומם את העצם ה- j + 1 {\displaystyle j+1} . הוכחה שאלגוריתם זה נותן פתרון 2-קירוב: נסמן ב- m {\displaystyle m} את הערך של הפתרון האופטימלי. נתבונן בבעיה דומה בה מותר לנו להכניס חלקי עצמים (נניח חצי עצם), ולא רק עצמים שלמים, ונסמן ב- M {\displaystyle M} את הערך של הפתרון האופטימלי עבור בעיה זו. הפתרון האופטימלי עבור בעיה בה מותר להכניס חלקי עצמים, הוא גדול-שווה לפתרון האופטימלי של הבעיה המקורית בה ניתן להכניס רק עצמים שלמים (שכן כל פתרון עבור הבעיה המקורית, הוא גם פתרון אפשרי של הבעיה עם חלקי עצמים). אם יכולנו להכניס לתרמיל חלקי עצמים, אזי ברור שהפתרון האופטימלי היה להכניס את j העצמים הראשונים, ולהכניס חלק מהעצם ה-j+1 שבדיוק ממלא את המשקל שניתן עוד להכניס לתרמיל (המשקל שניתן עוד להכניס לתרמיל לאחר הכנסת j העצמים הראשונים הוא W − ∑ i = 1 j w i {\displaystyle W-\sum _{i=1}^{j}w_{i}} , ולכן החלק של העצם ה-j+1 שיוכנס לתרמיל הוא x j + 1 = W − ∑ i = 1 j w i w j + 1 < 1 {\displaystyle x_{j+1}={\frac {W-\sum _{i=1}^{j}w_{i}}{w_{j+1}}}<1} ). לכן: ∑ i = 1 j + 1 p i = ∑ i = 1 j p i + p j + 1 ≥ ∑ i = 1 j p i + x j + 1 p j + 1 = M ≥ m {\displaystyle \sum _{i=1}^{j+1}p_{i}=\sum _{i=1}^{j}p_{i}+p_{j+1}\geq \sum _{i=1}^{j}p_{i}+x_{j+1}p_{j+1}=M\geq m} ,     ולענייננו מה שחשוב זה ש:   ∑ i = 1 j + 1 p i ≥ m {\displaystyle \sum _{i=1}^{j+1}p_{i}\geq m} ובמילים: המחיר של העצמים 1 עד j+1 גדול מהמחיר של עצמים 1 עד j + המחיר של חלק מעצם j+1, ששווה ל-M (הפתרון של הבעיה עם חלקי עצמים), שגדול-שווה ל-m (הפתרון של הבעיה שלנו). לכן, אם נחלק את הסכום ∑ i = 1 j + 1 p i {\displaystyle \sum _{i=1}^{j+1}p_{i}} ל- ∑ i = 1 j p i {\displaystyle \sum _{i=1}^{j}p_{i}} ול- p j + 1 {\displaystyle p_{j+1}} , בהכרח לפחות אחד משני הביטויים הללו חייב להיות בעל מחיר של לפחות m 2 {\displaystyle {\frac {m}{2}}} , כיוון שסכום המשקלים של שניהם חייב להיות גדול-שווה ל- m {\displaystyle m} . David Pisinger, Algorithms for Knapsack Problems, Ph.D thesis, February 1995, University of Copenhagen מימושים של בעיית תרמיל הגב הבינארית במגוון שפות תכנות בעיית תרמיל הגב, באתר MathWorld (באנגלית) Vazirani, Algorithms, 2006 David Pisinger, Algorithms for Knapsack Problems ,1995 בעיית החלוקה היא בעיית ההכרעה המוגדרת באופן הבא: בהינתן מולטי קבוצה של מספרים שלמים, האם ניתן לחלק אותה לשתי מולטי קבוצות כך שסכום כל אחת מהן שווה? Anupam Gupta, 15-854: Approximations Algorithms - Dynamic Programming, 2005 פתרון חוקי לבעיה הוא פתרון המקיים את כל האילוצים של הבעיה מקור לקוד התוכנית: http://rosettacode.org/wiki/Knapsack_problem/0-1 Katherine Lai,The Knapsack Problem and Fully Polynomial Time Approximation Schemes (FPTAS)2006 Alexander Souza, Combinatorial Algorithms - Lecture Notes, Winter Term 10/11, page 40, 2011