Skip to Main Content

SQL & PL/SQL

Announcement

For appeals, questions and feedback about Oracle Forums, please email oracle-forums-moderators_us@oracle.com. Technical questions should be asked in the appropriate category. Thank you!

PL/SQL Implementation of Unbounded Knapsack Algorithm?

Dev_WinDec 6 2013 — edited Dec 16 2013

Hi everyone,

I am currently working on an algorithm to find the best possible combination depending on least cost to get >= the required quantity (It's very much an/similar to an unbounded KNAPSACK algorithm) . Say for example if 100 parts are required from the given pool of stores (see table data below) , i have to come up with a best possible combination. So in this example the best choice would be to pull from store_id (A, B, C ). i am currently experimenting with a record set limited to 20 which according to my approach (i.e brute force) that would be SELECT (POWER(2,20)) FROM DUAL;  1048576 * 20 iterations and it takes 30 plus seconds using pure PL/SQL . Just few more information I am performing couple of checks to see if a single store has a required qty then pick the store and exit else perform a cross join comparison ensuring that the same store_id's are not joined then finally if nothing gets me the required qty then i am going for a brute force approach. If there is a better way to short circuit the third step or any one have a similar problem and came up with a BETTER solution please let me know. It might help me and possibly many others ( SQL approach is available but not PL/SQL) . I tried looking for something similar online but was never able to find any PL/SQL implementation of Knapsack algorithm.

store_id     available_qty     cost_per_unit

A431.93
B342.01
C272.12
D242.18
E242.18
F24

2.18

Table script and insert statements for the above

DROP TABLE available_qty;

CREATE TABLE available_qty(store_id        VARCHAR2(1) PRIMARY KEY,

                             available_qty   NUMBER,

                             cost_per_unit   NUMBER);

INSERT INTO available_qty(

               store_id,

               available_qty,

               cost_per_unit)

     VALUES ('A',

             43,

             1.93);

INSERT INTO available_qty(

               store_id,

               available_qty,

               cost_per_unit)

     VALUES ('B',

             34,

             2.01);

INSERT INTO available_qty(

               store_id,

               available_qty,

               cost_per_unit)

     VALUES ('C',

             27,

             2.12);

INSERT INTO available_qty(

               store_id,

               available_qty,

               cost_per_unit)

     VALUES ('D',

             24,

             2.18);

INSERT INTO available_qty(

               store_id,

               available_qty,

               cost_per_unit)

     VALUES ('E',

             24,

             2.18);

INSERT INTO available_qty(

               store_id,

               available_qty,

               cost_per_unit)

     VALUES ('F',

             24,

             2.18);

COMMIT;

Package that i implemented ( I took ideas from multiple sources and came up with this, just so everyone know)

CREATE OR REPLACE PACKAGE get_available_qty

IS

   TYPE available_qty_typ IS RECORD(store_id          VARCHAR2(1),

                                    available_qty     NUMBER,

                                    cost_per_unit     NUMBER,

                                    best_combo_flag   VARCHAR2(1));

   TYPE available_qty_nt IS TABLE OF available_qty_typ;

   TYPE available_qty_aat IS TABLE OF available_qty_typ

      INDEX BY PLS_INTEGER;

   FUNCTION get_best_combo(p_qty_required NUMBER)

      RETURN available_qty_nt

      PIPELINED;

END;

/

CREATE OR REPLACE PACKAGE BODY get_available_qty

AS

   FUNCTION get_best_combo(p_qty_required NUMBER)

      RETURN available_qty_nt

      PIPELINED

   IS

      get_available_qty_nt   get_available_qty.available_qty_nt

                                := get_available_qty.available_qty_nt();

      --Local subprogram

      FUNCTION get_required_qty

         RETURN available_qty_nt

      IS

         v_qty                    NUMBER := 0;

         v_cost_per_unit          NUMBER := 0;

         v_cost_per_unit1         NUMBER := 0;

         v_best_store_id          VARCHAR2(4000) DEFAULT NULL;

         v_best_store_id_combo    VARCHAR2(4000);

         is_qty_available         BOOLEAN DEFAULT FALSE;

         is_first_run             BOOLEAN DEFAULT TRUE;

         v_assign_store_id_flag   DBMS_UTILITY.uncl_array;

         v_table_length           BINARY_INTEGER;

         -- Nested local subprogram

         FUNCTION return_null

            RETURN available_qty_nt

         IS

         BEGIN

            get_available_qty_nt.delete;

            RETURN get_available_qty_nt;

         END;

      BEGIN

         SELECT aq.*,

                'N' best_combo_flag

           BULK COLLECT INTO get_available_qty_nt

           FROM available_qty aq;

         FOR indx IN 1 .. get_available_qty_nt.COUNT LOOP

            v_qty   := v_qty + get_available_qty_nt(indx).available_qty;

         END LOOP;

         -- If total available qty is less than required qty then return null

         IF v_qty >= p_qty_required THEN

            is_qty_available   := TRUE;

         ELSE

            get_available_qty_nt   := return_null();

         END IF;

         --The below condition will execute when available qty >= required qty

         IF is_qty_available THEN

            --Check if any single store has the available qty!

            FOR rec IN 1 .. get_available_qty_nt.COUNT LOOP

               IF get_available_qty_nt(rec).available_qty >= p_qty_required THEN

                  IF (v_cost_per_unit > get_available_qty_nt(rec).cost_per_unit) THEN

                     v_cost_per_unit         := get_available_qty_nt(rec).cost_per_unit;

                     v_best_store_id_combo   := get_available_qty_nt(rec).store_id;

                  ELSIF (v_cost_per_unit = 0) THEN

                     v_cost_per_unit         := get_available_qty_nt(rec).cost_per_unit;

                     v_best_store_id_combo   := get_available_qty_nt(rec).store_id;

                  END IF;

               END IF;

            END LOOP;

            -- Reset local variables

            IF v_best_store_id_combo IS NOT NULL THEN

               is_qty_available   := FALSE;

            ELSE

               v_cost_per_unit         := 0;

               v_best_store_id_combo   := NULL;

            END IF;

            --If above condition does not satisfy then perfrom a cartesian product combination comparision

            IF is_qty_available THEN

               FOR i IN 1 .. get_available_qty_nt.COUNT LOOP

                  v_qty              := 0;

                  v_cost_per_unit1   := 0;

                  FOR j IN 1 .. get_available_qty_nt.COUNT LOOP

                     IF (get_available_qty_nt(i).store_id != get_available_qty_nt(j).store_id) THEN

                        v_qty                 :=   get_available_qty_nt(i).available_qty

                                                 + get_available_qty_nt(j).available_qty;

                        v_cost_per_unit1      :=   get_available_qty_nt(i).cost_per_unit

                                                 + get_available_qty_nt(j).cost_per_unit;

                        IF v_qty >= p_qty_required THEN

                           IF (v_cost_per_unit != 0

                           AND v_cost_per_unit1 < v_cost_per_unit) THEN

                              v_cost_per_unit            := v_cost_per_unit1;

                              v_best_store_id_combo      :=    get_available_qty_nt(i).store_id

                                                            || ','

                                                            || get_available_qty_nt(j).store_id;

                           ELSIF v_cost_per_unit = 0 THEN

                              v_cost_per_unit            := v_cost_per_unit1;

                              v_best_store_id_combo      :=    get_available_qty_nt(i).store_id

                                                            || ','

                                                            || get_available_qty_nt(j).store_id;

                           END IF;

                        END IF;

                     END IF;

                  END LOOP;

               END LOOP;

               -- Reset local variables

               IF v_best_store_id_combo IS NOT NULL THEN

                  is_qty_available   := FALSE;

               ELSE

                  v_cost_per_unit         := 0;

                  v_cost_per_unit1        := 0;

                  v_best_store_id_combo   := NULL;

               END IF;

               -- Perform every possible combination(Brute Force Approach)

               IF is_qty_available THEN

               FOR i IN 1 .. POWER (2, get_available_qty_nt.COUNT) - 1 LOOP

                    v_qty := 0;

                    v_best_store_id := NULL;

                    v_cost_per_unit := 0;

                  FOR j IN 1..get_available_qty_nt.COUNT LOOP

                        IF BITAND (i, POWER(2, j - 1)) != 0 THEN

                            v_qty := v_qty + get_available_qty_nt(j).available_qty;

                            v_best_store_id := v_best_store_id || get_available_qty_nt(j).store_id || ',';

                            v_cost_per_unit :=  ( v_cost_per_unit +  get_available_qty_nt(j).cost_per_unit);

                        END IF;

                    END LOOP;

                    IF (v_qty >= p_qty_required) THEN

                       IF is_first_run THEN

                         v_cost_per_unit1 := v_cost_per_unit;

                         is_first_run := FALSE;

                       END IF;

                       IF ( v_cost_per_unit <= v_cost_per_unit1 ) THEN

                         v_best_store_id_combo := RTRIM (v_best_store_id, ',');

                         v_cost_per_unit1 := v_cost_per_unit;

                       END IF;

                    END IF;

               END LOOP;

               END IF;

            END IF;

         END IF;

         -- Get the list for assigning flag

         DBMS_UTILITY.comma_to_table(list     => v_best_store_id_combo,

                                     tablen   => v_table_length,

                                     tab      => v_assign_store_id_flag);

         FOR k IN 1 .. v_assign_store_id_flag.COUNT LOOP

            FOR l IN 1 .. get_available_qty_nt.COUNT LOOP

               IF get_available_qty_nt(l).store_id = v_assign_store_id_flag(k) THEN

                  get_available_qty_nt(l).best_combo_flag   := 'Y';

               END IF;

            END LOOP;

         END LOOP;

         RETURN get_available_qty_nt;

      END;

   BEGIN

      get_available_qty_nt   := get_required_qty();

      FOR i IN 1 .. get_available_qty_nt.COUNT LOOP

         PIPE ROW (get_available_qty_nt(i));

      END LOOP;

      RETURN;

   END;

END get_available_qty;

-- When you call the function it returns the result set back with updated flags for stores with the most cost effective combination!

SELECT *

  FROM TABLE(get_available_qty.get_best_combo(100))

ORDER BY best_combo_flag DESC;

The best stores combination for 100 items are flagged 'Y'

store_id        available_qty        cost_per_unit                best_combo_flag

A431.93Y
B342.01Y
C272.12Y
D242.18N
E242.18N
F242.18N

SELECT *

  FROM TABLE(get_available_qty.get_best_combo(70))

ORDER BY best_combo_flag DESC;

The best store combination that will make up 70 or more items are flagged 'Y'

store_id        available_qty        cost_per_unit                best_combo_flag

A431.93Y
B342.01Y
E242.18N
D242.18N
F242.18N
C272.12N
This post has been answered by chris227 on Dec 6 2013
Jump to Answer
Comments
Locked Post
New comments cannot be posted to this locked post.
Post Details
Locked on Jan 13 2014
Added on Dec 6 2013
12 comments
5,786 views